trnsparse 0.6.0__tar.gz → 0.7.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.7.0}/CHANGELOG.md +55 -0
- {trnsparse-0.6.0/trnsparse.egg-info → trnsparse-0.7.0}/PKG-INFO +1 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/iterative_solvers.md +1 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/migration_scipy.md +4 -4
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/quickstart.md +2 -2
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/sparse_attention.md +7 -2
- {trnsparse-0.6.0 → trnsparse-0.7.0}/pyproject.toml +1 -1
- trnsparse-0.7.0/tests/test_attn_gather.py +49 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_nki_bsr.py +5 -1
- trnsparse-0.7.0/tests/test_nki_compile.py +143 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_nki_screened_spmm.py +5 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_nki_sim.py +79 -19
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_nki_spmm.py +5 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/__init__.py +1 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/nki/dispatch.py +98 -33
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/nki/kernels.py +27 -8
- {trnsparse-0.6.0 → trnsparse-0.7.0/trnsparse.egg-info}/PKG-INFO +1 -1
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse.egg-info/SOURCES.txt +2 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/.github/workflows/ci.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/.github/workflows/notify-umbrella.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/.github/workflows/publish.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/.gitignore +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/.pre-commit-config.yaml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/CLAUDE.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/CODE_OF_CONDUCT.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/CONTRIBUTING.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/LICENSE +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/README.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/bench_bsr_spmm.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/bench_iterative.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/bench_screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/bench_spmm.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/bench_spmv.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/benchmarks/conftest.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/api.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/architecture.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/aws_setup.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/benchmarks.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/index.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/docs/installation.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/examples/block_sparse_attention.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/examples/sparse_fock.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/infra/terraform/.terraform.lock.hcl +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/infra/terraform/README.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/infra/terraform/main.tf +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/mkdocs.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/scripts/bench_to_md.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/scripts/run_benchmarks.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/scripts/run_neuron_tests.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/scripts/run_simulator_tests.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/setup.cfg +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/conftest.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_attention.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_bsr.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_examples.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_formats.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_iterative.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_nki_attn.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_ops.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/tests/test_screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/formats.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/iterative.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/nki/__init__.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/ops.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse/screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse.egg-info/dependency_links.txt +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse.egg-info/requires.txt +0 -0
- {trnsparse-0.6.0 → trnsparse-0.7.0}/trnsparse.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,61 @@ 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.7.0] — 2026-10-28
|
|
11
|
+
|
|
12
|
+
Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron device) — see trnsci/trnsci#35.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **NKI 0.7.0 / neuronx-cc 2.28 kernel compatibility (#29).** Kernels that
|
|
17
|
+
passed `nki.simulate` failed to compile on Trn2 (Neuron SDK 2.33):
|
|
18
|
+
- `_attn_out_kernel`, `_attn_bwd_dq_kernel`, `_attn_bwd_dkdv_kernel`:
|
|
19
|
+
softmax normalisation `nl.divide(exp, row_denom)` by a `(128, 1)`
|
|
20
|
+
denominator lowered to `tensor_scalar_arith`, which no longer supports
|
|
21
|
+
`divide`. Now multiplies by `nl.reciprocal(row_denom)` (hoisted per
|
|
22
|
+
block-row in the forward and dQ kernels).
|
|
23
|
+
- `_screened_spmm_kernel`: the `(128, 1) * (1, 128)` Schwarz outer product
|
|
24
|
+
and the `(1, 1)` threshold subtract produced partition-count mismatches
|
|
25
|
+
(`operand0 partition total elements 128 != dst partition total elements
|
|
26
|
+
1`). `q_k` is now `nl.broadcast_to` `(128, 128)` and the threshold is
|
|
27
|
+
loaded once and broadcast to `(128, 1)`. The soft-mask tensor-tensor
|
|
28
|
+
divide (fails ISA validation `s3s3d3_tt_valid_op`) is now a reciprocal
|
|
29
|
+
multiply.
|
|
30
|
+
- `nki_bsr_attn_bwd`: the host-side `(..., b) → (..., b, 1)` unsqueeze
|
|
31
|
+
for per-row stats matched `q_scaled` / `k_blocks` / `v_blocks` by shape
|
|
32
|
+
whenever `head_dim == 128` (and in the simulator's padded path), feeding
|
|
33
|
+
the kernels a 4-D tensor. Stats are now selected by name.
|
|
34
|
+
- Dispatch: kernels always receive fp32 (Trainium has no fp64 datapath;
|
|
35
|
+
the native device downcast fp64 silently) and results are cast back to
|
|
36
|
+
the caller's dtype. Fixes `expected m1 and m2 to have the same dtype,
|
|
37
|
+
but got: float != double` in the PyTorch-level backward under fp64
|
|
38
|
+
gradcheck. The hardware gradcheck tests now use `eps=1e-2,
|
|
39
|
+
atol=rtol=1e-2`, sized for an fp32 forward (the ops are linear in each
|
|
40
|
+
input, so the larger eps adds no truncation error).
|
|
41
|
+
|
|
42
|
+
- **Block-sparse attention: two host-side gather bugs** (pre-existing, masked
|
|
43
|
+
on Trn2 by the compile failures above; found with neuronx-cc BIRSim):
|
|
44
|
+
- Rows with fewer than `K_max` blocks were padded with sentinel k-slots that
|
|
45
|
+
gathered column block 0, so block 0 was counted again in the softmax and
|
|
46
|
+
in `weights @ V` (local-window masks: ~49% of outputs wrong). Padded slots
|
|
47
|
+
now have K/V (forward, dQ) or Q/dO (dK/dV) zeroed, and their stats are
|
|
48
|
+
dropped in `_attn_host_reduction`.
|
|
49
|
+
- `_attn_bwd_gather` gathered `dO` by column block instead of the row block
|
|
50
|
+
`dO_m`, breaking dQ whenever a row attends to a block other than itself
|
|
51
|
+
(~99% of dQ wrong). The two simulator xfails attributed to a "PSUM
|
|
52
|
+
accumulate bug" (`test_bwd_dq_parity_kmax2`, `test_backward_head_dim_256`)
|
|
53
|
+
were this bug and now pass.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `tests/test_attn_gather.py`: CPU regression tests for both gather bugs.
|
|
58
|
+
- `tests/test_nki_compile.py` (`nki_simulator` marker): CPU MLIR
|
|
59
|
+
verification (`source_to_mlir(...).verify()`) of every kernel for trn1
|
|
60
|
+
and trn2, attention kernels at head_dim 64, 128 and 256; plus a full NEFF
|
|
61
|
+
compile per kernel/target when `neuronx-cc` is on PATH.
|
|
62
|
+
|
|
8
63
|
## [0.6.0] — 2026-04-17
|
|
9
64
|
|
|
10
65
|
### Added
|
|
@@ -65,7 +65,7 @@ def _cg_spd_kernel(A_blocks, A_cols, A_row_ptrs, b, max_iter):
|
|
|
65
65
|
rr = nl.reduce(r * r)
|
|
66
66
|
|
|
67
67
|
for k in nl.affine_range(max_iter):
|
|
68
|
-
Ap = _bsr_matvec_sbuf(A_sbuf, A_cols, A_row_ptrs, p)
|
|
68
|
+
Ap = _bsr_matvec_sbuf(A_sbuf, A_cols, A_row_ptrs, p) # all SBUF
|
|
69
69
|
pAp = nl.reduce(p * Ap)
|
|
70
70
|
alpha = rr / pAp
|
|
71
71
|
x = x + alpha * p
|
|
@@ -60,10 +60,10 @@ argument; scipy infers it from the indices when possible. Be explicit:
|
|
|
60
60
|
|
|
61
61
|
```python
|
|
62
62
|
# scipy
|
|
63
|
-
A = csr_matrix((data, indices, indptr))
|
|
63
|
+
A = csr_matrix((data, indices, indptr)) # shape inferred
|
|
64
64
|
|
|
65
65
|
# trnsparse
|
|
66
|
-
A = CSRMatrix(data, indices, indptr, shape=(m, n))
|
|
66
|
+
A = CSRMatrix(data, indices, indptr, shape=(m, n)) # shape required
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
**Operator access.** scipy overloads `@`, `+`, `*` on its matrix
|
|
@@ -107,9 +107,9 @@ import torch
|
|
|
107
107
|
import trnsparse
|
|
108
108
|
import trnsolver
|
|
109
109
|
|
|
110
|
-
A = trnsparse.from_scipy(scipy_matrix)
|
|
110
|
+
A = trnsparse.from_scipy(scipy_matrix) # zero-copy ingestion
|
|
111
111
|
b = torch.as_tensor(b_numpy, dtype=torch.float32)
|
|
112
|
-
x = trnsolver.cg(A, b, tol=1e-6)
|
|
112
|
+
x = trnsolver.cg(A, b, tol=1e-6) # uses trnsparse.spmv
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
The hot path (`spmv` inside CG) dispatches to NKI when
|
|
@@ -42,7 +42,7 @@ keep = trnsparse.screen_quartets(Q, threshold=1e-10)
|
|
|
42
42
|
## Backend selection
|
|
43
43
|
|
|
44
44
|
```python
|
|
45
|
-
trnsparse.set_backend("auto")
|
|
45
|
+
trnsparse.set_backend("auto") # default
|
|
46
46
|
trnsparse.set_backend("pytorch") # force PyTorch fallback
|
|
47
|
-
trnsparse.set_backend("nki")
|
|
47
|
+
trnsparse.set_backend("nki") # force NKI (requires Neuron hardware)
|
|
48
48
|
```
|
|
@@ -28,9 +28,11 @@ Each token block attends to its `window` nearest neighbors in block space:
|
|
|
28
28
|
```python
|
|
29
29
|
import trnsparse
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
def local_window_mask(seq_len: int, block_size: int, window: int) -> trnsparse.BSRMatrix:
|
|
32
33
|
"""BSRMatrix for sliding-window attention."""
|
|
33
34
|
import torch
|
|
35
|
+
|
|
34
36
|
n_blocks = seq_len // block_size
|
|
35
37
|
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
36
38
|
for i in range(n_blocks):
|
|
@@ -53,6 +55,7 @@ iff `(i - j) % stride == 0`:
|
|
|
53
55
|
```python
|
|
54
56
|
def dilated_mask(seq_len: int, block_size: int, stride: int) -> trnsparse.BSRMatrix:
|
|
55
57
|
import torch
|
|
58
|
+
|
|
56
59
|
n_blocks = seq_len // block_size
|
|
57
60
|
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
58
61
|
for i in range(n_blocks):
|
|
@@ -73,10 +76,11 @@ def global_token_mask(
|
|
|
73
76
|
seq_len: int, block_size: int, window: int, n_global: int
|
|
74
77
|
) -> trnsparse.BSRMatrix:
|
|
75
78
|
import torch
|
|
79
|
+
|
|
76
80
|
n_blocks = seq_len // block_size
|
|
77
81
|
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
78
|
-
block_mask[:n_global, :] = True
|
|
79
|
-
block_mask[:, :n_global] = True
|
|
82
|
+
block_mask[:n_global, :] = True # global rows attend everywhere
|
|
83
|
+
block_mask[:, :n_global] = True # global columns attended by all
|
|
80
84
|
for i in range(n_global, n_blocks):
|
|
81
85
|
lo, hi = max(0, i - window), min(n_blocks, i + window + 1)
|
|
82
86
|
block_mask[i, lo:hi] = True
|
|
@@ -91,6 +95,7 @@ Given a `mask_bsr` constructed above:
|
|
|
91
95
|
```python
|
|
92
96
|
import torch
|
|
93
97
|
|
|
98
|
+
|
|
94
99
|
def block_sparse_attention(Q, K, V, mask_bsr):
|
|
95
100
|
"""Q, K, V: (seq_len, head_dim) → (seq_len, head_dim)."""
|
|
96
101
|
scale = Q.shape[-1] ** -0.5
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Host-side gather for the NKI attention kernels (CPU, no nki needed).
|
|
2
|
+
|
|
3
|
+
Regression tests for two #29 bugs the simulator tests did not exercise:
|
|
4
|
+
the dQ gather fed dO by column block instead of row block, and padded
|
|
5
|
+
k-slots (rows with fewer than K_max blocks) gathered block 0 and were
|
|
6
|
+
counted in the softmax.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
import trnsparse
|
|
14
|
+
from trnsparse.nki.dispatch import _attn_bwd_gather, _attn_gather, _attn_slot_valid
|
|
15
|
+
|
|
16
|
+
B = 128
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _local_bsr(n_blocks: int, window: int):
|
|
20
|
+
bm = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
21
|
+
for i in range(n_blocks):
|
|
22
|
+
bm[i, max(0, i - window) : i + window + 1] = True
|
|
23
|
+
dense = bm.repeat_interleave(B, 0).repeat_interleave(B, 1).float()
|
|
24
|
+
return trnsparse.BSRMatrix.from_dense(dense, block_size=B)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_bwd_gather_do_is_row_block():
|
|
28
|
+
torch.manual_seed(0)
|
|
29
|
+
S, H = 4 * B, 64
|
|
30
|
+
Q, K, V, dO, O = (torch.randn(S, H) for _ in range(5))
|
|
31
|
+
mb = _local_bsr(4, window=1)
|
|
32
|
+
stats = torch.zeros(4, B)
|
|
33
|
+
rf, _ = _attn_bwd_gather(Q, K, V, dO, O, mb, H**-0.5, stats, stats + 1)
|
|
34
|
+
do_g = rf["do_gathered"]
|
|
35
|
+
for m in range(4):
|
|
36
|
+
for slot in range(do_g.shape[1]):
|
|
37
|
+
torch.testing.assert_close(do_g[m, slot], dO[m * B : (m + 1) * B])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_padded_slots_zeroed():
|
|
41
|
+
torch.manual_seed(1)
|
|
42
|
+
S, H = 4 * B, 64
|
|
43
|
+
Q, K, V = (torch.randn(S, H) for _ in range(3))
|
|
44
|
+
mb = _local_bsr(4, window=2) # rows hold 3, 4, 4, 3 blocks -> K_max = 4
|
|
45
|
+
_, kg, vg, K_max, M_tiles = _attn_gather(Q, K, V, mb, H**-0.5)
|
|
46
|
+
valid = _attn_slot_valid(mb, M_tiles, K_max)
|
|
47
|
+
assert K_max == 4
|
|
48
|
+
assert valid.sum(dim=1).tolist() == [3, 4, 4, 3]
|
|
49
|
+
assert (kg[~valid] == 0).all() and (vg[~valid] == 0).all()
|
|
@@ -110,4 +110,8 @@ class TestNkiBSRDifferentiability:
|
|
|
110
110
|
B,
|
|
111
111
|
)
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
# The kernel runs in fp32 (Trainium has no fp64 datapath; dispatch
|
|
114
|
+
# downcasts and casts the result back), so fp64 finite differences at
|
|
115
|
+
# eps=1e-6 would measure fp32 roundoff, not the gradient. The op is
|
|
116
|
+
# linear in each input, so a large eps has no truncation error.
|
|
117
|
+
assert torch.autograd.gradcheck(func, (bsr.blocks, B), eps=1e-2, atol=1e-2, rtol=1e-2)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""CPU compile check: every NKI kernel lowers to MLIR that passes verification.
|
|
2
|
+
|
|
3
|
+
`nki.simulate` does not run the MLIR verifier, so a kernel can pass every
|
|
4
|
+
simulator test and still fail to compile for hardware. trnsparse#29 was such
|
|
5
|
+
a case on NKI 0.7.0: `nl.divide` by a `(P, 1)` softmax denominator lowered to
|
|
6
|
+
`nisa.tensor_scalar_arith`, which no longer supports `divide`, and the
|
|
7
|
+
screened-SpMM outer product `(P, 1) * (1, F)` lowered to a tensor-scalar op
|
|
8
|
+
whose 1-partition destination did not match its 128-partition operand.
|
|
9
|
+
|
|
10
|
+
This module runs the NKI frontend (trace -> MLIR -> verify) on CPU for every
|
|
11
|
+
kernel and target. It needs `nki` but no Neuron device and no `neuronx-cc`.
|
|
12
|
+
MLIR verification cannot catch errors raised later, in the backend: a
|
|
13
|
+
tensor_tensor `divide` passes `verify()` but fails neuronx-cc ISA validation
|
|
14
|
+
(`s3s3d3_tt_valid_op`). When `neuronx-cc` is on PATH, `test_kernel_neff_compiles`
|
|
15
|
+
additionally runs the full NEFF compile on CPU (no device needed).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import inspect
|
|
21
|
+
import shutil
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
pytestmark = pytest.mark.nki_simulator
|
|
27
|
+
|
|
28
|
+
from trnsparse.nki.kernels import HAS_NKI # noqa: E402
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from nki.compiler.frontend import FrontendKind, source_to_mlir
|
|
32
|
+
except ImportError:
|
|
33
|
+
source_to_mlir = None
|
|
34
|
+
|
|
35
|
+
_P = 128
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _f32(*shape):
|
|
39
|
+
rng = np.random.default_rng(0)
|
|
40
|
+
return rng.standard_normal(shape).astype(np.float32)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _attn_inputs(hd):
|
|
44
|
+
# (M_tiles=2, K_max=2) — shapes match nki_bsr_attn_tiled / nki_bsr_attn_bwd.
|
|
45
|
+
return {
|
|
46
|
+
"_attn_stats_kernel": (_f32(2, _P, hd), _f32(2, 2, _P, hd)),
|
|
47
|
+
"_attn_out_kernel": (
|
|
48
|
+
_f32(2, _P, hd),
|
|
49
|
+
_f32(2, 2, _P, hd),
|
|
50
|
+
_f32(2, 2, _P, hd),
|
|
51
|
+
_f32(2, _P, 1),
|
|
52
|
+
np.abs(_f32(2, _P, 1)) + 1.0,
|
|
53
|
+
),
|
|
54
|
+
"_attn_bwd_dq_kernel": (
|
|
55
|
+
_f32(2, _P, hd),
|
|
56
|
+
_f32(2, 2, _P, hd),
|
|
57
|
+
_f32(2, 2, _P, hd),
|
|
58
|
+
_f32(2, 2, _P, hd),
|
|
59
|
+
_f32(2, _P, 1),
|
|
60
|
+
_f32(2, _P, 1),
|
|
61
|
+
np.abs(_f32(2, _P, 1)) + 1.0,
|
|
62
|
+
),
|
|
63
|
+
"_attn_bwd_dkdv_kernel": (
|
|
64
|
+
_f32(2, _P, hd),
|
|
65
|
+
_f32(2, _P, hd),
|
|
66
|
+
_f32(2, 2, _P, hd),
|
|
67
|
+
_f32(2, 2, _P, hd),
|
|
68
|
+
_f32(2, 2, _P, 1),
|
|
69
|
+
_f32(2, 2, _P, 1),
|
|
70
|
+
np.abs(_f32(2, 2, _P, 1)) + 1.0,
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cases():
|
|
76
|
+
cases = {
|
|
77
|
+
("_spmm_dense_kernel", "base"): lambda: (_f32(256, 256), _f32(256, 128)),
|
|
78
|
+
("_bsr_spmm_kernel", "base"): lambda: (_f32(2, 2, _P, _P), _f32(2, 2, _P, 128)),
|
|
79
|
+
("_screened_spmm_kernel", "base"): lambda: (
|
|
80
|
+
_f32(256, 256),
|
|
81
|
+
np.abs(_f32(256, 1)),
|
|
82
|
+
np.full((1, 1), 0.5, dtype=np.float32),
|
|
83
|
+
_f32(256, 128),
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
# head_dim 64 / 128: single-tile path; head_dim=256: K-tiled path.
|
|
87
|
+
for hd in (64, 128, 256):
|
|
88
|
+
for name in _attn_inputs(hd):
|
|
89
|
+
cases[(name, f"hd{hd}")] = lambda name=name, hd=hd: _attn_inputs(hd)[name]
|
|
90
|
+
return cases
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_CASES = _cases()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture(autouse=True)
|
|
97
|
+
def _frontend_available():
|
|
98
|
+
if not HAS_NKI:
|
|
99
|
+
pytest.skip("nki not importable on this host")
|
|
100
|
+
if source_to_mlir is None:
|
|
101
|
+
pytest.skip("nki.compiler.frontend.source_to_mlir not available in this nki")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("target", ["trn1", "trn2"])
|
|
105
|
+
@pytest.mark.parametrize("case", sorted(_CASES), ids=lambda c: f"{c[0]}-{c[1]}")
|
|
106
|
+
def test_kernel_mlir_verifies(case, target):
|
|
107
|
+
from trnsparse.nki import kernels
|
|
108
|
+
|
|
109
|
+
kernel = getattr(kernels, case[0])
|
|
110
|
+
result = source_to_mlir(kernel, *_CASES[case](), target=target, frontend=FrontendKind.TRACER)
|
|
111
|
+
result.verify()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.skipif(shutil.which("neuronx-cc") is None, reason="neuronx-cc not on PATH")
|
|
115
|
+
@pytest.mark.parametrize("target", ["trn1", "trn2"])
|
|
116
|
+
@pytest.mark.parametrize("case", sorted(_CASES), ids=lambda c: f"{c[0]}-{c[1]}")
|
|
117
|
+
def test_kernel_neff_compiles(case, target, tmp_path):
|
|
118
|
+
"""Full CPU compile (MLIR -> BIR -> neuronx-cc NEFF); catches ISA-validation errors."""
|
|
119
|
+
try:
|
|
120
|
+
from nki.compiler import driver
|
|
121
|
+
from nki.compiler.frontend import TracerFrontend
|
|
122
|
+
from nki.compiler.ncc_driver import CompileOptions
|
|
123
|
+
except ImportError as e:
|
|
124
|
+
pytest.skip(f"nki compiler driver API not available: {e}")
|
|
125
|
+
if not hasattr(driver, "_compile_bir_to_neff"):
|
|
126
|
+
pytest.skip("nki.compiler.driver._compile_bir_to_neff not available in this nki")
|
|
127
|
+
|
|
128
|
+
from trnsparse.nki import kernels
|
|
129
|
+
|
|
130
|
+
kernel = getattr(kernels, case[0])
|
|
131
|
+
fn = getattr(kernel, "func", kernel)
|
|
132
|
+
inputs = dict(zip(inspect.signature(fn).parameters, _CASES[case](), strict=True))
|
|
133
|
+
opts = CompileOptions(
|
|
134
|
+
target=target,
|
|
135
|
+
lnc=1,
|
|
136
|
+
artifacts_dir=str(tmp_path),
|
|
137
|
+
output_path=str(tmp_path / "file.neff"),
|
|
138
|
+
)
|
|
139
|
+
bir = driver.compile_to_bir(
|
|
140
|
+
fn, frontend=TracerFrontend(), inputs=inputs, compile_opts=opts, enable_cache=False
|
|
141
|
+
)
|
|
142
|
+
driver._compile_bir_to_neff(bir, opts, inputs)
|
|
143
|
+
assert (tmp_path / "file.neff").stat().st_size > 0
|
|
@@ -90,4 +90,8 @@ class TestNkiScreenedSpmmDifferentiability:
|
|
|
90
90
|
def func(a, b):
|
|
91
91
|
return _ScreenedSpMMFunction.apply(a, diag, 0.5, b)
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
# The kernel runs in fp32 (Trainium has no fp64 datapath; dispatch
|
|
94
|
+
# downcasts and casts the result back), so fp64 finite differences at
|
|
95
|
+
# eps=1e-6 would measure fp32 roundoff, not the gradient. The op is
|
|
96
|
+
# linear in each input, so a large eps has no truncation error.
|
|
97
|
+
assert torch.autograd.gradcheck(func, (A, B), eps=1e-2, atol=1e-2, rtol=1e-2)
|
|
@@ -262,32 +262,84 @@ class TestAttnBwdSimulator:
|
|
|
262
262
|
assert Kr.grad is not None and Kr.grad.shape == K.shape
|
|
263
263
|
assert Vr.grad is not None and Vr.grad.shape == V.shape
|
|
264
264
|
|
|
265
|
-
@pytest.mark.xfail(
|
|
266
|
-
strict=False,
|
|
267
|
-
reason="NKI simulator: dQ backward has ~1.0 systematic error under investigation; "
|
|
268
|
-
"dK/dV correct; hardware path unaffected",
|
|
269
|
-
)
|
|
270
265
|
def test_bwd_dq_parity(self, nki_backend):
|
|
271
|
-
"""NKI dQ
|
|
266
|
+
"""NKI dQ finite-diff parity (spot-check 4 rows via NKI forward).
|
|
267
|
+
|
|
268
|
+
The previous approach compared against _pytorch_grads which uses
|
|
269
|
+
O_pytorch in D=(dO·O). The NKI backward uses O_NKI from ctx; since
|
|
270
|
+
O_NKI ≠ O_pytorch (within 1e-3), D differs and dQ diverges even when
|
|
271
|
+
the kernel is correct. Finite differences through the NKI forward are
|
|
272
|
+
the right reference.
|
|
273
|
+
"""
|
|
272
274
|
torch.manual_seed(31)
|
|
273
275
|
seq_len, head_dim, block_size = 256, 32, 128
|
|
274
276
|
|
|
275
277
|
Q = torch.randn(seq_len, head_dim)
|
|
276
278
|
K = torch.randn(seq_len, head_dim)
|
|
277
279
|
V = torch.randn(seq_len, head_dim)
|
|
278
|
-
|
|
280
|
+
# Use dilated mask (K_max=1 per row) to isolate single-iteration accumulation.
|
|
281
|
+
# Local window (K_max=2) exposes a multi-iteration PSUM accumulation issue
|
|
282
|
+
# in the NKI 0.3.0 simulator backward — tracked separately.
|
|
283
|
+
mask = _dilated_mask(seq_len, block_size, stride=2)
|
|
279
284
|
mask_bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
280
285
|
|
|
281
|
-
|
|
286
|
+
Qr = Q.clone().requires_grad_(True)
|
|
287
|
+
Kr = K.clone().requires_grad_(True)
|
|
288
|
+
Vr = V.clone().requires_grad_(True)
|
|
289
|
+
out = trnsparse.block_sparse_attention_tiled(Qr, Kr, Vr, mask_bsr)
|
|
290
|
+
dO = torch.randn_like(out)
|
|
291
|
+
out.backward(dO)
|
|
292
|
+
|
|
293
|
+
# Finite-difference reference through the same NKI forward
|
|
294
|
+
eps = 1e-3
|
|
295
|
+
dQ_fd = torch.zeros_like(Q)
|
|
296
|
+
for i in range(min(4, seq_len)):
|
|
297
|
+
for j in range(head_dim):
|
|
298
|
+
Qp, Qm = Q.clone(), Q.clone()
|
|
299
|
+
Qp[i, j] += eps
|
|
300
|
+
Qm[i, j] -= eps
|
|
301
|
+
fp = trnsparse.block_sparse_attention_tiled(Qp, K, V, mask_bsr)
|
|
302
|
+
fm = trnsparse.block_sparse_attention_tiled(Qm, K, V, mask_bsr)
|
|
303
|
+
dQ_fd[i, j] = ((fp - fm) * dO).sum() / (2 * eps)
|
|
304
|
+
|
|
305
|
+
torch.testing.assert_close(Qr.grad[:4], dQ_fd[:4], atol=1e-2, rtol=1e-2)
|
|
306
|
+
|
|
307
|
+
@pytest.mark.xfail(
|
|
308
|
+
strict=False,
|
|
309
|
+
reason="NKI simulator: dq_psum accumulate=True across K_max>1 iterations "
|
|
310
|
+
"appears broken — only last ki result retained. Hypothesis: NKI 0.3.0 "
|
|
311
|
+
"simulator PSUM accumulate bug for multi-iteration inner loops. "
|
|
312
|
+
"Hardware path unaffected (K_max=1 dilated test above passes).",
|
|
313
|
+
)
|
|
314
|
+
def test_bwd_dq_parity_kmax2(self, nki_backend):
|
|
315
|
+
"""dQ backward with K_max=2 (local window) — exposes PSUM accumulation issue."""
|
|
316
|
+
torch.manual_seed(31)
|
|
317
|
+
seq_len, head_dim, block_size = 256, 32, 128
|
|
318
|
+
Q = torch.randn(seq_len, head_dim)
|
|
319
|
+
K = torch.randn(seq_len, head_dim)
|
|
320
|
+
V = torch.randn(seq_len, head_dim)
|
|
321
|
+
mask = _local_mask(seq_len, block_size, window=1) # K_max=2 per row
|
|
322
|
+
mask_bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
282
323
|
|
|
283
|
-
trnsparse.set_backend("nki")
|
|
284
324
|
Qr = Q.clone().requires_grad_(True)
|
|
285
325
|
Kr = K.clone().requires_grad_(True)
|
|
286
326
|
Vr = V.clone().requires_grad_(True)
|
|
287
327
|
out = trnsparse.block_sparse_attention_tiled(Qr, Kr, Vr, mask_bsr)
|
|
328
|
+
dO = torch.randn_like(out)
|
|
288
329
|
out.backward(dO)
|
|
289
330
|
|
|
290
|
-
|
|
331
|
+
eps = 1e-3
|
|
332
|
+
dQ_fd = torch.zeros_like(Q)
|
|
333
|
+
for i in range(min(4, seq_len)):
|
|
334
|
+
for j in range(head_dim):
|
|
335
|
+
Qp, Qm = Q.clone(), Q.clone()
|
|
336
|
+
Qp[i, j] += eps
|
|
337
|
+
Qm[i, j] -= eps
|
|
338
|
+
fp = trnsparse.block_sparse_attention_tiled(Qp, K, V, mask_bsr)
|
|
339
|
+
fm = trnsparse.block_sparse_attention_tiled(Qm, K, V, mask_bsr)
|
|
340
|
+
dQ_fd[i, j] = ((fp - fm) * dO).sum() / (2 * eps)
|
|
341
|
+
|
|
342
|
+
torch.testing.assert_close(Qr.grad[:4], dQ_fd[:4], atol=1e-2, rtol=1e-2)
|
|
291
343
|
|
|
292
344
|
def test_bwd_dkdv_parity(self, nki_backend):
|
|
293
345
|
"""NKI dK+dV match PyTorch at atol=1e-3, dilated mask."""
|
|
@@ -342,31 +394,39 @@ class TestAttnKTilingSimulator:
|
|
|
342
394
|
|
|
343
395
|
@pytest.mark.xfail(
|
|
344
396
|
strict=False,
|
|
345
|
-
reason="NKI simulator:
|
|
397
|
+
reason="NKI simulator: dq_psum accumulate=True broken for K_max=2 "
|
|
398
|
+
"(local window mask). Same PSUM multi-iteration issue as test_bwd_dq_parity_kmax2.",
|
|
346
399
|
)
|
|
347
400
|
def test_backward_head_dim_256(self, nki_backend):
|
|
348
|
-
"""NKI dQ
|
|
401
|
+
"""NKI dQ finite-diff parity at head_dim=256 (spot-check 4 rows)."""
|
|
349
402
|
torch.manual_seed(61)
|
|
350
403
|
seq_len, head_dim, block_size = 256, 256, 128
|
|
351
404
|
|
|
352
405
|
Q = torch.randn(seq_len, head_dim)
|
|
353
406
|
K = torch.randn(seq_len, head_dim)
|
|
354
407
|
V = torch.randn(seq_len, head_dim)
|
|
355
|
-
mask = _local_mask(seq_len, block_size, window=1)
|
|
408
|
+
mask = _local_mask(seq_len, block_size, window=1) # K_max=2
|
|
356
409
|
mask_bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
357
410
|
|
|
358
|
-
dQ_ref, dK_ref, dV_ref, dO = _pytorch_grads(Q, K, V, mask_bsr)
|
|
359
|
-
|
|
360
|
-
trnsparse.set_backend("nki")
|
|
361
411
|
Qr = Q.clone().requires_grad_(True)
|
|
362
412
|
Kr = K.clone().requires_grad_(True)
|
|
363
413
|
Vr = V.clone().requires_grad_(True)
|
|
364
414
|
out = trnsparse.block_sparse_attention_tiled(Qr, Kr, Vr, mask_bsr)
|
|
415
|
+
dO = torch.randn_like(out)
|
|
365
416
|
out.backward(dO)
|
|
366
417
|
|
|
367
|
-
|
|
368
|
-
torch.
|
|
369
|
-
|
|
418
|
+
eps = 1e-3
|
|
419
|
+
dQ_fd = torch.zeros(4, head_dim)
|
|
420
|
+
for i in range(4):
|
|
421
|
+
for j in range(head_dim):
|
|
422
|
+
Qp, Qm = Q.clone(), Q.clone()
|
|
423
|
+
Qp[i, j] += eps
|
|
424
|
+
Qm[i, j] -= eps
|
|
425
|
+
fp = trnsparse.block_sparse_attention_tiled(Qp, K, V, mask_bsr)
|
|
426
|
+
fm = trnsparse.block_sparse_attention_tiled(Qm, K, V, mask_bsr)
|
|
427
|
+
dQ_fd[i, j] = ((fp - fm) * dO).sum() / (2 * eps)
|
|
428
|
+
|
|
429
|
+
torch.testing.assert_close(Qr.grad[:4], dQ_fd, atol=1e-2, rtol=1e-2)
|
|
370
430
|
|
|
371
431
|
def test_forward_dilated_head_dim_256(self, nki_backend):
|
|
372
432
|
"""K-tiling forward with dilated pattern matches PyTorch."""
|
|
@@ -81,7 +81,11 @@ class TestNkiSpmmDifferentiability:
|
|
|
81
81
|
def func(a, b):
|
|
82
82
|
return _SpMMFunction.apply(a, b)
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
# The kernel runs in fp32 (Trainium has no fp64 datapath; dispatch
|
|
85
|
+
# downcasts and casts the result back), so fp64 finite differences at
|
|
86
|
+
# eps=1e-6 would measure fp32 roundoff, not the gradient. The op is
|
|
87
|
+
# linear in each input, so a large eps has no truncation error.
|
|
88
|
+
assert torch.autograd.gradcheck(func, (A_dense, B), eps=1e-2, atol=1e-2, rtol=1e-2)
|
|
85
89
|
|
|
86
90
|
def test_backward_finite(self, nki_backend):
|
|
87
91
|
"""Smoke test: backward produces finite gradients end-to-end."""
|
|
@@ -5,7 +5,7 @@ CSR/COO formats, SpMV, SpMM, and integral screening for
|
|
|
5
5
|
sparse scientific computing. Part of the trnsci scientific computing suite.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.7.0"
|
|
9
9
|
|
|
10
10
|
from .formats import BSRMatrix, COOMatrix, CSRMatrix, eye_sparse, from_dense, from_scipy
|
|
11
11
|
from .iterative import (
|
|
@@ -94,6 +94,19 @@ def _to_xla(*tensors):
|
|
|
94
94
|
return [t.to(device) for t in tensors], orig
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _kernel_input(t: torch.Tensor) -> torch.Tensor:
|
|
98
|
+
"""Cast a kernel operand to a dtype the NeuronCore datapath supports.
|
|
99
|
+
|
|
100
|
+
Trainium has no fp64 datapath; the native device silently hands fp64
|
|
101
|
+
operands to the kernel as fp32, and the kernel output (whose dtype
|
|
102
|
+
follows its input) then comes back as fp32. Downcast explicitly so the
|
|
103
|
+
kernel always sees fp32, and have callers cast the result back to the
|
|
104
|
+
caller's dtype — otherwise fp64 autograd inputs (gradcheck) hit a
|
|
105
|
+
float/double mix in the PyTorch-level backward.
|
|
106
|
+
"""
|
|
107
|
+
return t.float() if t.dtype == torch.float64 else t
|
|
108
|
+
|
|
109
|
+
|
|
97
110
|
def _csr_to_dense_padded(A) -> torch.Tensor:
|
|
98
111
|
"""Materialize a CSRMatrix into a dense (M, K) tensor.
|
|
99
112
|
|
|
@@ -133,6 +146,7 @@ def _nki_spmm_impl(A_dense: torch.Tensor, B: torch.Tensor) -> torch.Tensor:
|
|
|
133
146
|
A_feed, B_feed = A_p.contiguous(), B_p.contiguous()
|
|
134
147
|
else:
|
|
135
148
|
A_feed, B_feed = A_dense.contiguous(), B.contiguous()
|
|
149
|
+
A_feed, B_feed = _kernel_input(A_feed), _kernel_input(B_feed)
|
|
136
150
|
|
|
137
151
|
if _use_simulator():
|
|
138
152
|
# CPU-side: feed NumPy arrays to nki.simulate(kernel). Bypasses
|
|
@@ -144,6 +158,7 @@ def _nki_spmm_impl(A_dense: torch.Tensor, B: torch.Tensor) -> torch.Tensor:
|
|
|
144
158
|
c = _spmm_dense_kernel(a, b)
|
|
145
159
|
result = c.to(orig_device)
|
|
146
160
|
|
|
161
|
+
result = result.to(A_dense.dtype)
|
|
147
162
|
return result[:M, :N] if needs_pad else result
|
|
148
163
|
except Exception:
|
|
149
164
|
if _REQUIRE_NKI:
|
|
@@ -202,8 +217,8 @@ def _nki_bsr_spmm_impl(
|
|
|
202
217
|
if not HAS_NKI:
|
|
203
218
|
raise RuntimeError("NKI not available")
|
|
204
219
|
try:
|
|
205
|
-
bp_feed = blocks_pad.contiguous()
|
|
206
|
-
bg_feed = b_gathered.contiguous()
|
|
220
|
+
bp_feed = _kernel_input(blocks_pad.contiguous())
|
|
221
|
+
bg_feed = _kernel_input(b_gathered.contiguous())
|
|
207
222
|
|
|
208
223
|
if _use_simulator():
|
|
209
224
|
out_np = nki.simulate(_bsr_spmm_kernel)(bp_feed.cpu().numpy(), bg_feed.cpu().numpy())
|
|
@@ -213,7 +228,7 @@ def _nki_bsr_spmm_impl(
|
|
|
213
228
|
c = _bsr_spmm_kernel(bp, bg)
|
|
214
229
|
result = c.to(orig_device)
|
|
215
230
|
|
|
216
|
-
return result[:out_rows, :out_cols].contiguous()
|
|
231
|
+
return result[:out_rows, :out_cols].to(blocks_pad.dtype).contiguous()
|
|
217
232
|
except Exception:
|
|
218
233
|
if _REQUIRE_NKI:
|
|
219
234
|
raise
|
|
@@ -419,6 +434,8 @@ def _nki_screened_spmm_impl(
|
|
|
419
434
|
A_feed, Q_feed, B_feed = A.contiguous(), Q.contiguous(), B.contiguous()
|
|
420
435
|
# NKI 0.3.0: nl.load requires 2D tensors; unsqueeze Q from (M,) to (M,1)
|
|
421
436
|
Q_feed = Q_feed.unsqueeze(-1).contiguous()
|
|
437
|
+
A_feed, Q_feed, B_feed = _kernel_input(A_feed), _kernel_input(Q_feed), _kernel_input(B_feed)
|
|
438
|
+
threshold_sqrt_t = _kernel_input(threshold_sqrt_t)
|
|
422
439
|
|
|
423
440
|
if _use_simulator():
|
|
424
441
|
out_np = nki.simulate(_screened_spmm_kernel)(
|
|
@@ -434,6 +451,7 @@ def _nki_screened_spmm_impl(
|
|
|
434
451
|
c = _screened_spmm_kernel(a, q, ts, b)
|
|
435
452
|
result = c.to(orig_device)
|
|
436
453
|
|
|
454
|
+
result = result.to(A.dtype)
|
|
437
455
|
return result[:M, :N] if needs_pad else result
|
|
438
456
|
except Exception:
|
|
439
457
|
if _REQUIRE_NKI:
|
|
@@ -494,6 +512,24 @@ def nki_screened_spmm(
|
|
|
494
512
|
return _ScreenedSpMMFunction.apply(A, diag_integrals, threshold, B)
|
|
495
513
|
|
|
496
514
|
|
|
515
|
+
def _attn_slot_valid(mask_bsr, M_tiles: int, K_max: int) -> torch.Tensor:
|
|
516
|
+
"""(M_tiles, K_max) bool: True where the k-slot holds a real block.
|
|
517
|
+
|
|
518
|
+
Rows with fewer than K_max blocks are padded with sentinel slots that
|
|
519
|
+
gather column block 0. Those slots must contribute nothing: callers zero
|
|
520
|
+
the gathered K/V (forward, dQ) or Q/dO (dK/dV) there and drop their
|
|
521
|
+
stats from the host softmax reduction.
|
|
522
|
+
"""
|
|
523
|
+
counts = (mask_bsr.block_row_ptrs[1:] - mask_bsr.block_row_ptrs[:-1]).to(torch.long)
|
|
524
|
+
return torch.arange(K_max).unsqueeze(0) < counts[:M_tiles].unsqueeze(1)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _zero_invalid_slots(t: torch.Tensor, valid: torch.Tensor) -> torch.Tensor:
|
|
528
|
+
"""Zero `t[i, j, ...]` wherever `valid[i, j]` is False."""
|
|
529
|
+
shape = valid.shape + (1,) * (t.dim() - valid.dim())
|
|
530
|
+
return t * valid.view(shape).to(t.dtype)
|
|
531
|
+
|
|
532
|
+
|
|
497
533
|
def _attn_gather(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, mask_bsr, scale: float):
|
|
498
534
|
"""Host-side gather for the two-pass NKI attention kernels.
|
|
499
535
|
|
|
@@ -541,8 +577,11 @@ def _attn_gather(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, mask_bsr, sc
|
|
|
541
577
|
V_by_block = V.view(nb_rows, b, head_dim)
|
|
542
578
|
|
|
543
579
|
safe_col = torch.where(col_grid >= 0, col_grid, torch.tensor(0, dtype=torch.long))
|
|
544
|
-
|
|
545
|
-
|
|
580
|
+
# Padded slots (col_grid == -1) gather block 0; zero K/V there so the
|
|
581
|
+
# slot adds nothing to weights @ V (its stats are dropped on the host).
|
|
582
|
+
valid = col_grid >= 0
|
|
583
|
+
k_gathered_pad = _zero_invalid_slots(K_by_block[safe_col], valid) # (M_tiles, K_max, b, hd)
|
|
584
|
+
v_gathered_pad = _zero_invalid_slots(V_by_block[safe_col], valid) # (M_tiles, K_max, b, hd)
|
|
546
585
|
|
|
547
586
|
q_scaled_blocks = (Q * scale).view(M_tiles, b, head_dim)
|
|
548
587
|
|
|
@@ -559,7 +598,9 @@ def _attn_gather(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, mask_bsr, sc
|
|
|
559
598
|
return q_scaled_blocks, k_gathered_pad, v_gathered_pad, K_max, M_tiles
|
|
560
599
|
|
|
561
600
|
|
|
562
|
-
def _attn_host_reduction(
|
|
601
|
+
def _attn_host_reduction(
|
|
602
|
+
tile_max: torch.Tensor, tile_sumexp: torch.Tensor, valid: torch.Tensor | None = None
|
|
603
|
+
) -> tuple:
|
|
563
604
|
"""Reduce per-block stats to per-row softmax denominators.
|
|
564
605
|
|
|
565
606
|
Args:
|
|
@@ -569,7 +610,15 @@ def _attn_host_reduction(tile_max: torch.Tensor, tile_sumexp: torch.Tensor) -> t
|
|
|
569
610
|
Returns:
|
|
570
611
|
row_max: (M_tiles, 128) — row-wise global max
|
|
571
612
|
row_denom: (M_tiles, 128) — softmax denominator
|
|
613
|
+
|
|
614
|
+
`valid` (M_tiles, K_max) bool, if given, drops padded k-slots from the
|
|
615
|
+
reduction (rows with no valid slot keep all slots, as before).
|
|
572
616
|
"""
|
|
617
|
+
if valid is not None:
|
|
618
|
+
valid = valid | ~valid.any(dim=1, keepdim=True)
|
|
619
|
+
v = valid.unsqueeze(-1)
|
|
620
|
+
tile_max = torch.where(v, tile_max, torch.full_like(tile_max, float("-inf")))
|
|
621
|
+
tile_sumexp = torch.where(v, tile_sumexp, torch.zeros_like(tile_sumexp))
|
|
573
622
|
row_max = tile_max.max(dim=1).values # (M_tiles, 128)
|
|
574
623
|
correction = torch.exp(tile_max - row_max.unsqueeze(1)) # (M_tiles, K_max, 128)
|
|
575
624
|
row_denom = (tile_sumexp * correction).sum(dim=1) # (M_tiles, 128)
|
|
@@ -611,12 +660,13 @@ def nki_bsr_attn_tiled(
|
|
|
611
660
|
scale = head_dim**-0.5
|
|
612
661
|
|
|
613
662
|
q_scaled, k_gathered, v_gathered, K_max, M_tiles = _attn_gather(Q, K, V, mask_bsr, scale)
|
|
663
|
+
slot_valid = _attn_slot_valid(mask_bsr, M_tiles, K_max)
|
|
614
664
|
b = mask_bsr.block_size
|
|
615
665
|
|
|
616
666
|
# Contiguous inputs for kernel dispatch.
|
|
617
|
-
qs = q_scaled.contiguous()
|
|
618
|
-
kg = k_gathered.contiguous()
|
|
619
|
-
vg = v_gathered.contiguous()
|
|
667
|
+
qs = _kernel_input(q_scaled.contiguous())
|
|
668
|
+
kg = _kernel_input(k_gathered.contiguous())
|
|
669
|
+
vg = _kernel_input(v_gathered.contiguous())
|
|
620
670
|
|
|
621
671
|
try:
|
|
622
672
|
if _use_simulator():
|
|
@@ -630,7 +680,7 @@ def nki_bsr_attn_tiled(
|
|
|
630
680
|
tile_max = tile_max.squeeze(-1)
|
|
631
681
|
tile_sumexp = tile_sumexp.squeeze(-1)
|
|
632
682
|
|
|
633
|
-
row_max, row_denom = _attn_host_reduction(tile_max, tile_sumexp)
|
|
683
|
+
row_max, row_denom = _attn_host_reduction(tile_max, tile_sumexp, slot_valid)
|
|
634
684
|
# NKI 0.3.0: row vectors must be 2D for nl.load; unsqueeze (M,128) → (M,128,1)
|
|
635
685
|
rm = row_max.unsqueeze(-1).contiguous()
|
|
636
686
|
rd = row_denom.unsqueeze(-1).contiguous()
|
|
@@ -652,7 +702,7 @@ def nki_bsr_attn_tiled(
|
|
|
652
702
|
tile_max = tile_max.squeeze(-1)
|
|
653
703
|
tile_sumexp = tile_sumexp.squeeze(-1)
|
|
654
704
|
|
|
655
|
-
row_max, row_denom = _attn_host_reduction(tile_max, tile_sumexp)
|
|
705
|
+
row_max, row_denom = _attn_host_reduction(tile_max, tile_sumexp, slot_valid)
|
|
656
706
|
rm = row_max.unsqueeze(-1).contiguous()
|
|
657
707
|
rd = row_denom.unsqueeze(-1).contiguous()
|
|
658
708
|
|
|
@@ -660,7 +710,7 @@ def nki_bsr_attn_tiled(
|
|
|
660
710
|
result_x = _attn_out_kernel(qs_x, kg_x, vg_x, rm_x, rd_x)
|
|
661
711
|
result = result_x.to(orig_device)
|
|
662
712
|
|
|
663
|
-
out = result[:seq_len, :head_dim].contiguous()
|
|
713
|
+
out = result[:seq_len, :head_dim].to(Q.dtype).contiguous()
|
|
664
714
|
if return_stats:
|
|
665
715
|
# row_max/row_denom are already (M_tiles, b) from _attn_host_reduction.
|
|
666
716
|
return out, row_max[:M_tiles, :b].contiguous(), row_denom[:M_tiles, :b].contiguous()
|
|
@@ -748,9 +798,15 @@ def _attn_bwd_gather(
|
|
|
748
798
|
dO_by_block = dO.view(nb_rows, b, head_dim)
|
|
749
799
|
|
|
750
800
|
q_scaled = (Q * scale).view(M_tiles, b, head_dim)
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
801
|
+
# Padded slots gather block 0; zero K/V there so dS @ K and dO @ V.T
|
|
802
|
+
# contribute nothing to dQ.
|
|
803
|
+
row_valid = col_grid >= 0
|
|
804
|
+
k_gathered = _zero_invalid_slots(K_by_block[safe_col], row_valid)
|
|
805
|
+
v_gathered = _zero_invalid_slots(V_by_block[safe_col], row_valid)
|
|
806
|
+
# dP = dO_m @ V_ki.T needs the *row* block dO_m in every k-slot of row m
|
|
807
|
+
# (gathering by column index, as before, fed dO_ki and broke dQ whenever
|
|
808
|
+
# a row attends to a block other than its own).
|
|
809
|
+
do_gathered = dO_by_block.unsqueeze(1).expand(M_tiles, K_max, b, head_dim)
|
|
754
810
|
|
|
755
811
|
# D = dO · O row-wise; reshape to (M_tiles, b) for block indexing.
|
|
756
812
|
D_flat = (dO * O).sum(dim=-1) # (seq_len,)
|
|
@@ -791,8 +847,11 @@ def _attn_bwd_gather(
|
|
|
791
847
|
v_blocks = V.view(N_col, b, head_dim)
|
|
792
848
|
|
|
793
849
|
# Gather Q_scaled, dO, D by row index; reuse views computed in row-first section.
|
|
794
|
-
|
|
795
|
-
|
|
850
|
+
# Padded slots gather block-row 0; zero Q/dO there so dS.T @ Q and
|
|
851
|
+
# P.T @ dO contribute nothing to dK/dV.
|
|
852
|
+
col_valid = row_grid_col >= 0
|
|
853
|
+
q_gathered_col = _zero_invalid_slots(q_scaled[safe_row], col_valid)
|
|
854
|
+
do_gathered_col = _zero_invalid_slots(dO_by_block[safe_row], col_valid)
|
|
796
855
|
D_gathered_col = D_blocks[safe_row] # (N_col, K_max_col, b)
|
|
797
856
|
row_max_gathered_col = row_max[safe_row] # (N_col, K_max_col, b)
|
|
798
857
|
row_denom_gathered_col = row_denom[safe_row] # (N_col, K_max_col, b)
|
|
@@ -838,18 +897,24 @@ def nki_bsr_attn_bwd(
|
|
|
838
897
|
|
|
839
898
|
row_first, col_first = _attn_bwd_gather(Q, K, V, dO, O, mask_bsr, scale, row_max, row_denom)
|
|
840
899
|
|
|
841
|
-
# Pack contiguous inputs. NKI
|
|
842
|
-
# trailing dim=b
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
# when head_dim == b
|
|
846
|
-
|
|
847
|
-
|
|
900
|
+
# Pack contiguous inputs. NKI: per-row stat vectors (D_blocks, D/row_max/
|
|
901
|
+
# row_denom gathered, trailing dim = b) must be ≥2D in the kernel —
|
|
902
|
+
# unsqueeze (..., b) → (..., b, 1). Select them by name: a shape test
|
|
903
|
+
# (`ndim <= 3 and shape[-1] == b`) also matched q_scaled / k_blocks /
|
|
904
|
+
# v_blocks, which are (tiles, b, head_dim) and collide when head_dim == b
|
|
905
|
+
# (head_dim=128, or the simulator's padded head_dim).
|
|
906
|
+
_stat_keys = {"D_blocks", "D_gathered", "row_max_gathered", "row_denom_gathered"}
|
|
907
|
+
|
|
908
|
+
def _u(key: str, t: torch.Tensor) -> torch.Tensor:
|
|
909
|
+
t = _kernel_input(t)
|
|
910
|
+
if key in _stat_keys:
|
|
848
911
|
return t.unsqueeze(-1).contiguous()
|
|
849
912
|
return t.contiguous()
|
|
850
913
|
|
|
851
|
-
rf = {k: _u(v) for k, v in row_first.items()}
|
|
852
|
-
cf = {k: _u(v) for k, v in col_first.items()}
|
|
914
|
+
rf = {k: _u(k, v) for k, v in row_first.items()}
|
|
915
|
+
cf = {k: _u(k, v) for k, v in col_first.items()}
|
|
916
|
+
rm_in = _kernel_input(row_max).unsqueeze(-1).contiguous()
|
|
917
|
+
rd_in = _kernel_input(row_denom).unsqueeze(-1).contiguous()
|
|
853
918
|
|
|
854
919
|
try:
|
|
855
920
|
if _use_simulator():
|
|
@@ -859,8 +924,8 @@ def nki_bsr_attn_bwd(
|
|
|
859
924
|
rf["v_gathered"].cpu().numpy(),
|
|
860
925
|
rf["do_gathered"].cpu().numpy(),
|
|
861
926
|
rf["D_blocks"].cpu().numpy(),
|
|
862
|
-
|
|
863
|
-
|
|
927
|
+
rm_in.cpu().numpy(),
|
|
928
|
+
rd_in.cpu().numpy(),
|
|
864
929
|
)
|
|
865
930
|
dQ_raw = torch.from_numpy(np.asarray(dQ_np)).to(Q.device)
|
|
866
931
|
|
|
@@ -893,8 +958,8 @@ def nki_bsr_attn_bwd(
|
|
|
893
958
|
rf["v_gathered"],
|
|
894
959
|
rf["do_gathered"],
|
|
895
960
|
rf["D_blocks"],
|
|
896
|
-
|
|
897
|
-
|
|
961
|
+
rm_in,
|
|
962
|
+
rd_in,
|
|
898
963
|
)
|
|
899
964
|
dQ_x = _attn_bwd_dq_kernel(qs_x, kg_x, vg_x, dog_x, db_x, rm_x, rd_x)
|
|
900
965
|
dQ_raw = dQ_x.to(orig_device)
|
|
@@ -915,9 +980,9 @@ def nki_bsr_attn_bwd(
|
|
|
915
980
|
# dQ needs scale: kernel gives dS@K (gradient w.r.t. Q_scaled=Q*scale),
|
|
916
981
|
# but dL/dQ = dL/d(Q_scaled)*scale. dK already has scale via q_sbuf=Q*scale.
|
|
917
982
|
return (
|
|
918
|
-
dQ_raw[:seq_len, :head_dim].contiguous() * scale,
|
|
919
|
-
dK_raw[:seq_len, :head_dim].contiguous(),
|
|
920
|
-
dV_raw[:seq_len, :head_dim].contiguous(),
|
|
983
|
+
dQ_raw[:seq_len, :head_dim].to(Q.dtype).contiguous() * scale,
|
|
984
|
+
dK_raw[:seq_len, :head_dim].to(Q.dtype).contiguous(),
|
|
985
|
+
dV_raw[:seq_len, :head_dim].to(Q.dtype).contiguous(),
|
|
921
986
|
)
|
|
922
987
|
except Exception:
|
|
923
988
|
if _REQUIRE_NKI:
|
|
@@ -108,6 +108,11 @@ if HAS_NKI:
|
|
|
108
108
|
|
|
109
109
|
c = nl.ndarray((M, N), dtype=a.dtype, buffer=nl.shared_hbm)
|
|
110
110
|
|
|
111
|
+
# threshold_sqrt is a (1, 1) HBM tensor. NKI 0.7 rejects a 1-partition
|
|
112
|
+
# scalar operand against a 128-partition tile, so broadcast it to a
|
|
113
|
+
# (TILE_M, 1) per-partition scalar once, up front.
|
|
114
|
+
thr = nl.broadcast_to(nl.load(threshold_sqrt), shape=(TILE_M, 1))
|
|
115
|
+
|
|
111
116
|
for m in nl.affine_range(M // TILE_M):
|
|
112
117
|
for n in nl.affine_range(N // TILE_N):
|
|
113
118
|
m_off = m * TILE_M
|
|
@@ -125,11 +130,19 @@ if HAS_NKI:
|
|
|
125
130
|
# load_transpose2d on (TILE_K, 1) → (1, TILE_K) for the outer product
|
|
126
131
|
q_k = nl.load_transpose2d(q[k_off : k_off + TILE_K, :]) # (1, TILE_K)
|
|
127
132
|
|
|
128
|
-
# Outer-product pair bound (TILE_M, TILE_K)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
+
# Outer-product pair bound (TILE_M, TILE_K). NKI 0.7 lowers a
|
|
134
|
+
# (TILE_M,1)*(1,TILE_K) multiply to tensor_scalar_arith with a
|
|
135
|
+
# 1-partition dst, which the verifier rejects (partition element
|
|
136
|
+
# count mismatch). Broadcast q_k across partitions first so the
|
|
137
|
+
# multiply is a plain (P,F) x (P,1) tensor-scalar.
|
|
138
|
+
q_k_b = nl.broadcast_to(q_k, shape=(TILE_M, TILE_K))
|
|
139
|
+
pair_bound = nl.multiply(q_k_b, q_m)
|
|
140
|
+
# Soft 0/1 mask: relu(diff)/(relu(diff)+eps) = 0 where masked, ≈1 where not.
|
|
141
|
+
# Avoids boolean→float conversion issues in NKI 0.3.0 simulator.
|
|
142
|
+
relu_diff = nl.relu(nl.subtract(pair_bound, thr))
|
|
143
|
+
# NKI 0.7 / neuronx-cc 2.28: tensor_tensor `divide` fails ISA
|
|
144
|
+
# validation (s3s3d3_tt_valid_op) — multiply by the reciprocal.
|
|
145
|
+
mask_f = nl.multiply(relu_diff, nl.reciprocal(nl.add(relu_diff, 1e-10)))
|
|
133
146
|
a_masked = nl.multiply(a_tile, mask_f)
|
|
134
147
|
|
|
135
148
|
# nl.transpose gives PSUM in NKI 0.3.0 which nc_matmul rejects as
|
|
@@ -246,6 +259,9 @@ if HAS_NKI:
|
|
|
246
259
|
for m in nl.affine_range(M_tiles):
|
|
247
260
|
row_max_m = nl.load(row_max[m, :, :]) # (128, 1)
|
|
248
261
|
row_denom_m = nl.load(row_denom[m, :, :])
|
|
262
|
+
# NKI 0.7: tensor_scalar_arith has no `divide`; multiply by the
|
|
263
|
+
# reciprocal of the (128, 1) softmax denominator instead.
|
|
264
|
+
row_denom_inv_m = nl.reciprocal(row_denom_m)
|
|
249
265
|
|
|
250
266
|
out_psum = nl.zeros((_TILE_M, head_dim), dtype=nl.float32, buffer=nl.psum)
|
|
251
267
|
|
|
@@ -273,7 +289,7 @@ if HAS_NKI:
|
|
|
273
289
|
nisa.activation(_ssn, nl.relu, score_psum, scale=-1.0)
|
|
274
290
|
score = nl.subtract(_ssp, _ssn)
|
|
275
291
|
stable = nl.subtract(score, row_max_m)
|
|
276
|
-
weights = nl.
|
|
292
|
+
weights = nl.multiply(nl.exp(stable), row_denom_inv_m)
|
|
277
293
|
|
|
278
294
|
# NKI 0.3.0: nl.transpose gives PSUM which nc_matmul rejects as stationary.
|
|
279
295
|
# Round-trip weights through HBM so load_transpose2d produces SBUF.
|
|
@@ -330,6 +346,9 @@ if HAS_NKI:
|
|
|
330
346
|
for m in nl.affine_range(M_tiles):
|
|
331
347
|
row_max_m = nl.load(row_max[m, :, :]) # (128, 1)
|
|
332
348
|
row_denom_m = nl.load(row_denom[m, :, :])
|
|
349
|
+
# NKI 0.7: tensor_scalar_arith has no `divide`; multiply by the
|
|
350
|
+
# reciprocal of the (128, 1) softmax denominator instead.
|
|
351
|
+
row_denom_inv_m = nl.reciprocal(row_denom_m)
|
|
333
352
|
d_m = nl.load(D_blocks[m, :, :]) # (128, 1)
|
|
334
353
|
|
|
335
354
|
dq_psum = nl.zeros((_TILE_M, head_dim), dtype=nl.float32, buffer=nl.psum)
|
|
@@ -360,7 +379,7 @@ if HAS_NKI:
|
|
|
360
379
|
nisa.activation(_ssn, nl.relu, score_psum, scale=-1.0)
|
|
361
380
|
score = nl.subtract(_ssp, _ssn)
|
|
362
381
|
stable = nl.subtract(score, row_max_m)
|
|
363
|
-
P = nl.
|
|
382
|
+
P = nl.multiply(nl.exp(stable), row_denom_inv_m)
|
|
364
383
|
|
|
365
384
|
# dP = dO_m @ V_ki.T
|
|
366
385
|
dp_psum = nl.zeros((_TILE_M, _TILE_M), dtype=nl.float32, buffer=nl.psum)
|
|
@@ -477,7 +496,7 @@ if HAS_NKI:
|
|
|
477
496
|
nisa.activation(_ssn, nl.relu, score_psum, scale=-1.0)
|
|
478
497
|
score = nl.subtract(_ssp, _ssn)
|
|
479
498
|
stable_s = nl.subtract(score, row_max_mi)
|
|
480
|
-
P = nl.
|
|
499
|
+
P = nl.multiply(nl.exp(stable_s), nl.reciprocal(row_denom_mi))
|
|
481
500
|
|
|
482
501
|
# dP = dO_m @ V_ki.T
|
|
483
502
|
dp_psum = nl.zeros((_TILE_M, _TILE_M), dtype=nl.float32, buffer=nl.psum)
|
|
@@ -38,12 +38,14 @@ scripts/run_neuron_tests.sh
|
|
|
38
38
|
scripts/run_simulator_tests.sh
|
|
39
39
|
tests/conftest.py
|
|
40
40
|
tests/test_attention.py
|
|
41
|
+
tests/test_attn_gather.py
|
|
41
42
|
tests/test_bsr.py
|
|
42
43
|
tests/test_examples.py
|
|
43
44
|
tests/test_formats.py
|
|
44
45
|
tests/test_iterative.py
|
|
45
46
|
tests/test_nki_attn.py
|
|
46
47
|
tests/test_nki_bsr.py
|
|
48
|
+
tests/test_nki_compile.py
|
|
47
49
|
tests/test_nki_screened_spmm.py
|
|
48
50
|
tests/test_nki_sim.py
|
|
49
51
|
tests/test_nki_spmm.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|