trnsparse 0.9.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.
- {trnsparse-0.9.0 → trnsparse-0.10.0}/CHANGELOG.md +42 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/PKG-INFO +1 -1
- trnsparse-0.10.0/benchmarks/bench_multicore_trn2.py +466 -0
- trnsparse-0.10.0/benchmarks/bench_multiproc_trn2.py +130 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/api.md +31 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/benchmarks.md +159 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/pyproject.toml +1 -1
- trnsparse-0.10.0/tests/test_multicore.py +206 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_native_dispatch.py +57 -5
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_compile.py +43 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/__init__.py +1 -1
- trnsparse-0.10.0/trnsparse/multicore.py +315 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/nki/_native.py +82 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/nki/dispatch.py +41 -10
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/nki/kernels.py +187 -8
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse.egg-info/PKG-INFO +1 -1
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse.egg-info/SOURCES.txt +4 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/.github/workflows/ci.yml +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/.github/workflows/notify-umbrella.yml +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/.github/workflows/publish.yml +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/.gitignore +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/.pre-commit-config.yaml +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/CLAUDE.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/CODE_OF_CONDUCT.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/CONTRIBUTING.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/LICENSE +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/README.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_bsr_spmm.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_iterative.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_native_trn2.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_screening.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_spmm.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/bench_spmv.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/benchmarks/conftest.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/architecture.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/aws_setup.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/index.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/installation.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/iterative_solvers.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/migration_scipy.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/quickstart.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/docs/sparse_attention.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/examples/block_sparse_attention.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/examples/sparse_fock.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/infra/terraform/.terraform.lock.hcl +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/infra/terraform/README.md +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/infra/terraform/main.tf +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/mkdocs.yml +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/scripts/bench_to_md.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/scripts/run_benchmarks.sh +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/scripts/run_neuron_tests.sh +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/scripts/run_simulator_tests.sh +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/setup.cfg +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/conftest.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_attention.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_attn_gather.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_bsr.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_core_split.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_examples.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_formats.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_iterative.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_attn.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_bsr.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_native.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_screened_spmm.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_sim.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_nki_spmm.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_ops.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/tests/test_screening.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/formats.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/iterative.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/nki/__init__.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/nki/_native_attn.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/ops.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse/screening.py +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse.egg-info/dependency_links.txt +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse.egg-info/requires.txt +0 -0
- {trnsparse-0.9.0 → trnsparse-0.10.0}/trnsparse.egg-info/top_level.txt +0 -0
|
@@ -7,6 +7,48 @@ 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
|
+
|
|
10
52
|
## [0.9.0] — 2026-10-30
|
|
11
53
|
|
|
12
54
|
Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron; LNC=1 and LNC=2) — see trnsci/trnsci#35.
|
|
@@ -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()
|
|
@@ -50,3 +50,34 @@ lineages, detected on the first kernel launch (force with
|
|
|
50
50
|
on the host; dense operands and BSR `blocks` may be on `neuron`; results are
|
|
51
51
|
on the dense operand's device. See
|
|
52
52
|
[Installation](installation.md#placement-sparse-structure-on-the-host).
|
|
53
|
+
|
|
54
|
+
## Multi-core (`trnsparse.multicore`, native path)
|
|
55
|
+
|
|
56
|
+
A Trn2 chip at LNC=2 has four logical NeuronCores, which TorchNeuron
|
|
57
|
+
exposes as `neuron:0` … `neuron:3` (`NEURON_RT_VISIBLE_CORES=0-3`). Each
|
|
58
|
+
static kernel already splits its work across the two physical cores of one
|
|
59
|
+
logical core; these classes split it across logical cores.
|
|
60
|
+
|
|
61
|
+
- `logical_cores(n=None)`: the visible logical cores as devices.
|
|
62
|
+
- `partition_block_rows(row_ptrs, n_parts)`: block rows split into groups
|
|
63
|
+
balanced by stored blocks (the same greedy as the in-kernel split).
|
|
64
|
+
- `ShardedBSR(A, devices=None, pretransposed=True)`: A split by block rows,
|
|
65
|
+
with each core's blocks resident on that core.
|
|
66
|
+
- `.replicate(B)` copies B to every core.
|
|
67
|
+
- `.spmm(B_replicas)` returns one output per core, issuing one kernel
|
|
68
|
+
launch per core without waiting.
|
|
69
|
+
- `.gather(outs)` assembles C on the host.
|
|
70
|
+
- `pretransposed=True` runs the pre-transposed kernel (no in-kernel
|
|
71
|
+
transpose, fewer DMAs, and B tiles shared by groups of up to 8 block
|
|
72
|
+
rows). Gradients reach each core's B replica and block copy.
|
|
73
|
+
- `ShardedAttention(mask, devices=None)`: a single-head block-sparse
|
|
74
|
+
attention forward, split by query block rows. Use `.shard_rows(Q)`,
|
|
75
|
+
`.replicate_kv(K, V)`, `.forward(Qts, KVs)` and `.gather(outs)`. It is
|
|
76
|
+
forward only. For several heads, put whole heads on different cores and
|
|
77
|
+
call `block_sparse_attention_tiled` there; that path has autograd.
|
|
78
|
+
|
|
79
|
+
TorchNeuron does not copy tensors between logical cores. A `neuron:0`
|
|
80
|
+
tensor's `.to("neuron:1")` comes back still on core 0, and `copy_` across
|
|
81
|
+
cores raises. So per-core operands are built on the host and copied once,
|
|
82
|
+
and results return through the host. The kernel ops run on the logical core
|
|
83
|
+
of their operands, and operands on two different cores are an error.
|