trnsparse 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trnsparse-0.4.0 → trnsparse-0.4.2}/CHANGELOG.md +55 -0
- {trnsparse-0.4.0/trnsparse.egg-info → trnsparse-0.4.2}/PKG-INFO +14 -1
- {trnsparse-0.4.0 → trnsparse-0.4.2}/README.md +13 -0
- trnsparse-0.4.2/docs/sparse_attention.md +162 -0
- trnsparse-0.4.2/examples/block_sparse_attention.py +284 -0
- trnsparse-0.4.2/examples/sparse_fock.py +164 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/mkdocs.yml +2 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/pyproject.toml +1 -1
- trnsparse-0.4.2/tests/test_attention.py +93 -0
- trnsparse-0.4.2/tests/test_examples.py +70 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/__init__.py +1 -1
- {trnsparse-0.4.0 → trnsparse-0.4.2/trnsparse.egg-info}/PKG-INFO +14 -1
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse.egg-info/SOURCES.txt +4 -0
- trnsparse-0.4.0/examples/sparse_fock.py +0 -80
- {trnsparse-0.4.0 → trnsparse-0.4.2}/.github/workflows/ci.yml +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/.github/workflows/notify-umbrella.yml +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/.github/workflows/publish.yml +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/.gitignore +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/.pre-commit-config.yaml +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/CLAUDE.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/CODE_OF_CONDUCT.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/CONTRIBUTING.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/LICENSE +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/bench_bsr_spmm.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/bench_iterative.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/bench_screening.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/bench_spmm.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/bench_spmv.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/benchmarks/conftest.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/api.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/architecture.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/aws_setup.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/benchmarks.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/index.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/installation.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/iterative_solvers.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/migration_scipy.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/docs/quickstart.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/infra/terraform/.terraform.lock.hcl +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/infra/terraform/README.md +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/infra/terraform/main.tf +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/scripts/bench_to_md.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/scripts/run_benchmarks.sh +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/scripts/run_neuron_tests.sh +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/scripts/run_simulator_tests.sh +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/setup.cfg +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/conftest.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_bsr.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_formats.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_iterative.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_nki_bsr.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_nki_screened_spmm.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_nki_sim.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_nki_spmm.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_ops.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/tests/test_screening.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/formats.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/iterative.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/nki/__init__.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/nki/dispatch.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/nki/kernels.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/ops.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse/screening.py +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse.egg-info/dependency_links.txt +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/trnsparse.egg-info/requires.txt +0 -0
- {trnsparse-0.4.0 → trnsparse-0.4.2}/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
|
+
## [0.4.2] — 2026-04-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`examples/block_sparse_attention.py`** — block-sparse attention
|
|
13
|
+
reference using `BSRMatrix` + `bsr_spmm`. Three mask patterns
|
|
14
|
+
(local window, dilated, global tokens); verifies against a dense
|
|
15
|
+
reference; reports block density and timing for the `bsr_spmm` step.
|
|
16
|
+
Closes [#21](https://github.com/trnsci/trnsparse/issues/21).
|
|
17
|
+
- **`docs/sparse_attention.md`** — writeup: how BSR-128 maps to
|
|
18
|
+
Longformer/BigBird-style attention masks, block density arithmetic,
|
|
19
|
+
pattern construction helpers, and the fused-tile follow-up.
|
|
20
|
+
- **`docs/iterative_solvers.md`** now linked in `mkdocs.yml` nav
|
|
21
|
+
(was present but missing from navigation).
|
|
22
|
+
- **`tests/test_attention.py`** — 8 CPU tests: mask shape/symmetry
|
|
23
|
+
checks + parity against dense reference at `atol=1e-4` for all three
|
|
24
|
+
patterns and the full-attention edge case.
|
|
25
|
+
|
|
26
|
+
### Notes
|
|
27
|
+
|
|
28
|
+
No API changes, no kernel changes. The claim in #21 is: `bsr_spmm` is
|
|
29
|
+
the block-sparse attention primitive; `BSRMatrix` captures the mask.
|
|
30
|
+
The example and docs make that explicit.
|
|
31
|
+
|
|
32
|
+
## [0.4.1] — 2026-04-14
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`examples/sparse_fock.py`** rewritten around v0.4.0's
|
|
37
|
+
`screened_spmm`. Three paths side-by-side on the same inputs:
|
|
38
|
+
(1) v0.1.x unfused `schwarz_bounds → screen → from_dense → spmm`;
|
|
39
|
+
(2) v0.4.0 fused `screened_spmm` (one call);
|
|
40
|
+
(3) full Fock build — the coulomb from path 2 contracted against MO
|
|
41
|
+
coefficients via `trnblas.gemm` for `F_MO = C.T @ J @ C` (falls
|
|
42
|
+
back to `torch.matmul` if trnblas isn't installed). On a 50-basis
|
|
43
|
+
synthetic system, the fused path is ~130× faster than the unfused
|
|
44
|
+
(dominated by eliminating the Python `from_dense` CSR construction).
|
|
45
|
+
Closes #6.
|
|
46
|
+
- **`examples/pyscf_bridge.py`** (new) — optional PySCF-driven demo.
|
|
47
|
+
Builds H2O (or benzene, or H2), pulls real AO ERIs via
|
|
48
|
+
`mol.intor("int2e")`, feeds the `(μμ|μμ)` diagonal into
|
|
49
|
+
`schwarz_bounds` + `screened_spmm` against a mock density matrix.
|
|
50
|
+
Reports realistic sparsity at `threshold=1e-8`. Requires
|
|
51
|
+
`pip install pyscf`; tests skip cleanly if not available.
|
|
52
|
+
Closes #13.
|
|
53
|
+
- **`tests/test_examples.py`** — 2 CPU smoke tests plus a
|
|
54
|
+
PySCF-gated test. Exercises the `sparse_fock` unfused + fused
|
|
55
|
+
paths end-to-end and asserts parity (`atol=1e-6`).
|
|
56
|
+
|
|
57
|
+
### Notes
|
|
58
|
+
|
|
59
|
+
No API changes, no kernel changes — pure integration demo release.
|
|
60
|
+
Users already on v0.4.0 can stay there; upgrade to v0.4.1 only to
|
|
61
|
+
pick up the new examples.
|
|
62
|
+
|
|
8
63
|
## [0.4.0] — 2026-04-14
|
|
9
64
|
|
|
10
65
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trnsparse
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Sparse matrix operations for AWS Trainium via NKI
|
|
5
5
|
Author-email: Scott Friedman <scttfrdmn@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -102,3 +102,16 @@ stats = trnsparse.sparsity_stats(Q)
|
|
|
102
102
|
## License
|
|
103
103
|
|
|
104
104
|
Apache 2.0 — Copyright 2026 Scott Friedman
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Disclaimer
|
|
108
|
+
|
|
109
|
+
trnsci is an **independent open-source project**. It is not sponsored by, endorsed by, or affiliated with Amazon.com, Inc., Amazon Web Services, Inc., or Annapurna Labs Ltd.
|
|
110
|
+
|
|
111
|
+
"AWS", "Amazon", "Trainium", "Inferentia", "NeuronCore", "Neuron SDK", and related identifiers are trademarks of their respective owners and are used here solely for descriptive and interoperability purposes. Use does not imply endorsement, partnership, or any other relationship.
|
|
112
|
+
|
|
113
|
+
All work, opinions, analyses, benchmark results, architectural commentary, and editorial judgments in this repository and on [trnsci.dev](https://trnsci.dev) are those of the project's contributors. They do not represent the views, positions, or commitments of Amazon, AWS, or Annapurna Labs.
|
|
114
|
+
|
|
115
|
+
Feedback directed at the Neuron SDK or Trainium hardware is good-faith ecosystem commentary from independent users. It is not privileged information, is not pre-reviewed by AWS, and should not be read as authoritative about product roadmap, behavior, or quality.
|
|
116
|
+
|
|
117
|
+
For official AWS guidance, see [aws-neuron documentation](https://awsdocs-neuron.readthedocs-hosted.com/) and the [AWS Trainium product page](https://aws.amazon.com/ai/machine-learning/trainium/).
|
|
@@ -71,3 +71,16 @@ stats = trnsparse.sparsity_stats(Q)
|
|
|
71
71
|
## License
|
|
72
72
|
|
|
73
73
|
Apache 2.0 — Copyright 2026 Scott Friedman
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Disclaimer
|
|
77
|
+
|
|
78
|
+
trnsci is an **independent open-source project**. It is not sponsored by, endorsed by, or affiliated with Amazon.com, Inc., Amazon Web Services, Inc., or Annapurna Labs Ltd.
|
|
79
|
+
|
|
80
|
+
"AWS", "Amazon", "Trainium", "Inferentia", "NeuronCore", "Neuron SDK", and related identifiers are trademarks of their respective owners and are used here solely for descriptive and interoperability purposes. Use does not imply endorsement, partnership, or any other relationship.
|
|
81
|
+
|
|
82
|
+
All work, opinions, analyses, benchmark results, architectural commentary, and editorial judgments in this repository and on [trnsci.dev](https://trnsci.dev) are those of the project's contributors. They do not represent the views, positions, or commitments of Amazon, AWS, or Annapurna Labs.
|
|
83
|
+
|
|
84
|
+
Feedback directed at the Neuron SDK or Trainium hardware is good-faith ecosystem commentary from independent users. It is not privileged information, is not pre-reviewed by AWS, and should not be read as authoritative about product roadmap, behavior, or quality.
|
|
85
|
+
|
|
86
|
+
For official AWS guidance, see [aws-neuron documentation](https://awsdocs-neuron.readthedocs-hosted.com/) and the [AWS Trainium product page](https://aws.amazon.com/ai/machine-learning/trainium/).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Block-sparse attention
|
|
2
|
+
|
|
3
|
+
## The connection
|
|
4
|
+
|
|
5
|
+
Transformer attention computes `O = softmax(Q @ K.T / √d) @ V`. The expensive
|
|
6
|
+
part at long sequence lengths is the `(seq_len, seq_len)` score matrix and the
|
|
7
|
+
final `attn_weights @ V` matmul. Sparse attention restricts which positions can
|
|
8
|
+
attend to each other, zeroing out large regions of the score matrix. What
|
|
9
|
+
remains is a sparse × dense matmul: exactly `bsr_spmm`.
|
|
10
|
+
|
|
11
|
+
The block size that matters is 128. Trainium's Tensor Engine is a 128-partition
|
|
12
|
+
systolic array; `nc_matmul` consumes a 128×K×N tile in one call. A local-window
|
|
13
|
+
attention mask with window granularity of 128 tokens has exactly `2w+1` nonzero
|
|
14
|
+
128×128 blocks per block-row — each nonzero block maps one-to-one to one
|
|
15
|
+
`nc_matmul` call, with no gather overhead. The tile size of the mask and the
|
|
16
|
+
tile size of the hardware coincide.
|
|
17
|
+
|
|
18
|
+
cuSPARSE's BSR is a specialization added on top of CSR. Here it's the other
|
|
19
|
+
way: BSR is what the attention mask asks for, and the hardware is built around
|
|
20
|
+
that shape.
|
|
21
|
+
|
|
22
|
+
## Building patterns
|
|
23
|
+
|
|
24
|
+
### Local window (Longformer-style)
|
|
25
|
+
|
|
26
|
+
Each token block attends to its `window` nearest neighbors in block space:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import trnsparse
|
|
30
|
+
|
|
31
|
+
def local_window_mask(seq_len: int, block_size: int, window: int) -> trnsparse.BSRMatrix:
|
|
32
|
+
"""BSRMatrix for sliding-window attention."""
|
|
33
|
+
import torch
|
|
34
|
+
n_blocks = seq_len // block_size
|
|
35
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
36
|
+
for i in range(n_blocks):
|
|
37
|
+
lo, hi = max(0, i - window), min(n_blocks, i + window + 1)
|
|
38
|
+
block_mask[i, lo:hi] = True
|
|
39
|
+
mask_dense = block_mask.repeat_interleave(block_size, 0).repeat_interleave(block_size, 1)
|
|
40
|
+
return trnsparse.BSRMatrix.from_dense(mask_dense.float(), block_size=block_size)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
At `seq_len=4096` and `window=2`: `5` nonzero blocks per row, `5/32 ≈ 15.6%`
|
|
44
|
+
block density vs 100% for dense attention. Memory for the stored blocks:
|
|
45
|
+
`32 rows × 5 blocks × 128 × 128 × 4 bytes ≈ 10 MB` vs 64 MB for the full
|
|
46
|
+
float32 attention matrix.
|
|
47
|
+
|
|
48
|
+
### Dilated sparse
|
|
49
|
+
|
|
50
|
+
Every `stride`-th block column is attended to. Block `(i, j)` is nonzero
|
|
51
|
+
iff `(i - j) % stride == 0`:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def dilated_mask(seq_len: int, block_size: int, stride: int) -> trnsparse.BSRMatrix:
|
|
55
|
+
import torch
|
|
56
|
+
n_blocks = seq_len // block_size
|
|
57
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
58
|
+
for i in range(n_blocks):
|
|
59
|
+
for j in range(n_blocks):
|
|
60
|
+
if (i - j) % stride == 0:
|
|
61
|
+
block_mask[i, j] = True
|
|
62
|
+
mask_dense = block_mask.repeat_interleave(block_size, 0).repeat_interleave(block_size, 1)
|
|
63
|
+
return trnsparse.BSRMatrix.from_dense(mask_dense.float(), block_size=block_size)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Global tokens (BigBird-style)
|
|
67
|
+
|
|
68
|
+
The first `n_global` token-blocks attend to and are attended to by every
|
|
69
|
+
block. Remaining block-rows follow a local window:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
def global_token_mask(
|
|
73
|
+
seq_len: int, block_size: int, window: int, n_global: int
|
|
74
|
+
) -> trnsparse.BSRMatrix:
|
|
75
|
+
import torch
|
|
76
|
+
n_blocks = seq_len // block_size
|
|
77
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
78
|
+
block_mask[:n_global, :] = True # global rows attend everywhere
|
|
79
|
+
block_mask[:, :n_global] = True # global columns attended by all
|
|
80
|
+
for i in range(n_global, n_blocks):
|
|
81
|
+
lo, hi = max(0, i - window), min(n_blocks, i + window + 1)
|
|
82
|
+
block_mask[i, lo:hi] = True
|
|
83
|
+
mask_dense = block_mask.repeat_interleave(block_size, 0).repeat_interleave(block_size, 1)
|
|
84
|
+
return trnsparse.BSRMatrix.from_dense(mask_dense.float(), block_size=block_size)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## The computation
|
|
88
|
+
|
|
89
|
+
Given a `mask_bsr` constructed above:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import torch
|
|
93
|
+
|
|
94
|
+
def block_sparse_attention(Q, K, V, mask_bsr):
|
|
95
|
+
"""Q, K, V: (seq_len, head_dim) → (seq_len, head_dim)."""
|
|
96
|
+
scale = Q.shape[-1] ** -0.5
|
|
97
|
+
scores = (Q @ K.T) * scale
|
|
98
|
+
|
|
99
|
+
# Mask out unattended positions before softmax.
|
|
100
|
+
mask_dense = mask_bsr.to_dense().bool()
|
|
101
|
+
masked_scores = scores.masked_fill(~mask_dense, float("-inf"))
|
|
102
|
+
attn_weights = torch.softmax(masked_scores, dim=-1)
|
|
103
|
+
|
|
104
|
+
# Positions outside the mask go to zero after softmax; from_dense
|
|
105
|
+
# with threshold=0 will still store them unless they're exactly zero.
|
|
106
|
+
# Use threshold slightly above zero to keep only the attended blocks.
|
|
107
|
+
attn_bsr = trnsparse.BSRMatrix.from_dense(
|
|
108
|
+
attn_weights, block_size=mask_bsr.block_size, threshold=1e-9
|
|
109
|
+
)
|
|
110
|
+
return trnsparse.bsr_spmm(attn_bsr, V)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**What this materializes**: the full `(seq_len, seq_len)` score matrix. At
|
|
114
|
+
`seq_len=4096` that's 64 MB of float32 even before the matmul. See
|
|
115
|
+
[What's next](#whats-next) for the fused-tile path that avoids this.
|
|
116
|
+
|
|
117
|
+
**Gradients**: `bsr_spmm` is differentiable (see `architecture.md`). The
|
|
118
|
+
backward through `block_sparse_attention` works out-of-the-box with
|
|
119
|
+
`torch.autograd`; the block-selection step (`from_dense`) is non-differentiable
|
|
120
|
+
by construction and is treated as a constant by the autograd graph.
|
|
121
|
+
|
|
122
|
+
## Block density arithmetic
|
|
123
|
+
|
|
124
|
+
For `seq_len = S`, `block_size = b`, `window = w` local-window mask:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
blocks per row = 2w + 1 (clamped at edges)
|
|
128
|
+
block density = (2w+1) / (S/b)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| seq_len | window | n_blocks_per_row | block density |
|
|
132
|
+
|--------:|-------:|-----------------:|:--------------|
|
|
133
|
+
| 1024 | 2 | 5 | 39.1% |
|
|
134
|
+
| 2048 | 2 | 5 | 19.5% |
|
|
135
|
+
| 4096 | 2 | 5 | 9.8% |
|
|
136
|
+
| 4096 | 4 | 9 | 17.6% |
|
|
137
|
+
| 8192 | 2 | 5 | 4.9% |
|
|
138
|
+
|
|
139
|
+
The win is at long sequences. At `seq_len=1024`, a window-2 mask still stores
|
|
140
|
+
40% of blocks — dispatch overhead dominates on Trainium at that size. At
|
|
141
|
+
`seq_len=8192`, 5% density means 95% of the `nc_matmul` calls are skipped and
|
|
142
|
+
the per-block compute becomes the bottleneck, which is where the Tensor Engine
|
|
143
|
+
thrives.
|
|
144
|
+
|
|
145
|
+
## What's next
|
|
146
|
+
|
|
147
|
+
The current path materializes the full `(seq_len, seq_len)` score matrix to
|
|
148
|
+
compute attention weights. A production path would:
|
|
149
|
+
|
|
150
|
+
1. Iterate over nonzero blocks in `mask_bsr`.
|
|
151
|
+
2. For each block `(i, j)`, load `Q[i*b:(i+1)*b]` and `K[j*b:(j+1)*b]` into
|
|
152
|
+
SBUF, compute the score tile, and apply softmax over the block-row.
|
|
153
|
+
3. Multiply the score tile by `V[j*b:(j+1)*b]` and accumulate into the output.
|
|
154
|
+
|
|
155
|
+
This fused tile-level kernel avoids the `O(seq_len²)` intermediate entirely.
|
|
156
|
+
It's the same architectural opportunity as [on-chip iterative solvers](iterative_solvers.md):
|
|
157
|
+
load A once, iterate on-chip. The NKI building block is available (the BSR
|
|
158
|
+
kernel in `nki/kernels.py`); the row-wise softmax over variable numbers of
|
|
159
|
+
tiles is the authoring challenge.
|
|
160
|
+
|
|
161
|
+
A runnable reference for the current (non-fused) path is in
|
|
162
|
+
[`examples/block_sparse_attention.py`](https://github.com/trnsci/trnsparse/blob/main/examples/block_sparse_attention.py).
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Block-sparse attention via BSRMatrix and bsr_spmm.
|
|
2
|
+
|
|
3
|
+
The final matmul in sparse attention — `attn_weights @ V` — is a sparse ×
|
|
4
|
+
dense matrix multiplication. When the attention mask has 128×128 block
|
|
5
|
+
structure (local window, dilated, global tokens), that matmul is exactly
|
|
6
|
+
`bsr_spmm(attn_weights_bsr, V)`. No new kernel; BSRMatrix captures the
|
|
7
|
+
sparsity pattern, and the Tensor Engine sees a sequence of dense 128×128
|
|
8
|
+
tiles with zero gather overhead.
|
|
9
|
+
|
|
10
|
+
Three mask patterns are demonstrated:
|
|
11
|
+
|
|
12
|
+
local — sliding-window (Longformer-style): each token attends to the
|
|
13
|
+
nearest `window` blocks. Block (i, j) is stored iff |i-j| ≤ window.
|
|
14
|
+
dilated — every `stride`-th block column is stored. Covers dilated sparse
|
|
15
|
+
attention variants.
|
|
16
|
+
global — first `n_global` token blocks attend to and are attended to by
|
|
17
|
+
everyone; remaining rows follow a local window.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python examples/block_sparse_attention.py --demo
|
|
21
|
+
python examples/block_sparse_attention.py --seq-len 512 --pattern dilated
|
|
22
|
+
python examples/block_sparse_attention.py --seq-len 1024 --head-dim 64 --pattern global
|
|
23
|
+
|
|
24
|
+
Note on materialization: this example computes the full score matrix
|
|
25
|
+
`Q @ K.T` before masking. A production path would compute only the nonzero
|
|
26
|
+
tiles of S = Q @ K.T, avoiding the O(seq_len²) intermediate. That fused
|
|
27
|
+
tile-level score computation is the architectural follow-up; here the claim
|
|
28
|
+
being demonstrated is that `bsr_spmm` handles the attn_weights × V step.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
import torch
|
|
39
|
+
|
|
40
|
+
# Allow running from repo root without install.
|
|
41
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
42
|
+
import trnsparse
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Mask pattern helpers
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _local_window_mask(seq_len: int, block_size: int, window: int) -> torch.Tensor:
|
|
50
|
+
"""Boolean mask for local-window (Longformer-style) attention.
|
|
51
|
+
|
|
52
|
+
Block (i, j) in block-row i, block-column j is nonzero iff |i - j| ≤ window.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool tensor of shape (seq_len, seq_len).
|
|
56
|
+
"""
|
|
57
|
+
assert seq_len % block_size == 0, "seq_len must be a multiple of block_size"
|
|
58
|
+
n_blocks = seq_len // block_size
|
|
59
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
60
|
+
for i in range(n_blocks):
|
|
61
|
+
lo = max(0, i - window)
|
|
62
|
+
hi = min(n_blocks, i + window + 1)
|
|
63
|
+
block_mask[i, lo:hi] = True
|
|
64
|
+
# Expand each block indicator to (block_size, block_size)
|
|
65
|
+
return block_mask.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dilated_mask(seq_len: int, block_size: int, stride: int) -> torch.Tensor:
|
|
69
|
+
"""Boolean mask for dilated sparse attention.
|
|
70
|
+
|
|
71
|
+
Block (i, j) is nonzero iff (i - j) % stride == 0 (i.e., block column j
|
|
72
|
+
is a multiple of stride away from block row i, including j == i).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
bool tensor of shape (seq_len, seq_len).
|
|
76
|
+
"""
|
|
77
|
+
assert seq_len % block_size == 0
|
|
78
|
+
n_blocks = seq_len // block_size
|
|
79
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
80
|
+
for i in range(n_blocks):
|
|
81
|
+
for j in range(n_blocks):
|
|
82
|
+
if (i - j) % stride == 0:
|
|
83
|
+
block_mask[i, j] = True
|
|
84
|
+
return block_mask.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _global_token_mask(seq_len: int, block_size: int, window: int, n_global: int) -> torch.Tensor:
|
|
88
|
+
"""Boolean mask with global tokens + local window (BigBird-style).
|
|
89
|
+
|
|
90
|
+
First `n_global` token-blocks attend to all blocks and are attended to
|
|
91
|
+
by all blocks. Remaining rows follow a local window of `window` blocks.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
bool tensor of shape (seq_len, seq_len).
|
|
95
|
+
"""
|
|
96
|
+
assert seq_len % block_size == 0
|
|
97
|
+
n_blocks = seq_len // block_size
|
|
98
|
+
assert 0 <= n_global <= n_blocks
|
|
99
|
+
block_mask = torch.zeros(n_blocks, n_blocks, dtype=torch.bool)
|
|
100
|
+
# Global rows: attend everywhere
|
|
101
|
+
block_mask[:n_global, :] = True
|
|
102
|
+
# Global columns: attended to by everyone
|
|
103
|
+
block_mask[:, :n_global] = True
|
|
104
|
+
# Local window for non-global rows
|
|
105
|
+
for i in range(n_global, n_blocks):
|
|
106
|
+
lo = max(0, i - window)
|
|
107
|
+
hi = min(n_blocks, i + window + 1)
|
|
108
|
+
block_mask[i, lo:hi] = True
|
|
109
|
+
return block_mask.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Attention computation
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def block_sparse_attention(
|
|
118
|
+
Q: torch.Tensor,
|
|
119
|
+
K: torch.Tensor,
|
|
120
|
+
V: torch.Tensor,
|
|
121
|
+
mask_bsr: trnsparse.BSRMatrix,
|
|
122
|
+
) -> torch.Tensor:
|
|
123
|
+
"""Compute sparse attention output using bsr_spmm for the final matmul.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
Q: (seq_len, head_dim) query tensor.
|
|
127
|
+
K: (seq_len, head_dim) key tensor.
|
|
128
|
+
V: (seq_len, head_dim) value tensor.
|
|
129
|
+
mask_bsr: BSRMatrix encoding which attention blocks are nonzero.
|
|
130
|
+
The blocks themselves are ignored — only the sparsity pattern
|
|
131
|
+
(block_row_ptrs, block_col_indices) is used. The actual weights
|
|
132
|
+
come from softmax(Q @ K.T).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
(seq_len, head_dim) attention output.
|
|
136
|
+
|
|
137
|
+
Note: this materializes the full (seq_len, seq_len) score matrix Q @ K.T
|
|
138
|
+
before masking. See module docstring on the production alternative.
|
|
139
|
+
"""
|
|
140
|
+
seq_len, head_dim = Q.shape
|
|
141
|
+
scale = head_dim**-0.5
|
|
142
|
+
|
|
143
|
+
# Full score matrix — O(seq_len²). A production path computes only the
|
|
144
|
+
# nonzero tiles of S to avoid this intermediate.
|
|
145
|
+
scores = (Q @ K.T) * scale # (seq_len, seq_len)
|
|
146
|
+
|
|
147
|
+
# Apply additive mask: positions outside the pattern → -inf before softmax.
|
|
148
|
+
mask_dense = mask_bsr.to_dense().bool()
|
|
149
|
+
masked_scores = scores.masked_fill(~mask_dense, float("-inf"))
|
|
150
|
+
|
|
151
|
+
# Row-wise softmax over all attended positions (positions outside mask → 0).
|
|
152
|
+
attn_weights = torch.softmax(masked_scores, dim=-1) # (seq_len, seq_len)
|
|
153
|
+
|
|
154
|
+
# Convert the post-softmax weights to BSR (same pattern, real values now).
|
|
155
|
+
attn_bsr = trnsparse.BSRMatrix.from_dense(attn_weights, block_size=mask_bsr.block_size)
|
|
156
|
+
|
|
157
|
+
# Final matmul via bsr_spmm: (seq_len, seq_len) sparse × (seq_len, head_dim) dense.
|
|
158
|
+
return trnsparse.bsr_spmm(attn_bsr, V) # (seq_len, head_dim)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _dense_reference(
|
|
162
|
+
Q: torch.Tensor,
|
|
163
|
+
K: torch.Tensor,
|
|
164
|
+
V: torch.Tensor,
|
|
165
|
+
mask: torch.Tensor,
|
|
166
|
+
) -> torch.Tensor:
|
|
167
|
+
"""Dense reference: full softmax(Q @ K.T) @ V with additive mask."""
|
|
168
|
+
scale = Q.shape[-1] ** -0.5
|
|
169
|
+
scores = (Q @ K.T) * scale
|
|
170
|
+
masked = scores.masked_fill(~mask, float("-inf"))
|
|
171
|
+
attn = torch.softmax(masked, dim=-1)
|
|
172
|
+
return attn @ V
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# CLI
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main() -> None:
|
|
181
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
182
|
+
parser.add_argument("--demo", action="store_true", help="run a small demo (seq_len=512)")
|
|
183
|
+
parser.add_argument("--seq-len", type=int, default=512)
|
|
184
|
+
parser.add_argument("--head-dim", type=int, default=64)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--pattern",
|
|
187
|
+
choices=["local", "dilated", "global"],
|
|
188
|
+
default="local",
|
|
189
|
+
help="attention mask pattern",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--window", type=int, default=2, help="block window for local/global patterns"
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument("--stride", type=int, default=2, help="block stride for dilated pattern")
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--n-global", type=int, default=2, help="global token blocks for global pattern"
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument("--block-size", type=int, default=128, help="BSR block size")
|
|
199
|
+
args = parser.parse_args()
|
|
200
|
+
|
|
201
|
+
if args.demo:
|
|
202
|
+
args.seq_len = 512
|
|
203
|
+
args.head_dim = 64
|
|
204
|
+
args.pattern = "local"
|
|
205
|
+
args.window = 1
|
|
206
|
+
|
|
207
|
+
seq_len = args.seq_len
|
|
208
|
+
head_dim = args.head_dim
|
|
209
|
+
block_size = args.block_size
|
|
210
|
+
|
|
211
|
+
if seq_len % block_size != 0:
|
|
212
|
+
print(f"Error: seq_len ({seq_len}) must be a multiple of block_size ({block_size}).")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
torch.manual_seed(42)
|
|
216
|
+
Q = torch.randn(seq_len, head_dim)
|
|
217
|
+
K = torch.randn(seq_len, head_dim)
|
|
218
|
+
V = torch.randn(seq_len, head_dim)
|
|
219
|
+
|
|
220
|
+
# Build the mask.
|
|
221
|
+
if args.pattern == "local":
|
|
222
|
+
mask = _local_window_mask(seq_len, block_size, args.window)
|
|
223
|
+
pattern_desc = f"local window (w={args.window})"
|
|
224
|
+
elif args.pattern == "dilated":
|
|
225
|
+
mask = _dilated_mask(seq_len, block_size, args.stride)
|
|
226
|
+
pattern_desc = f"dilated (stride={args.stride})"
|
|
227
|
+
else:
|
|
228
|
+
mask = _global_token_mask(seq_len, block_size, args.window, args.n_global)
|
|
229
|
+
pattern_desc = f"global tokens (n_global={args.n_global}, w={args.window})"
|
|
230
|
+
|
|
231
|
+
mask_bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
232
|
+
n_blocks_total = (seq_len // block_size) ** 2
|
|
233
|
+
block_density = mask_bsr.n_blocks / n_blocks_total
|
|
234
|
+
|
|
235
|
+
print("Block-sparse attention via bsr_spmm:")
|
|
236
|
+
print(f" seq_len: {seq_len}")
|
|
237
|
+
print(f" head_dim: {head_dim}")
|
|
238
|
+
print(f" block_size: {block_size}")
|
|
239
|
+
print(f" pattern: {pattern_desc}")
|
|
240
|
+
print(
|
|
241
|
+
f" n_blocks: {mask_bsr.n_blocks} / {n_blocks_total} ({block_density:.1%} block density)"
|
|
242
|
+
)
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
# Dense reference.
|
|
246
|
+
out_dense = _dense_reference(Q, K, V, mask)
|
|
247
|
+
|
|
248
|
+
# BSR path.
|
|
249
|
+
out_bsr = block_sparse_attention(Q, K, V, mask_bsr)
|
|
250
|
+
|
|
251
|
+
max_err = (out_bsr - out_dense).abs().max().item()
|
|
252
|
+
print(f" Numerical agreement: max |Δ| = {max_err:.2e}")
|
|
253
|
+
assert max_err < 1e-3, f"BSR and dense outputs disagree: max_err={max_err}"
|
|
254
|
+
|
|
255
|
+
# Timing: attn_weights @ V, BSR vs dense.
|
|
256
|
+
# Pre-compute attn_weights for a fair comparison of just the matmul step.
|
|
257
|
+
scale = head_dim**-0.5
|
|
258
|
+
masked_scores = (Q @ K.T * scale).masked_fill(~mask, float("-inf"))
|
|
259
|
+
attn_weights = torch.softmax(masked_scores, dim=-1)
|
|
260
|
+
attn_bsr_timed = trnsparse.BSRMatrix.from_dense(attn_weights, block_size=block_size)
|
|
261
|
+
|
|
262
|
+
N_REPS = 20
|
|
263
|
+
|
|
264
|
+
t0 = time.perf_counter()
|
|
265
|
+
for _ in range(N_REPS):
|
|
266
|
+
_ = trnsparse.bsr_spmm(attn_bsr_timed, V)
|
|
267
|
+
t_bsr = (time.perf_counter() - t0) / N_REPS
|
|
268
|
+
|
|
269
|
+
t0 = time.perf_counter()
|
|
270
|
+
for _ in range(N_REPS):
|
|
271
|
+
_ = attn_weights @ V
|
|
272
|
+
t_dense = (time.perf_counter() - t0) / N_REPS
|
|
273
|
+
|
|
274
|
+
print(f" Timing (attn_weights @ V, {N_REPS}-rep mean):")
|
|
275
|
+
print(f" bsr_spmm: {t_bsr * 1e6:8.1f} μs")
|
|
276
|
+
print(f" dense @: {t_dense * 1e6:8.1f} μs")
|
|
277
|
+
print()
|
|
278
|
+
print(" Note: on CPU the PyTorch fallback is used. bsr_spmm wins at")
|
|
279
|
+
print(" large seq_len on Trainium, where dispatch overhead amortizes")
|
|
280
|
+
print(" against the tile-level nc_matmul throughput advantage.")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Sparse Fock matrix build with integral screening.
|
|
2
|
+
|
|
3
|
+
Three paths, side by side:
|
|
4
|
+
|
|
5
|
+
1. **v0.1.x unfused** — `schwarz_bounds` → `screen_quartets` → mask-apply
|
|
6
|
+
→ `from_dense` → `spmm`. What the library shipped before v0.4.0.
|
|
7
|
+
Four host passes over an (n, n) mask + a separate CSR build + a
|
|
8
|
+
separate SpMM dispatch.
|
|
9
|
+
2. **v0.4.0 fused** — `screened_spmm(A, diag_integrals, B, threshold)`.
|
|
10
|
+
One NKI kernel on the NKI backend; explicit mask + matmul on CPU.
|
|
11
|
+
Same numeric result, fewer HBM round-trips, no mask tensor on HBM.
|
|
12
|
+
3. **Full Fock build** — the coulomb J from path 2 contracted against
|
|
13
|
+
MO coefficients via trnblas: `F_MO = C.T @ J @ C`. Demonstrates the
|
|
14
|
+
suite composition — trnsparse hands off to trnblas's GEMM once the
|
|
15
|
+
sparse step is done.
|
|
16
|
+
|
|
17
|
+
Schwarz bounds here are synthetic (Gaussian-distance decay) — realistic
|
|
18
|
+
enough to show non-trivial sparsity. For real AO integrals from a
|
|
19
|
+
molecule, see `examples/pyscf_bridge.py`.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
python examples/sparse_fock.py --demo
|
|
23
|
+
python examples/sparse_fock.py --nbasis 200 --threshold 1e-8
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
import torch
|
|
32
|
+
|
|
33
|
+
import trnsparse
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _synthetic_schwarz_system(n: int, seed: int = 42) -> torch.Tensor:
|
|
37
|
+
"""Return synthetic diagonal integrals `(μμ|μμ)` for a 1-D molecular chain.
|
|
38
|
+
|
|
39
|
+
Chemistry convention: the Schwarz bound for `(μν|μν)` factors as
|
|
40
|
+
`Q[μ] * Q[ν]` where `Q[i] = sqrt((ii|ii))`. This demo generates
|
|
41
|
+
per-shell magnitudes that span several orders of magnitude so the
|
|
42
|
+
outer-product bound `Q[i] * Q[j]` produces non-trivial sparsity at
|
|
43
|
+
a realistic threshold.
|
|
44
|
+
"""
|
|
45
|
+
torch.manual_seed(seed)
|
|
46
|
+
# Shells arranged in a 1-D chain; Gaussian-like decay in magnitude
|
|
47
|
+
# from a central "heavy" region so there's a tail of small Q values.
|
|
48
|
+
idx = torch.arange(n, dtype=torch.float32)
|
|
49
|
+
center = n / 2.0
|
|
50
|
+
diag_integrals = torch.exp(-((idx - center) ** 2) / (2.0 * (n / 6.0) ** 2)) + 0.01
|
|
51
|
+
return diag_integrals
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _unfused_path(
|
|
55
|
+
integrals_dense: torch.Tensor, diag_integrals: torch.Tensor, P: torch.Tensor, threshold: float
|
|
56
|
+
):
|
|
57
|
+
"""Path 1: explicit Schwarz bound + mask + from_dense + spmm. v0.1.x flow."""
|
|
58
|
+
import math
|
|
59
|
+
|
|
60
|
+
t0 = time.perf_counter()
|
|
61
|
+
Q = trnsparse.schwarz_bounds(diag_integrals) # (n,)
|
|
62
|
+
pair_bound = Q.unsqueeze(-1) * Q.unsqueeze(0) # (n, n)
|
|
63
|
+
mask = pair_bound > math.sqrt(threshold)
|
|
64
|
+
integrals_masked = integrals_dense * mask.to(integrals_dense.dtype)
|
|
65
|
+
integrals_sparse = trnsparse.from_dense(integrals_masked)
|
|
66
|
+
J = trnsparse.spmm(integrals_sparse, P)
|
|
67
|
+
return J, time.perf_counter() - t0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fused_path(
|
|
71
|
+
integrals_dense: torch.Tensor, diag_integrals: torch.Tensor, P: torch.Tensor, threshold: float
|
|
72
|
+
):
|
|
73
|
+
"""Path 2: v0.4.0 fused screened_spmm."""
|
|
74
|
+
t0 = time.perf_counter()
|
|
75
|
+
J = trnsparse.screened_spmm(integrals_dense, diag_integrals, P, threshold=threshold)
|
|
76
|
+
return J, time.perf_counter() - t0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _full_fock_build(J: torch.Tensor, C: torch.Tensor):
|
|
80
|
+
"""Path 3: transform the coulomb J into the MO basis via trnblas.
|
|
81
|
+
|
|
82
|
+
F_MO = C.T @ J @ C — two GEMMs. Falls back to torch.matmul if
|
|
83
|
+
trnblas isn't importable (it's an optional suite dep; pure-trnsparse
|
|
84
|
+
users don't need it).
|
|
85
|
+
"""
|
|
86
|
+
t0 = time.perf_counter()
|
|
87
|
+
try:
|
|
88
|
+
import trnblas
|
|
89
|
+
|
|
90
|
+
Jt = trnblas.gemm(1.0, C, J, transA=True) # C.T @ J
|
|
91
|
+
F_MO = trnblas.gemm(1.0, Jt, C) # (C.T @ J) @ C
|
|
92
|
+
backend = "trnblas"
|
|
93
|
+
except ImportError:
|
|
94
|
+
F_MO = C.T @ J @ C
|
|
95
|
+
backend = "torch.matmul (trnblas not installed)"
|
|
96
|
+
return F_MO, time.perf_counter() - t0, backend
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main() -> None:
|
|
100
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
101
|
+
parser.add_argument("--demo", action="store_true", help="run a small demo")
|
|
102
|
+
parser.add_argument("--nbasis", type=int, default=50)
|
|
103
|
+
parser.add_argument("--threshold", type=float, default=1e-4)
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
|
|
106
|
+
if args.demo:
|
|
107
|
+
args.nbasis = 50
|
|
108
|
+
|
|
109
|
+
n = args.nbasis
|
|
110
|
+
print("Sparse Fock build:")
|
|
111
|
+
print(f" Basis functions: {n}")
|
|
112
|
+
print(f" Threshold: {args.threshold:.0e}")
|
|
113
|
+
|
|
114
|
+
diag_integrals = _synthetic_schwarz_system(n)
|
|
115
|
+
Q = trnsparse.schwarz_bounds(diag_integrals) # 1-D Schwarz bounds
|
|
116
|
+
|
|
117
|
+
# Unscreened ERI slice — random, scaled by outer-product Schwarz
|
|
118
|
+
# (chemistry-realistic: integrals tracking the bound).
|
|
119
|
+
torch.manual_seed(0)
|
|
120
|
+
integrals_dense = torch.randn(n, n) * (Q.unsqueeze(-1) * Q.unsqueeze(0)) * 0.01
|
|
121
|
+
|
|
122
|
+
# Density matrix — random SPD.
|
|
123
|
+
M = torch.randn(n, n) * 0.1
|
|
124
|
+
P = M @ M.T
|
|
125
|
+
|
|
126
|
+
# MO coefficients (for the trnblas transform in path 3) — orthonormal.
|
|
127
|
+
U, _ = torch.linalg.qr(torch.randn(n, n))
|
|
128
|
+
C = U
|
|
129
|
+
|
|
130
|
+
# --- Path 1: unfused ---
|
|
131
|
+
J_unfused, t_unfused = _unfused_path(integrals_dense, diag_integrals, P, args.threshold)
|
|
132
|
+
|
|
133
|
+
# --- Path 2: fused ---
|
|
134
|
+
J_fused, t_fused = _fused_path(integrals_dense, diag_integrals, P, args.threshold)
|
|
135
|
+
|
|
136
|
+
# --- Path 3: trnblas MO transform ---
|
|
137
|
+
F_MO, t_transform, backend = _full_fock_build(J_fused, C)
|
|
138
|
+
|
|
139
|
+
# --- Sparsity stats for context ---
|
|
140
|
+
# Build the pair-bound matrix for reporting stats at the matmul scale.
|
|
141
|
+
pair_bound = Q.unsqueeze(-1) * Q.unsqueeze(0)
|
|
142
|
+
stats = trnsparse.sparsity_stats(pair_bound, args.threshold**0.5)
|
|
143
|
+
|
|
144
|
+
print()
|
|
145
|
+
print(" Sparsity statistics:")
|
|
146
|
+
print(f" Total shell pairs: {stats['total_pairs']}")
|
|
147
|
+
print(f" Significant pairs: {stats['significant_pairs']}")
|
|
148
|
+
print(f" Pair sparsity: {stats['pair_sparsity']:.1%}")
|
|
149
|
+
print()
|
|
150
|
+
print(" Coulomb build timings:")
|
|
151
|
+
print(f" Path 1 (unfused, 4-step): {t_unfused * 1e3:8.3f} ms")
|
|
152
|
+
print(
|
|
153
|
+
f" Path 2 (fused screened_spmm): {t_fused * 1e3:8.3f} ms ({t_unfused / t_fused:.2f}x vs unfused)"
|
|
154
|
+
)
|
|
155
|
+
print()
|
|
156
|
+
print(" Full Fock build (trnsparse → trnblas):")
|
|
157
|
+
print(f" MO transform (C.T @ J @ C): {t_transform * 1e3:8.3f} ms via {backend}")
|
|
158
|
+
print()
|
|
159
|
+
print(f" Unfused/fused J agreement: max |ΔJ| = {(J_unfused - J_fused).abs().max().item():.2e}")
|
|
160
|
+
print(f" F_MO shape: {tuple(F_MO.shape)}, mean |F_MO| = {F_MO.abs().mean().item():.3e}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
|
@@ -24,6 +24,8 @@ nav:
|
|
|
24
24
|
- Architecture: architecture.md
|
|
25
25
|
- Migration from scipy.sparse: migration_scipy.md
|
|
26
26
|
- Benchmarks: benchmarks.md
|
|
27
|
+
- Iterative Solvers: iterative_solvers.md
|
|
28
|
+
- Sparse Attention: sparse_attention.md
|
|
27
29
|
- Deployment:
|
|
28
30
|
- AWS Setup for Neuron CI: aws_setup.md
|
|
29
31
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Tests for block-sparse attention example and pattern helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
import trnsparse
|
|
11
|
+
|
|
12
|
+
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
|
|
13
|
+
sys.path.insert(0, str(EXAMPLES))
|
|
14
|
+
|
|
15
|
+
import block_sparse_attention as demo # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestMaskShapes:
|
|
19
|
+
"""Pattern helper sanity checks — block count and coverage."""
|
|
20
|
+
|
|
21
|
+
def test_local_window_mask_shape(self):
|
|
22
|
+
seq_len, block_size, window = 512, 128, 2
|
|
23
|
+
mask = demo._local_window_mask(seq_len, block_size, window)
|
|
24
|
+
assert mask.shape == (seq_len, seq_len)
|
|
25
|
+
assert mask.dtype == torch.bool
|
|
26
|
+
# Each block-row should have at most 2*window+1 nonzero blocks.
|
|
27
|
+
n_blocks = seq_len // block_size
|
|
28
|
+
bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
29
|
+
for i in range(n_blocks):
|
|
30
|
+
row_count = (bsr.block_row_ptrs[i + 1] - bsr.block_row_ptrs[i]).item()
|
|
31
|
+
assert row_count <= 2 * window + 1
|
|
32
|
+
|
|
33
|
+
def test_local_window_mask_symmetric(self):
|
|
34
|
+
mask = demo._local_window_mask(256, 128, 1)
|
|
35
|
+
assert (mask == mask.T).all(), "local window mask should be symmetric"
|
|
36
|
+
|
|
37
|
+
def test_dilated_mask_shape(self):
|
|
38
|
+
seq_len, block_size, stride = 512, 128, 2
|
|
39
|
+
mask = demo._dilated_mask(seq_len, block_size, stride)
|
|
40
|
+
assert mask.shape == (seq_len, seq_len)
|
|
41
|
+
n_blocks = seq_len // block_size
|
|
42
|
+
# For stride=2 and n_blocks=4: blocks 0,2 from row 0; blocks 1,3 from row 1; etc.
|
|
43
|
+
bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
44
|
+
expected_per_row = (n_blocks + stride - 1) // stride
|
|
45
|
+
for i in range(n_blocks):
|
|
46
|
+
row_count = (bsr.block_row_ptrs[i + 1] - bsr.block_row_ptrs[i]).item()
|
|
47
|
+
assert row_count == expected_per_row
|
|
48
|
+
|
|
49
|
+
def test_global_token_mask_global_rows_full(self):
|
|
50
|
+
seq_len, block_size, n_global = 384, 128, 1
|
|
51
|
+
mask = demo._global_token_mask(seq_len, block_size, window=1, n_global=n_global)
|
|
52
|
+
# First n_global*block_size rows must be fully attended.
|
|
53
|
+
assert mask[: n_global * block_size, :].all()
|
|
54
|
+
# First n_global*block_size columns must be fully attended by all rows.
|
|
55
|
+
assert mask[:, : n_global * block_size].all()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestAttentionParity:
|
|
59
|
+
"""bsr_spmm attention matches dense reference."""
|
|
60
|
+
|
|
61
|
+
def _run_parity(self, seq_len, head_dim, block_size, mask):
|
|
62
|
+
torch.manual_seed(0)
|
|
63
|
+
Q = torch.randn(seq_len, head_dim)
|
|
64
|
+
K = torch.randn(seq_len, head_dim)
|
|
65
|
+
V = torch.randn(seq_len, head_dim)
|
|
66
|
+
|
|
67
|
+
mask_bsr = trnsparse.BSRMatrix.from_dense(mask.float(), block_size=block_size)
|
|
68
|
+
out_bsr = demo.block_sparse_attention(Q, K, V, mask_bsr)
|
|
69
|
+
out_dense = demo._dense_reference(Q, K, V, mask)
|
|
70
|
+
|
|
71
|
+
torch.testing.assert_close(out_bsr, out_dense, atol=1e-4, rtol=1e-4)
|
|
72
|
+
assert out_bsr.shape == (seq_len, head_dim)
|
|
73
|
+
|
|
74
|
+
def test_local_window_parity(self):
|
|
75
|
+
seq_len, head_dim, block_size = 256, 32, 128
|
|
76
|
+
mask = demo._local_window_mask(seq_len, block_size, window=1)
|
|
77
|
+
self._run_parity(seq_len, head_dim, block_size, mask)
|
|
78
|
+
|
|
79
|
+
def test_dilated_parity(self):
|
|
80
|
+
seq_len, head_dim, block_size = 256, 32, 128
|
|
81
|
+
mask = demo._dilated_mask(seq_len, block_size, stride=2)
|
|
82
|
+
self._run_parity(seq_len, head_dim, block_size, mask)
|
|
83
|
+
|
|
84
|
+
def test_global_token_parity(self):
|
|
85
|
+
seq_len, head_dim, block_size = 384, 32, 128
|
|
86
|
+
mask = demo._global_token_mask(seq_len, block_size, window=1, n_global=1)
|
|
87
|
+
self._run_parity(seq_len, head_dim, block_size, mask)
|
|
88
|
+
|
|
89
|
+
def test_full_attention_is_dense(self):
|
|
90
|
+
"""A fully-nonzero BSR mask should match plain dense attention."""
|
|
91
|
+
seq_len, head_dim, block_size = 256, 32, 128
|
|
92
|
+
mask = torch.ones(seq_len, seq_len, dtype=torch.bool)
|
|
93
|
+
self._run_parity(seq_len, head_dim, block_size, mask)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Smoke tests for the examples directory.
|
|
2
|
+
|
|
3
|
+
These cover the user-facing integration demos without executing them as
|
|
4
|
+
subprocesses — the tests import the example modules and call their
|
|
5
|
+
entry points directly so failures surface with full tracebacks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
|
|
18
|
+
sys.path.insert(0, str(EXAMPLES))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestSparseFockDemo:
|
|
22
|
+
def test_unfused_path_runs(self):
|
|
23
|
+
"""Exercise the v0.1.x multi-step screening path end-to-end."""
|
|
24
|
+
import sparse_fock as demo
|
|
25
|
+
|
|
26
|
+
diag = demo._synthetic_schwarz_system(50)
|
|
27
|
+
import trnsparse
|
|
28
|
+
|
|
29
|
+
Q = trnsparse.schwarz_bounds(diag)
|
|
30
|
+
A = torch.randn(50, 50) * (Q.unsqueeze(-1) * Q.unsqueeze(0)) * 0.01
|
|
31
|
+
P = torch.randn(50, 50)
|
|
32
|
+
J, t = demo._unfused_path(A, diag, P, threshold=1e-4)
|
|
33
|
+
assert J.shape == (50, 50)
|
|
34
|
+
assert torch.isfinite(J).all()
|
|
35
|
+
assert t > 0
|
|
36
|
+
|
|
37
|
+
def test_fused_vs_unfused_parity(self):
|
|
38
|
+
"""The two paths should agree up to fp tolerance on the same inputs."""
|
|
39
|
+
import sparse_fock as demo
|
|
40
|
+
|
|
41
|
+
torch.manual_seed(7)
|
|
42
|
+
diag = demo._synthetic_schwarz_system(40)
|
|
43
|
+
import trnsparse
|
|
44
|
+
|
|
45
|
+
Q = trnsparse.schwarz_bounds(diag)
|
|
46
|
+
A = torch.randn(40, 40) * (Q.unsqueeze(-1) * Q.unsqueeze(0)) * 0.01
|
|
47
|
+
P = torch.randn(40, 40)
|
|
48
|
+
threshold = 1e-4
|
|
49
|
+
|
|
50
|
+
J_unfused, _ = demo._unfused_path(A, diag, P, threshold)
|
|
51
|
+
J_fused, _ = demo._fused_path(A, diag, P, threshold)
|
|
52
|
+
|
|
53
|
+
torch.testing.assert_close(J_unfused, J_fused, atol=1e-6, rtol=1e-6)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestPyScfBridge:
|
|
57
|
+
"""Skips cleanly without PySCF; asserts non-trivial screening on H2O."""
|
|
58
|
+
|
|
59
|
+
def test_h2o_sto3g_screening(self):
|
|
60
|
+
pytest.importorskip("pyscf")
|
|
61
|
+
import pyscf_bridge as demo
|
|
62
|
+
|
|
63
|
+
report = demo.run_demo("h2o", "sto-3g", threshold=1e-8)
|
|
64
|
+
assert report["nao"] > 0
|
|
65
|
+
# Tight tolerance would give no sparsity on a tiny basis; sto-3g
|
|
66
|
+
# H2O has only 7 AOs, so we just assert the API ran and produced
|
|
67
|
+
# a finite result — non-trivial sparsity needs larger bases.
|
|
68
|
+
assert math.isfinite(report["output_norm"])
|
|
69
|
+
assert 0.0 <= report["pair_sparsity"] <= 1.0
|
|
70
|
+
assert report["output_shape"] == (report["nao"], report["nao"])
|
|
@@ -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.4.
|
|
8
|
+
__version__ = "0.4.2"
|
|
9
9
|
|
|
10
10
|
from .formats import BSRMatrix, COOMatrix, CSRMatrix, eye_sparse, from_dense, from_scipy
|
|
11
11
|
from .iterative import bsr_diagonal, cg_bsr, jacobi_preconditioner_bsr, power_iteration_bsr
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trnsparse
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Sparse matrix operations for AWS Trainium via NKI
|
|
5
5
|
Author-email: Scott Friedman <scttfrdmn@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -102,3 +102,16 @@ stats = trnsparse.sparsity_stats(Q)
|
|
|
102
102
|
## License
|
|
103
103
|
|
|
104
104
|
Apache 2.0 — Copyright 2026 Scott Friedman
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Disclaimer
|
|
108
|
+
|
|
109
|
+
trnsci is an **independent open-source project**. It is not sponsored by, endorsed by, or affiliated with Amazon.com, Inc., Amazon Web Services, Inc., or Annapurna Labs Ltd.
|
|
110
|
+
|
|
111
|
+
"AWS", "Amazon", "Trainium", "Inferentia", "NeuronCore", "Neuron SDK", and related identifiers are trademarks of their respective owners and are used here solely for descriptive and interoperability purposes. Use does not imply endorsement, partnership, or any other relationship.
|
|
112
|
+
|
|
113
|
+
All work, opinions, analyses, benchmark results, architectural commentary, and editorial judgments in this repository and on [trnsci.dev](https://trnsci.dev) are those of the project's contributors. They do not represent the views, positions, or commitments of Amazon, AWS, or Annapurna Labs.
|
|
114
|
+
|
|
115
|
+
Feedback directed at the Neuron SDK or Trainium hardware is good-faith ecosystem commentary from independent users. It is not privileged information, is not pre-reviewed by AWS, and should not be read as authoritative about product roadmap, behavior, or quality.
|
|
116
|
+
|
|
117
|
+
For official AWS guidance, see [aws-neuron documentation](https://awsdocs-neuron.readthedocs-hosted.com/) and the [AWS Trainium product page](https://aws.amazon.com/ai/machine-learning/trainium/).
|
|
@@ -26,6 +26,8 @@ docs/installation.md
|
|
|
26
26
|
docs/iterative_solvers.md
|
|
27
27
|
docs/migration_scipy.md
|
|
28
28
|
docs/quickstart.md
|
|
29
|
+
docs/sparse_attention.md
|
|
30
|
+
examples/block_sparse_attention.py
|
|
29
31
|
examples/sparse_fock.py
|
|
30
32
|
infra/terraform/.terraform.lock.hcl
|
|
31
33
|
infra/terraform/README.md
|
|
@@ -35,7 +37,9 @@ scripts/run_benchmarks.sh
|
|
|
35
37
|
scripts/run_neuron_tests.sh
|
|
36
38
|
scripts/run_simulator_tests.sh
|
|
37
39
|
tests/conftest.py
|
|
40
|
+
tests/test_attention.py
|
|
38
41
|
tests/test_bsr.py
|
|
42
|
+
tests/test_examples.py
|
|
39
43
|
tests/test_formats.py
|
|
40
44
|
tests/test_iterative.py
|
|
41
45
|
tests/test_nki_bsr.py
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Sparse Fock matrix build with integral screening.
|
|
3
|
-
|
|
4
|
-
Demonstrates how Schwarz screening reduces the Fock build from O(N⁴) to
|
|
5
|
-
effectively O(N²) for large molecules. The sparsity pattern is stored
|
|
6
|
-
as a CSR matrix, and SpMM handles the screened contraction.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python examples/sparse_fock.py --demo
|
|
10
|
-
python examples/sparse_fock.py --nbasis 200
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import argparse
|
|
14
|
-
import time
|
|
15
|
-
|
|
16
|
-
import torch
|
|
17
|
-
|
|
18
|
-
import trnsparse
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def main():
|
|
22
|
-
parser = argparse.ArgumentParser()
|
|
23
|
-
parser.add_argument("--demo", action="store_true")
|
|
24
|
-
parser.add_argument("--nbasis", type=int, default=50)
|
|
25
|
-
parser.add_argument("--threshold", type=float, default=1e-10)
|
|
26
|
-
args = parser.parse_args()
|
|
27
|
-
|
|
28
|
-
if args.demo:
|
|
29
|
-
args.nbasis = 50
|
|
30
|
-
|
|
31
|
-
n = args.nbasis
|
|
32
|
-
print("Sparse Fock build:")
|
|
33
|
-
print(f" Basis functions: {n}")
|
|
34
|
-
print(f" Threshold: {args.threshold:.0e}")
|
|
35
|
-
|
|
36
|
-
torch.manual_seed(42)
|
|
37
|
-
|
|
38
|
-
# Simulate Schwarz bounds (decay with distance for realistic sparsity)
|
|
39
|
-
positions = torch.rand(n, 3) * 10.0 # Random 3D positions
|
|
40
|
-
distances = torch.cdist(positions, positions)
|
|
41
|
-
Q = torch.exp(-0.5 * distances) # Gaussian decay
|
|
42
|
-
|
|
43
|
-
# Screen
|
|
44
|
-
stats = trnsparse.sparsity_stats(Q, args.threshold)
|
|
45
|
-
print("\n Sparsity statistics:")
|
|
46
|
-
print(f" Total shell pairs: {stats['total_pairs']}")
|
|
47
|
-
print(f" Significant pairs: {stats['significant_pairs']}")
|
|
48
|
-
print(f" Pair sparsity: {stats['pair_sparsity']:.1%}")
|
|
49
|
-
print(f" Quartet sparsity (lower): {stats['quartet_sparsity_lower']:.1%}")
|
|
50
|
-
|
|
51
|
-
# Build sparse integral matrix (simulated)
|
|
52
|
-
mask = trnsparse.screen_quartets(Q, args.threshold)
|
|
53
|
-
integrals_dense = torch.randn(n, n) * Q * 0.01
|
|
54
|
-
integrals_dense[~mask] = 0.0
|
|
55
|
-
integrals_sparse = trnsparse.from_dense(integrals_dense)
|
|
56
|
-
print(f" Integral matrix nnz: {integrals_sparse.nnz} / {n * n}")
|
|
57
|
-
|
|
58
|
-
# Density matrix (random SPD for demo)
|
|
59
|
-
P = torch.randn(n, n) * 0.1
|
|
60
|
-
P = P @ P.T
|
|
61
|
-
|
|
62
|
-
# Sparse Fock build: J_μν = Σ_λσ P_λσ * (μν|λσ)
|
|
63
|
-
# Approximated here as SpMM: J ≈ sparse_integrals @ P
|
|
64
|
-
t0 = time.perf_counter()
|
|
65
|
-
J_sparse = trnsparse.spmm(integrals_sparse, P)
|
|
66
|
-
t_sparse = time.perf_counter() - t0
|
|
67
|
-
|
|
68
|
-
# Dense reference
|
|
69
|
-
t0 = time.perf_counter()
|
|
70
|
-
J_dense = integrals_dense @ P
|
|
71
|
-
t_dense = time.perf_counter() - t0
|
|
72
|
-
|
|
73
|
-
error = torch.linalg.norm(J_sparse - J_dense).item()
|
|
74
|
-
print(f"\n Sparse SpMM: {t_sparse:.4f}s")
|
|
75
|
-
print(f" Dense matmul: {t_dense:.4f}s")
|
|
76
|
-
print(f" Error: {error:.2e}")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if __name__ == "__main__":
|
|
80
|
-
main()
|
|
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
|
|
File without changes
|
|
File without changes
|