trnsparse 0.8.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {trnsparse-0.8.0 → trnsparse-0.10.0}/CHANGELOG.md +63 -0
  2. {trnsparse-0.8.0 → trnsparse-0.10.0}/PKG-INFO +1 -1
  3. trnsparse-0.10.0/benchmarks/bench_multicore_trn2.py +466 -0
  4. trnsparse-0.10.0/benchmarks/bench_multiproc_trn2.py +130 -0
  5. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_native_trn2.py +60 -13
  6. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/api.md +31 -0
  7. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/architecture.md +18 -0
  8. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/benchmarks.md +159 -0
  9. {trnsparse-0.8.0 → trnsparse-0.10.0}/pyproject.toml +1 -1
  10. trnsparse-0.10.0/tests/test_core_split.py +53 -0
  11. trnsparse-0.10.0/tests/test_multicore.py +206 -0
  12. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_native_dispatch.py +108 -3
  13. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_compile.py +159 -0
  14. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_native.py +104 -0
  15. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/__init__.py +1 -1
  16. trnsparse-0.10.0/trnsparse/multicore.py +315 -0
  17. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/nki/_native.py +82 -0
  18. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/nki/_native_attn.py +20 -4
  19. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/nki/dispatch.py +78 -22
  20. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/nki/kernels.py +333 -49
  21. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse.egg-info/PKG-INFO +1 -1
  22. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse.egg-info/SOURCES.txt +5 -0
  23. {trnsparse-0.8.0 → trnsparse-0.10.0}/.github/workflows/ci.yml +0 -0
  24. {trnsparse-0.8.0 → trnsparse-0.10.0}/.github/workflows/notify-umbrella.yml +0 -0
  25. {trnsparse-0.8.0 → trnsparse-0.10.0}/.github/workflows/publish.yml +0 -0
  26. {trnsparse-0.8.0 → trnsparse-0.10.0}/.gitignore +0 -0
  27. {trnsparse-0.8.0 → trnsparse-0.10.0}/.pre-commit-config.yaml +0 -0
  28. {trnsparse-0.8.0 → trnsparse-0.10.0}/CLAUDE.md +0 -0
  29. {trnsparse-0.8.0 → trnsparse-0.10.0}/CODE_OF_CONDUCT.md +0 -0
  30. {trnsparse-0.8.0 → trnsparse-0.10.0}/CONTRIBUTING.md +0 -0
  31. {trnsparse-0.8.0 → trnsparse-0.10.0}/LICENSE +0 -0
  32. {trnsparse-0.8.0 → trnsparse-0.10.0}/README.md +0 -0
  33. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_bsr_spmm.py +0 -0
  34. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_iterative.py +0 -0
  35. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_screening.py +0 -0
  36. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_spmm.py +0 -0
  37. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/bench_spmv.py +0 -0
  38. {trnsparse-0.8.0 → trnsparse-0.10.0}/benchmarks/conftest.py +0 -0
  39. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/aws_setup.md +0 -0
  40. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/index.md +0 -0
  41. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/installation.md +0 -0
  42. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/iterative_solvers.md +0 -0
  43. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/migration_scipy.md +0 -0
  44. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/quickstart.md +0 -0
  45. {trnsparse-0.8.0 → trnsparse-0.10.0}/docs/sparse_attention.md +0 -0
  46. {trnsparse-0.8.0 → trnsparse-0.10.0}/examples/block_sparse_attention.py +0 -0
  47. {trnsparse-0.8.0 → trnsparse-0.10.0}/examples/sparse_fock.py +0 -0
  48. {trnsparse-0.8.0 → trnsparse-0.10.0}/infra/terraform/.terraform.lock.hcl +0 -0
  49. {trnsparse-0.8.0 → trnsparse-0.10.0}/infra/terraform/README.md +0 -0
  50. {trnsparse-0.8.0 → trnsparse-0.10.0}/infra/terraform/main.tf +0 -0
  51. {trnsparse-0.8.0 → trnsparse-0.10.0}/mkdocs.yml +0 -0
  52. {trnsparse-0.8.0 → trnsparse-0.10.0}/scripts/bench_to_md.py +0 -0
  53. {trnsparse-0.8.0 → trnsparse-0.10.0}/scripts/run_benchmarks.sh +0 -0
  54. {trnsparse-0.8.0 → trnsparse-0.10.0}/scripts/run_neuron_tests.sh +0 -0
  55. {trnsparse-0.8.0 → trnsparse-0.10.0}/scripts/run_simulator_tests.sh +0 -0
  56. {trnsparse-0.8.0 → trnsparse-0.10.0}/setup.cfg +0 -0
  57. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/conftest.py +0 -0
  58. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_attention.py +0 -0
  59. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_attn_gather.py +0 -0
  60. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_bsr.py +0 -0
  61. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_examples.py +0 -0
  62. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_formats.py +0 -0
  63. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_iterative.py +0 -0
  64. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_attn.py +0 -0
  65. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_bsr.py +0 -0
  66. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_screened_spmm.py +0 -0
  67. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_sim.py +0 -0
  68. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_nki_spmm.py +0 -0
  69. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_ops.py +0 -0
  70. {trnsparse-0.8.0 → trnsparse-0.10.0}/tests/test_screening.py +0 -0
  71. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/formats.py +0 -0
  72. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/iterative.py +0 -0
  73. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/nki/__init__.py +0 -0
  74. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/ops.py +0 -0
  75. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse/screening.py +0 -0
  76. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse.egg-info/dependency_links.txt +0 -0
  77. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse.egg-info/requires.txt +0 -0
  78. {trnsparse-0.8.0 → trnsparse-0.10.0}/trnsparse.egg-info/top_level.txt +0 -0
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] — 2026-10-31
11
+
12
+ Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron) — see trnsci/trnsci#35.
13
+
14
+ Headline: pre-transposed static BSR SpMM beats dense aten up to ~82–98% block density on one core (was 26–49%), plus `ShardedBSR`/`ShardedAttention` across logical NeuronCores (#38).
15
+
16
+ ### Added
17
+
18
+ #### Static BSR SpMM and block-sparse attention across logical NeuronCores (#30)
19
+
20
+ - `trnsparse.multicore`: `ShardedBSR` (block rows split across the logical
21
+ cores of a Trn2 chip, balanced by stored blocks), `ShardedAttention`
22
+ (single-head attention forward split by query block rows),
23
+ `partition_block_rows` and `logical_cores`. Per-core operands are built
24
+ on the host and stay resident; one kernel launch per core, issued
25
+ without waiting.
26
+ - Pre-transposed BSR kernel (`bsr_spmm_t_keyed`, the `ShardedBSR`
27
+ default). The blocks are stored transposed, side by side, so the kernel
28
+ needs no in-kernel `load_transpose2d`. It loads up to 16 of a row's
29
+ blocks per DMA, reads PSUM out with one copy, and shares each B tile
30
+ across groups of up to 8 block rows, one PSUM bank each
31
+ (`TRNSPARSE_ROW_GROUP`). The kernel has autograd.
32
+ - `benchmarks/bench_multicore_trn2.py` (1, 2 and 4 logical cores, against
33
+ dense aten on 1 core and split across cores) and
34
+ `benchmarks/bench_multiproc_trn2.py` (one process per core).
35
+
36
+ ### Changed
37
+
38
+ - The static kernels take their structure as a registered key, a content
39
+ hash (`kernels.register_structure`), instead of int lists. The key
40
+ crosses the custom-op boundary as one int, so the host cost of a launch
41
+ no longer grows with the stored-block count: on Trn2 it was 0.13 ms at
42
+ 41 blocks and 0.67 ms at 2048, and is now about 0.12 ms at any size. The
43
+ public `bsr_spmm` and attention-forward paths use the keyed ops.
44
+ - Native kernels run on the logical core that holds their operands
45
+ (`neuron:k`) instead of always on the default `neuron` device. Operands
46
+ spread over two cores raise an error. TorchNeuron does not copy between
47
+ logical cores: `.to()` returns the tensor still on its old core.
48
+ - Kernel docstrings now give the measured `nl.reciprocal` behaviour:
49
+ about 1.5e-6 relative error in range, but it silently returns 1e7 or 0
50
+ outside roughly |x| in [2^-42, 2^41].
51
+
52
+ ## [0.9.0] — 2026-10-30
53
+
54
+ Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron; LNC=1 and LNC=2) — see trnsci/trnsci#35.
55
+
56
+ Headline performance: static BSR SpMM and block-sparse attention split across both LNC=2 cores, with bf16 operands on fp32 PSUM; the fp32 BSR crossover against dense aten at n=8192, width 512 rises from ~27% to ~49% block density, and at seq 8192 attention forward plus backward beats unmasked dense SDPA for all three masks (#36).
57
+
58
+ ### Changed
59
+
60
+ #### Native path: static kernels on both LNC=2 cores, bf16 operands (#30)
61
+
62
+ - `bsr_spmm_static`, `attn_fwd_static` and `attn_bwd_static` split their
63
+ output tiles between the two physical cores of an LNC=2 logical core
64
+ (`program_id`), balanced by stored blocks; each output tile has one
65
+ writer. `TRNSPARSE_CORE_SPLIT=0` turns the split off.
66
+ - bf16 inputs (all operands bf16) run bf16 matmuls with fp32 PSUM
67
+ accumulation; scores and softmax statistics stay fp32, and the attention
68
+ statistics returned by `trnsparse::bsr_attn` are fp32 for bf16 / fp16 Q.
69
+ Mixed fp32/bf16 operands run in fp32.
70
+ - `benchmarks/bench_native_trn2.py`: `--dtype bf16` (matched-precision
71
+ dense aten baselines) and `--fast` (device columns only).
72
+
10
73
  ## [0.8.0] — 2026-10-30
11
74
 
12
75
  Native TorchNeuron dispatch (Trn2/Trn3) alongside the PyTorch/XLA path (Trn1, maintenance); validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0) — see trnsci/trnsci#35.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trnsparse
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Sparse matrix operations for AWS Trainium via NKI
5
5
  Author-email: Scott Friedman <scttfrdmn@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,466 @@
1
+ """Multi-core (logical NeuronCore) scaling benchmark for Trn2, native TorchNeuron.
2
+
3
+ Run with every logical core of the chip visible to one process:
4
+
5
+ NEURON_RT_VISIBLE_CORES=0-3 NKI_ENABLE_TRACE_CACHE=0 \\
6
+ python benchmarks/bench_multicore_trn2.py --section bsr --size 8192 --widths 512
7
+
8
+ Sections:
9
+ bsr static BSR SpMM on 1, 2 and 4 logical cores (`ShardedBSR`,
10
+ block rows balanced by stored blocks) vs dense aten `A @ B` on
11
+ one core and with A's rows split across 2 and 4 cores.
12
+ attn single-head block-sparse attention forward, query block rows
13
+ split across 1, 2 and 4 cores (`ShardedAttention`), vs dense
14
+ and masked SDPA on one core and with the query rows split
15
+ across 4 cores.
16
+ heads multi-head block-sparse attention (``--heads``) forward and
17
+ forward+backward with whole heads on different cores, vs
18
+ dense SDPA over the same heads on one core and split across
19
+ cores.
20
+
21
+ Every case keeps its operands resident on the cores that use them (built
22
+ on the host, copied once; TorchNeuron does not copy between logical cores),
23
+ issues one launch per core from this thread, and then synchronizes every
24
+ core used. That is the time of a sharded call whose inputs and outputs stay
25
+ on the cores. The host gather of a sharded result is reported separately
26
+ (`*_gather_ms`). Warm median of ``--reps`` calls; the first calls compile.
27
+ """
28
+
29
+ # ruff: noqa: B023 (every closure is benchmarked before its loop variables change)
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import gc
34
+ import json
35
+ import statistics
36
+ import sys
37
+ import time
38
+ from pathlib import Path
39
+
40
+ import torch
41
+
42
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
43
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
44
+
45
+ from bench_native_trn2 import ( # noqa: E402
46
+ block_mask,
47
+ mask_bsr_from_block_mask,
48
+ random_bsr,
49
+ )
50
+
51
+ import trnsparse # noqa: E402
52
+ from trnsparse.multicore import ShardedAttention, ShardedBSR, logical_cores # noqa: E402
53
+
54
+ P = 128
55
+ F = torch.nn.functional
56
+
57
+
58
+ def pick(cores, k):
59
+ """k devices: the first k cores (wrapping onto the visible ones for smoke runs)."""
60
+ return [cores[i % len(cores)] for i in range(k)]
61
+
62
+
63
+ def sync(devs):
64
+ for i in sorted({d.index for d in devs}):
65
+ torch.neuron.synchronize(i)
66
+
67
+
68
+ def bench(fn, devs, warmup=2, reps=10):
69
+ for _ in range(warmup):
70
+ fn()
71
+ sync(devs)
72
+ ts = []
73
+ for _ in range(reps):
74
+ t0 = time.perf_counter()
75
+ fn()
76
+ sync(devs)
77
+ ts.append((time.perf_counter() - t0) * 1e3)
78
+ return statistics.median(ts)
79
+
80
+
81
+ def safe(fn, devs, **kw):
82
+ try:
83
+ return bench(fn, devs, **kw)
84
+ except Exception as e: # report, don't abort the section
85
+ print(f"# bench failed: {type(e).__name__}: {str(e)[:300]}", file=sys.stderr)
86
+ return None
87
+
88
+
89
+ def issue_time(fn, devs, reps=5):
90
+ """Host time to issue the launches (before any synchronization), median."""
91
+ ts = []
92
+ for _ in range(reps):
93
+ sync(devs)
94
+ t0 = time.perf_counter()
95
+ fn()
96
+ ts.append((time.perf_counter() - t0) * 1e3)
97
+ sync(devs)
98
+ return statistics.median(ts)
99
+
100
+
101
+ def cpu_bench(fn, reps=2):
102
+ fn()
103
+ ts = []
104
+ for _ in range(reps):
105
+ t0 = time.perf_counter()
106
+ fn()
107
+ ts.append((time.perf_counter() - t0) * 1e3)
108
+ return statistics.median(ts)
109
+
110
+
111
+ def rel_err(x, ref):
112
+ x = x.detach().cpu().double()
113
+ ref = ref.detach().cpu().double()
114
+ return float((x - ref).norm() / ref.norm().clamp_min(1e-30))
115
+
116
+
117
+ def emit(row, out):
118
+ line = json.dumps(row)
119
+ print(line, flush=True)
120
+ if out:
121
+ with open(out, "a") as f:
122
+ f.write(line + "\n")
123
+
124
+
125
+ def free():
126
+ gc.collect()
127
+
128
+
129
+ # ── BSR ──────────────────────────────────────────────────────────────────────
130
+
131
+
132
+ def _row_check(A, B, rows):
133
+ """fp64 reference for block rows `rows` of A @ B (host)."""
134
+ ptrs = A.block_row_ptrs.tolist()
135
+ cols = A.block_col_indices.tolist()
136
+ Bd = B.double()
137
+ out = []
138
+ for m in rows:
139
+ acc = torch.zeros(P, B.shape[1], dtype=torch.float64)
140
+ for idx in range(ptrs[m], ptrs[m + 1]):
141
+ j = cols[idx]
142
+ acc += A.blocks[idx].double() @ Bd[j * P : (j + 1) * P]
143
+ out.append(acc)
144
+ return torch.cat(out)
145
+
146
+
147
+ def section_bsr(args, cores):
148
+ n = args.size
149
+ core_counts = [int(c) for c in args.cores.split(",")]
150
+ for width in [int(x) for x in args.widths.split(",")]:
151
+ g = torch.Generator().manual_seed(1)
152
+ Bc = torch.randn(n, width, generator=g)
153
+ B_on = {d: Bc.to(d) for d in cores[: max(core_counts)]}
154
+ # Dense aten, one core and A's rows split across k cores (B on every core).
155
+ Ac = torch.randn(n, n, generator=g) / n**0.5
156
+ dense = {}
157
+ for k in core_counts:
158
+ devs = pick(cores, k)
159
+ slabs = [Ac[i * n // k : (i + 1) * n // k].to(devs[i]) for i in range(k)]
160
+ dense[k] = safe(
161
+ lambda: [s @ B_on[d] for s, d in zip(slabs, devs, strict=True)],
162
+ devs,
163
+ reps=args.reps,
164
+ )
165
+ slabs = None
166
+ free()
167
+ cpu_dense = cpu_bench(lambda: Ac @ Bc) if args.cpu else None
168
+ Ac = None
169
+ free()
170
+ for dens in [float(x) for x in args.densities.split(",")]:
171
+ A = random_bsr(n, dens)
172
+ counts = (A.block_row_ptrs[1:] - A.block_row_ptrs[:-1]).tolist()
173
+ row = {
174
+ "section": "bsr",
175
+ "n": n,
176
+ "width": width,
177
+ "block_density": dens,
178
+ "n_blocks": A.n_blocks,
179
+ "K_max": max(counts),
180
+ "cpu_dense_ms": cpu_dense,
181
+ }
182
+ for k in core_counts:
183
+ row[f"dense_{k}c_ms"] = dense[k]
184
+ # The public one-core API (as in #34 / #36), for continuity.
185
+ d0 = cores[0]
186
+ A0 = trnsparse.BSRMatrix(
187
+ blocks=A.blocks.to(d0),
188
+ block_col_indices=A.block_col_indices,
189
+ block_row_ptrs=A.block_row_ptrs,
190
+ shape=A.shape,
191
+ block_size=P,
192
+ )
193
+ trnsparse.bsr_spmm(A0, B_on[d0])
194
+ row["api_1c_ms"] = safe(lambda: trnsparse.bsr_spmm(A0, B_on[d0]), [d0], reps=args.reps)
195
+ A0 = None
196
+ variants = [("", False)] if not args.only_pretransposed else []
197
+ if args.pretransposed or args.only_pretransposed:
198
+ variants.append(("t_", True))
199
+ for tag, pre in variants:
200
+ for k in core_counts:
201
+ devs = pick(cores, k)
202
+ t0 = time.perf_counter()
203
+ S = ShardedBSR(A, devs, pretransposed=pre)
204
+ reps_B = [B_on[d] for d in S.devices]
205
+ outs = S.spmm(reps_B)
206
+ sync(S.devices)
207
+ row[f"sparse_{tag}{k}c_first_ms"] = (
208
+ time.perf_counter() - t0
209
+ ) * 1e3 # incl. compile
210
+ row[f"sparse_{tag}{k}c_blocks"] = S.stored_blocks
211
+ # correctness: two block rows from every shard
212
+ chk = []
213
+ for p, rows in enumerate(S.rows):
214
+ sel = rows[:1] + rows[-1:]
215
+ o = outs[p].cpu().reshape(len(rows), P, width)
216
+ pos = [0, len(rows) - 1]
217
+ chk.append(rel_err(o[pos].reshape(-1, width), _row_check(A, Bc, sel)))
218
+ row[f"sparse_{tag}{k}c_rel_err"] = max(chk)
219
+ row[f"sparse_{tag}{k}c_ms"] = safe(
220
+ lambda: S.spmm(reps_B), S.devices, reps=args.reps
221
+ )
222
+ row[f"sparse_{tag}{k}c_issue_ms"] = issue_time(
223
+ lambda: S.spmm(reps_B), S.devices
224
+ )
225
+ if k == max(core_counts) and not pre:
226
+ row[f"sparse_{tag}{k}c_gather_ms"] = safe(
227
+ lambda: S.gather(S.spmm(reps_B)), S.devices, reps=max(3, args.reps // 2)
228
+ )
229
+ outs = S = None
230
+ free()
231
+ k1 = min(core_counts)
232
+ for k in core_counts:
233
+ if row.get(f"sparse_t_{k}c_ms") and row.get(f"sparse_{k}c_ms"):
234
+ row[f"t_speedup_{k}c"] = row[f"sparse_{k}c_ms"] / row[f"sparse_t_{k}c_ms"]
235
+ if row.get(f"sparse_{k}c_ms") and row.get(f"sparse_{k1}c_ms"):
236
+ row[f"sparse_scaling_{k}c"] = row[f"sparse_{k1}c_ms"] / row[f"sparse_{k}c_ms"]
237
+ if row.get(f"sparse_{k}c_ms") and dense.get(k1):
238
+ row[f"dense_{k1}c_over_sparse_{k}c"] = dense[k1] / row[f"sparse_{k}c_ms"]
239
+ if row.get(f"sparse_{k}c_ms") and dense.get(k):
240
+ row[f"dense_{k}c_over_sparse_{k}c"] = dense[k] / row[f"sparse_{k}c_ms"]
241
+ emit(row, args.out)
242
+ B_on = None
243
+ free()
244
+
245
+
246
+ # ── Attention: one head, query rows split ────────────────────────────────────
247
+
248
+
249
+ def _sdpa(q, k, v, m=None):
250
+ return F.scaled_dot_product_attention(q[None], k[None], v[None], attn_mask=m)[0]
251
+
252
+
253
+ def section_attn(args, cores):
254
+ seq = args.size
255
+ nb = seq // P
256
+ core_counts = [int(c) for c in args.cores.split(",")]
257
+ kmax = max(core_counts)
258
+ for hd in [int(x) for x in args.head_dims.split(",")]:
259
+ g = torch.Generator().manual_seed(0)
260
+ Qc, Kc, Vc = (torch.randn(seq, hd, generator=g) for _ in range(3))
261
+ K_on = {d: Kc.to(d) for d in pick(cores, kmax)}
262
+ V_on = {d: Vc.to(d) for d in pick(cores, kmax)}
263
+ dense = {}
264
+ for k in core_counts:
265
+ devs = pick(cores, k)
266
+ qs = [Qc[i * seq // k : (i + 1) * seq // k].to(devs[i]) for i in range(k)]
267
+ dense[k] = safe(
268
+ lambda: [_sdpa(q, K_on[d], V_on[d]) for q, d in zip(qs, devs, strict=True)],
269
+ devs,
270
+ reps=args.reps,
271
+ )
272
+ qs = None
273
+ for kind in args.masks.split(","):
274
+ bm = block_mask(kind, nb)
275
+ mb = mask_bsr_from_block_mask(bm)
276
+ counts = bm.sum(1).tolist()
277
+ row = {
278
+ "section": "attn",
279
+ "seq": seq,
280
+ "head_dim": hd,
281
+ "mask": kind,
282
+ "block_density": float(bm.float().mean()),
283
+ "K_max": max(counts),
284
+ }
285
+ for k in core_counts:
286
+ row[f"sdpa_dense_{k}c_ms"] = dense[k]
287
+ # masked SDPA on one core and with rows split over kmax cores
288
+ em = bm.repeat_interleave(P, 0).repeat_interleave(P, 1)
289
+ for k in (1, kmax):
290
+ devs = pick(cores, k)
291
+ qs = [Qc[i * seq // k : (i + 1) * seq // k].to(devs[i]) for i in range(k)]
292
+ ems = [em[i * seq // k : (i + 1) * seq // k].to(devs[i]) for i in range(k)]
293
+ row[f"sdpa_masked_{k}c_ms"] = safe(
294
+ lambda: [
295
+ _sdpa(q, K_on[d], V_on[d], m) for q, m, d in zip(qs, ems, devs, strict=True)
296
+ ],
297
+ devs,
298
+ reps=args.reps,
299
+ )
300
+ qs = ems = None
301
+ free()
302
+ ref = None
303
+ if args.check:
304
+ ref = _sdpa(Qc.double(), Kc.double(), Vc.double(), em)
305
+ em = None
306
+ for k in core_counts:
307
+ devs = pick(cores, k)
308
+ SA = ShardedAttention(mb, devs)
309
+ Qs = SA.shard_rows(Qc)
310
+ KVs = SA.replicate_kv(Kc, Vc)
311
+ outs = SA.forward(Qs, KVs)
312
+ sync(SA.devices)
313
+ if ref is not None:
314
+ row[f"sparse_{k}c_rel_err"] = rel_err(SA.gather(outs), ref)
315
+ row[f"sparse_{k}c_fwd_ms"] = safe(
316
+ lambda: SA.forward(Qs, KVs), SA.devices, reps=args.reps
317
+ )
318
+ row[f"sparse_{k}c_issue_ms"] = issue_time(lambda: SA.forward(Qs, KVs), SA.devices)
319
+ if k == kmax:
320
+ row[f"sparse_{k}c_gather_ms"] = safe(
321
+ lambda: SA.gather(SA.forward(Qs, KVs)),
322
+ SA.devices,
323
+ reps=max(3, args.reps // 2),
324
+ )
325
+ if k == 1:
326
+ Q1, K1, V1 = (t.to(devs[0]) for t in (Qc, Kc, Vc))
327
+ row["api_1c_fwd_ms"] = safe(
328
+ lambda: trnsparse.block_sparse_attention_tiled(Q1, K1, V1, mb),
329
+ devs,
330
+ reps=args.reps,
331
+ )
332
+ Q1 = K1 = V1 = None
333
+ KVs = None
334
+ SA = Qs = outs = None
335
+ free()
336
+ k1 = min(core_counts)
337
+ for k in core_counts:
338
+ if row.get(f"sparse_{k}c_fwd_ms") and row.get(f"sparse_{k1}c_fwd_ms"):
339
+ row[f"sparse_scaling_{k}c"] = (
340
+ row[f"sparse_{k1}c_fwd_ms"] / row[f"sparse_{k}c_fwd_ms"]
341
+ )
342
+ emit(row, args.out)
343
+ K_on = V_on = None
344
+ free()
345
+
346
+
347
+ # ── Attention: heads across cores ────────────────────────────────────────────
348
+
349
+
350
+ def section_heads(args, cores):
351
+ seq = args.size
352
+ nb = seq // P
353
+ H = args.heads
354
+ core_counts = [int(c) for c in args.cores.split(",")]
355
+ for hd in [int(x) for x in args.head_dims.split(",")]:
356
+ g = torch.Generator().manual_seed(0)
357
+ Qc, Kc, Vc, dOc = (torch.randn(H, seq, hd, generator=g) for _ in range(4))
358
+
359
+ def place(k):
360
+ """Head h on core h % k: per core, lists of that core's heads."""
361
+ devs = pick(cores, k)
362
+ out = []
363
+ for i, d in enumerate(devs):
364
+ hs = list(range(i, H, k))
365
+ out.append((d, [tuple(t[h].to(d) for t in (Qc, Kc, Vc, dOc)) for h in hs]))
366
+ return devs, out
367
+
368
+ def place_dense(k):
369
+ devs = pick(cores, k)
370
+ out = []
371
+ for i, d in enumerate(devs):
372
+ hs = list(range(i, H, k))
373
+ out.append(tuple(t[hs][None].to(d) for t in (Qc, Kc, Vc, dOc)))
374
+ return devs, out
375
+
376
+ dense_fwd, dense_fb = {}, {}
377
+ for k in core_counts:
378
+ devs, pl = place_dense(k)
379
+ dense_fwd[k] = safe(
380
+ lambda: [F.scaled_dot_product_attention(q, kk, v) for q, kk, v, _ in pl],
381
+ devs,
382
+ reps=args.reps,
383
+ )
384
+
385
+ def dfb():
386
+ for q, kk, v, do in pl:
387
+ q, kk, v = (t.detach().requires_grad_() for t in (q, kk, v))
388
+ F.scaled_dot_product_attention(q, kk, v).backward(do)
389
+
390
+ dense_fb[k] = safe(dfb, devs, reps=args.reps)
391
+ pl = None
392
+ free()
393
+ for kind in args.masks.split(","):
394
+ bm = block_mask(kind, nb)
395
+ mb = mask_bsr_from_block_mask(bm)
396
+ row = {
397
+ "section": "heads",
398
+ "seq": seq,
399
+ "head_dim": hd,
400
+ "heads": H,
401
+ "mask": kind,
402
+ "block_density": float(bm.float().mean()),
403
+ }
404
+ for k in core_counts:
405
+ row[f"sdpa_dense_{k}c_fwd_ms"] = dense_fwd[k]
406
+ row[f"sdpa_dense_{k}c_fwd_bwd_ms"] = dense_fb[k]
407
+ for k in core_counts:
408
+ devs, pl = place(k)
409
+
410
+ def fwd():
411
+ return [
412
+ trnsparse.block_sparse_attention_tiled(q, kk, v, mb)
413
+ for _, hs in pl
414
+ for q, kk, v, _ in hs
415
+ ]
416
+
417
+ def fb():
418
+ for _, hs in pl:
419
+ for q, kk, v, do in hs:
420
+ q, kk, v = (t.detach().requires_grad_() for t in (q, kk, v))
421
+ trnsparse.block_sparse_attention_tiled(q, kk, v, mb).backward(do)
422
+
423
+ row[f"sparse_{k}c_fwd_ms"] = safe(fwd, devs, reps=args.reps)
424
+ if not args.no_bwd:
425
+ row[f"sparse_{k}c_fwd_bwd_ms"] = safe(fb, devs, reps=args.reps)
426
+ pl = None
427
+ free()
428
+ k1 = min(core_counts)
429
+ for k in core_counts:
430
+ for ph in ("fwd", "fwd_bwd"):
431
+ a, b = row.get(f"sparse_{k1}c_{ph}_ms"), row.get(f"sparse_{k}c_{ph}_ms")
432
+ if a and b:
433
+ row[f"sparse_scaling_{k}c_{ph}"] = a / b
434
+ emit(row, args.out)
435
+
436
+
437
+ def main():
438
+ ap = argparse.ArgumentParser()
439
+ ap.add_argument("--section", required=True, choices=["bsr", "attn", "heads"])
440
+ ap.add_argument("--size", type=int, default=8192)
441
+ ap.add_argument("--densities", default="0.01,0.05,0.1,0.25,0.5")
442
+ ap.add_argument("--widths", default="128,512")
443
+ ap.add_argument("--head-dims", default="64,128")
444
+ ap.add_argument("--masks", default="local,dilated,global")
445
+ ap.add_argument("--heads", type=int, default=4)
446
+ ap.add_argument("--cores", default="1,2,4")
447
+ ap.add_argument("--no-bwd", action="store_true")
448
+ ap.add_argument("--check", action="store_true", help="fp64 reference for attention")
449
+ ap.add_argument("--cpu", action="store_true", help="CPU dense baseline")
450
+ ap.add_argument(
451
+ "--pretransposed", action="store_true", help="also the pre-transposed BSR kernel"
452
+ )
453
+ ap.add_argument(
454
+ "--only-pretransposed", action="store_true", help="only the pre-transposed BSR kernel"
455
+ )
456
+ ap.add_argument("--reps", type=int, default=10)
457
+ ap.add_argument("--out", default="")
458
+ args = ap.parse_args()
459
+ trnsparse.set_backend("nki")
460
+ cores = logical_cores()
461
+ print(f"# {len(cores)} logical cores visible", file=sys.stderr)
462
+ {"bsr": section_bsr, "attn": section_attn, "heads": section_heads}[args.section](args, cores)
463
+
464
+
465
+ if __name__ == "__main__":
466
+ main()
@@ -0,0 +1,130 @@
1
+ """One process per logical NeuronCore: BSR SpMM and dense aten, Trn2.
2
+
3
+ The companion of `bench_multicore_trn2.py`, which issues every core's launch
4
+ from one Python thread. Here each of k processes owns one logical core
5
+ (`NEURON_RT_VISIBLE_CORES=<i>`), its share of the block rows (the same
6
+ `partition_block_rows` split as `ShardedBSR`) and its own copy of B, so the
7
+ host-side launch costs run in parallel. Each rep: all processes meet at a
8
+ barrier, launch, synchronize, and meet again; the coordinator times the
9
+ span between the two barriers (the barrier cost itself is measured with an
10
+ empty step and reported as ``barrier_ms``).
11
+
12
+ NKI_ENABLE_TRACE_CACHE=0 python benchmarks/bench_multiproc_trn2.py --size 8192 --width 512 --procs 4
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import multiprocessing as mp
20
+ import os
21
+ import statistics
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ ROOT = Path(__file__).resolve().parent.parent
27
+
28
+
29
+ def worker(i, k, core, args, bar, q):
30
+ os.environ["NEURON_RT_VISIBLE_CORES"] = str(core)
31
+ sys.path.insert(0, str(ROOT))
32
+ sys.path.insert(0, str(ROOT / "benchmarks"))
33
+ import torch
34
+ import torch_neuronx # noqa: F401
35
+ from bench_native_trn2 import random_bsr
36
+
37
+ import trnsparse
38
+ from trnsparse.multicore import ShardedBSR, partition_block_rows
39
+
40
+ trnsparse.set_backend("nki")
41
+ dev = torch.device("neuron", 0)
42
+ n, w = args.size, args.width
43
+ g = torch.Generator().manual_seed(1)
44
+ Bc = torch.randn(n, w, generator=g)
45
+ Ac = torch.randn(n, n, generator=g) / n**0.5
46
+ B = Bc.to(dev)
47
+ slab = Ac[i * n // k : (i + 1) * n // k].to(dev)
48
+ Ac = None
49
+
50
+ def sync():
51
+ torch.neuron.synchronize()
52
+
53
+ def steps():
54
+ yield "barrier", lambda: None
55
+ yield "dense", lambda: slab @ B
56
+ for dens in [float(x) for x in args.densities.split(",")]:
57
+ A = random_bsr(n, dens)
58
+ rows = partition_block_rows(A.block_row_ptrs.tolist(), k)[i]
59
+ # This process's rows only: a one-core ShardedBSR over the sub-matrix.
60
+ from trnsparse.multicore import _sub_bsr
61
+
62
+ sub = _sub_bsr(A, rows, "cpu")
63
+ S = ShardedBSR(sub, [dev], pretransposed=args.pretransposed)
64
+ yield f"sparse_{dens}", lambda S=S: S.spmm([B])
65
+
66
+ for name, fn in steps():
67
+ fn()
68
+ fn()
69
+ sync()
70
+ for _ in range(args.reps):
71
+ bar.wait()
72
+ fn()
73
+ sync()
74
+ bar.wait()
75
+ if i == 0:
76
+ q.put(("done", name))
77
+
78
+
79
+ def main():
80
+ ap = argparse.ArgumentParser()
81
+ ap.add_argument("--size", type=int, default=8192)
82
+ ap.add_argument("--width", type=int, default=512)
83
+ ap.add_argument("--densities", default="0.01,0.05,0.1,0.25,0.5")
84
+ ap.add_argument("--procs", type=int, default=4)
85
+ ap.add_argument("--cores", default="", help="comma list of core ids (default 0..procs-1)")
86
+ ap.add_argument("--pretransposed", action="store_true")
87
+ ap.add_argument("--reps", type=int, default=10)
88
+ ap.add_argument("--out", default="")
89
+ args = ap.parse_args()
90
+ k = args.procs
91
+ cores = [int(c) for c in args.cores.split(",")] if args.cores else list(range(k))
92
+ ctx = mp.get_context("spawn")
93
+ bar = ctx.Barrier(k + 1)
94
+ q = ctx.Queue()
95
+ ps = [ctx.Process(target=worker, args=(i, k, cores[i], args, bar, q)) for i in range(k)]
96
+ for p in ps:
97
+ p.start()
98
+ results = {}
99
+ names = ["barrier", "dense"] + [f"sparse_{d}" for d in args.densities.split(",")]
100
+ for name in names:
101
+ ts = []
102
+ for _ in range(args.reps):
103
+ bar.wait()
104
+ t0 = time.perf_counter()
105
+ bar.wait()
106
+ ts.append((time.perf_counter() - t0) * 1e3)
107
+ results[name] = statistics.median(ts)
108
+ q.get()
109
+ for p in ps:
110
+ p.join()
111
+ row = {
112
+ "section": "bsr_multiproc",
113
+ "n": args.size,
114
+ "width": args.width,
115
+ "procs": k,
116
+ "pretransposed": args.pretransposed,
117
+ "barrier_ms": results["barrier"],
118
+ "dense_ms": results["dense"],
119
+ }
120
+ for d in args.densities.split(","):
121
+ row[f"sparse_{float(d)}_ms"] = results[f"sparse_{d}"]
122
+ line = json.dumps(row)
123
+ print(line, flush=True)
124
+ if args.out:
125
+ with open(args.out, "a") as f:
126
+ f.write(line + "\n")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()