trntensor 0.18.0__tar.gz → 0.19.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.
- {trntensor-0.18.0 → trntensor-0.19.0}/CHANGELOG.md +36 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/PKG-INFO +1 -1
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/nki.md +22 -9
- trntensor-0.19.0/docs/api/quantum.md +152 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/pyproject.toml +1 -1
- trntensor-0.19.0/scripts/bench_native_kernels.py +199 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_native_dispatch.py +219 -5
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_compile.py +28 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_native.py +63 -1
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/__init__.py +1 -1
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/_kernels.py +224 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/_native.py +29 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/dispatch.py +368 -6
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/quantum.py +64 -29
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/PKG-INFO +1 -1
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/SOURCES.txt +1 -0
- trntensor-0.18.0/docs/api/quantum.md +0 -109
- {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/ci.yml +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/notify-umbrella.yml +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/publish.yml +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/.gitignore +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/.pre-commit-config.yaml +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/CLAUDE.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/CODE_OF_CONDUCT.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/CONTRIBUTING.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/LICENSE +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/README.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/benchmarks/__init__.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/benchmarks/bench_einsum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/decompose.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/einsum.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/index.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/plan.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/architecture.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/aws_setup.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/benchmarks.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/contraction_patterns.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/design/matmul_dispatch_findings.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/design/matmul_kernel_profile.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/developing_kernels.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/index.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/installation.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/quickstart.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/docs/tutorial_df_mp2.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/examples/df_mp2_einsum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/infra/terraform/README.md +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/infra/terraform/main.tf +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/mkdocs.yml +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/autotune_dispatch.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/bench_native_einsum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/bench_to_md.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_benchmarks.sh +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_neuron_profile.sh +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_neuron_tests.sh +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_reduce_parallel_multirank.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_simulator_tests.sh +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/setup.cfg +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/conftest.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_decompose.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_einsum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_kernels.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_sim.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_parallel.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_plan.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_quantum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_sr_runtime.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/decompose.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/einsum.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/__init__.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/collectives.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/parallel.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/plan.py +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/py.typed +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/dependency_links.txt +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/requires.txt +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/top_level.txt +0 -0
- {trntensor-0.18.0 → trntensor-0.19.0}/uv.lock +0 -0
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.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: new native MP2-energy and AO→MO kernels beat aten by 1.25–1.9× and 1.3–1.6× in the `auto` region; native NKI matmul/bmm kernels deprecated (#52).
|
|
15
|
+
|
|
16
|
+
Resolves #51: on the native TorchNeuron path (Trn2/Trn3), the AO→MO and MP2-energy kernels are rewritten for Trn2 and now beat aten, and the NKI matmul/bmm kernels are retired in favour of aten.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`mp2_energy_row_kernel`, the native MP2-energy kernel (#51).**
|
|
21
|
+
- One launch per occupied row, using the pair symmetry `e_ij = e_ji`.
|
|
22
|
+
- Per pair, one GEMM fills `T` in SBUF; the `Tᵀ` blocks are Tensor Engine transposes. `T` never reaches device memory.
|
|
23
|
+
- Gridless launch: at LNC=2 each physical core takes half of the `j` window (`num_programs()` / `program_id`), and it also runs at LNC=1.
|
|
24
|
+
- The design follows trnblas `pair_energy_sym` (trnblas#55, #58).
|
|
25
|
+
- Native `"auto"` uses it for device-resident `B` once each row launch carries at least 3 GFLOP. On Trn2 it measured 1.25–1.9× faster than aten: for example 0.69 s against 1.30 s at `nocc=96, nvir=672, naux=2304`.
|
|
26
|
+
- Near a full device the kernel's input staging (about 1.5× the padded `B`) needs more memory than aten's 64 MiB chunks. `"auto"` keeps aten when live allocations plus that staging would leave less than 3 GiB of the core free, and remembers shapes whose launch failed.
|
|
27
|
+
- `TRNTENSOR_MP2_ENERGY=auto|rows|aten|legacy` overrides the choice.
|
|
28
|
+
- **`ao_to_mo_trn2_kernel`, the native AO→MO kernel (#51).**
|
|
29
|
+
- Two plain GEMM stages with the contraction index on the partition axis, so there are no DMA transposes.
|
|
30
|
+
- The auxiliary index is split across both physical cores at LNC=2.
|
|
31
|
+
- Native `"auto"` uses it for device-resident `eri` with `nocc ≤ 128` and stage-1 work of at least 5e8 multiply-adds (about nbasis ≥ 256). There it measured 1.3–1.6× faster than the two aten einsums on Trn2, at nbasis 256–768. Smaller problems stay on aten.
|
|
32
|
+
- `TRNTENSOR_AO_TO_MO=auto|trn2|aten|legacy` overrides the choice.
|
|
33
|
+
- `scripts/bench_native_kernels.py`: a kernel-level A/B harness (aten against the original and the Trn2 kernels), with `--hold-gb` for the memory-pressure regime of aws-neuron-sdk#1447.
|
|
34
|
+
|
|
35
|
+
### Deprecated
|
|
36
|
+
|
|
37
|
+
- **On the native path, the NKI `matmul` / `batched_matmul` kernels and the original `mp2_energy` / `ao_to_mo_transform` kernels (#51).** `set_backend("nki")` still runs the matmul/bmm kernels there, with a one-time `FutureWarning`; a later release will route them to aten.
|
|
38
|
+
- A Trn2 rewrite of matmul used both cores, Tensor Engine transposes and B-tile reuse. It was 1.2–4.3× faster than the original kernel, but still slower than aten at every size (4096³ fp32: 4.10 ms against 3.80 ms; bf16: 1.61 ms against 1.43 ms). It was not kept.
|
|
39
|
+
- Native `"auto"` already used aten for these.
|
|
40
|
+
- The XLA path (Trn1) keeps all four original kernels unchanged and does not warn.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Under `set_backend("nki")` on the native path, `mp2_energy` and `ao_to_mo_transform` run the Trn2 kernels instead of the original ones.
|
|
45
|
+
|
|
10
46
|
## [0.18.0] — 2026-10-30
|
|
11
47
|
|
|
12
48
|
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.
|
|
@@ -118,6 +118,8 @@ supported in maintenance mode."
|
|
|
118
118
|
| `TRNSCI_NKI_DISPATCH=auto\|native\|xla` | Suite-wide. Force the native or XLA dispatch path; `auto` (default) detects. Forcing an unavailable path raises. |
|
|
119
119
|
| `TRNSCI_XLA_DEPRECATION=1` | Suite-wide, off by default. Emit the one-time XLA-path and `to_xla` / `from_xla` deprecation warnings. |
|
|
120
120
|
| `NKI_ENABLE_TRACE_CACHE=0` | TorchNeuron setting (not trntensor's). Turns off the cross-process NKI kernel cache, which cannot version editable installs and may serve a stale kernel after a source edit. Set it with `pip install -e`; the test suite sets it by default. |
|
|
121
|
+
| `TRNTENSOR_MP2_ENERGY=auto\|rows\|aten\|legacy` | Native path: force the `mp2_energy` executor (`rows` = the Trn2 row kernel, `legacy` = the original kernel, deprecated there). |
|
|
122
|
+
| `TRNTENSOR_AO_TO_MO=auto\|trn2\|aten\|legacy` | Native path: force the `ao_to_mo_transform` executor (`trn2` = the Trn2 kernel, `legacy` = the original kernel, deprecated there). |
|
|
121
123
|
| `TRNTENSOR_FORCE_BACKEND=pytorch\|nki\|auto` | Override the backend selection at runtime without calling `set_backend()`. Used by benchmarks to sweep both paths on the same machine. |
|
|
122
124
|
| `TRNTENSOR_USE_SIMULATOR=1` | Route kernel dispatch through `nki.simulate(kernel)(numpy_args)` on CPU instead of XLA → NEFF → hardware. Catches Python-trace-level errors without AWS round-trips. MLIR verifier errors remain hardware-only. |
|
|
123
125
|
|
|
@@ -146,15 +148,26 @@ NeuronCore and two host threads, with `scripts/bench_native_einsum.py` and
|
|
|
146
148
|
The default of 500 M sits between these. A host with more threads moves the
|
|
147
149
|
crossover up. Re-measure on your host with `scripts/autotune_dispatch.py`.
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
aten
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`set_backend("nki")` still runs the
|
|
157
|
-
|
|
151
|
+
Kernel status on the native path after #51:
|
|
152
|
+
|
|
153
|
+
- **matmul / bmm: retired in favour of aten.** A Trn2 rewrite used both
|
|
154
|
+
physical cores through the `program_id` split, Tensor Engine transposes of
|
|
155
|
+
A and B-tile reuse across four stationaries. It was 1.2–4.3× faster than
|
|
156
|
+
the original kernel and still slower than aten at every size: 4096³ fp32
|
|
157
|
+
took 4.10 ms against 3.80 ms, and 4096³ bf16 1.61 ms against 1.43 ms.
|
|
158
|
+
It was not kept. `set_backend("nki")` still runs the original kernels on
|
|
159
|
+
the native path, with a one-time `FutureWarning`. A later release will
|
|
160
|
+
route them to aten.
|
|
161
|
+
- **AO→MO: kept, rewritten for Trn2** (`ao_to_mo_trn2_kernel`). It is
|
|
162
|
+
1.3–1.6× faster than the two aten einsums at nbasis 256–768, and native
|
|
163
|
+
`"auto"` uses it there for device-resident `eri`. Below nbasis ≈ 256
|
|
164
|
+
`"auto"` keeps aten.
|
|
165
|
+
- **MP2 energy: kept, rewritten for Trn2** (`mp2_energy_row_kernel`). It is
|
|
166
|
+
1.25–1.9× faster than aten once each row launch carries at least 3 GFLOP,
|
|
167
|
+
and native `"auto"` uses it there.
|
|
168
|
+
|
|
169
|
+
The original AO→MO and MP2 kernels remain the XLA-path (Trn1) kernels. See
|
|
170
|
+
[Quantum-chemistry primitives](quantum.md) for the tables.
|
|
158
171
|
|
|
159
172
|
## Kernels (internal)
|
|
160
173
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Quantum-chemistry primitives
|
|
2
|
+
|
|
3
|
+
Domain-specific fused kernels where the whole computation — contraction, elementwise shaping, reduction — lives in a single NKI program. The architectural core of trntensor (see [Architecture](../architecture.md)).
|
|
4
|
+
|
|
5
|
+
## `trntensor.mp2_energy(B, eps_occ, eps_vir) -> scalar`
|
|
6
|
+
|
|
7
|
+
Density-fitted second-order Møller–Plesset correlation energy.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
E_MP2 = Σ_{i,j,a,b} T_{i,j,a,b} (2 T_{i,j,a,b} - T_{i,j,b,a}) / Δ_{i,j,a,b}
|
|
11
|
+
|
|
12
|
+
T_{i,j,a,b} = Σ_P B[i, a, P] B[j, b, P]
|
|
13
|
+
Δ_{i,j,a,b} = ε_i + ε_j - ε_a - ε_b
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Arguments
|
|
17
|
+
|
|
18
|
+
- `B: (nocc, nvir, naux) tensor` — density-fitted ERI coefficients
|
|
19
|
+
- `eps_occ: (nocc,) tensor` — occupied orbital energies
|
|
20
|
+
- `eps_vir: (nvir,) tensor` — virtual orbital energies
|
|
21
|
+
|
|
22
|
+
### Returns
|
|
23
|
+
|
|
24
|
+
A 0-D tensor containing the correlation energy.
|
|
25
|
+
|
|
26
|
+
### Example
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import torch
|
|
30
|
+
import trntensor
|
|
31
|
+
|
|
32
|
+
nocc, nvir, naux = 5, 19, 72
|
|
33
|
+
B = torch.randn(nocc, nvir, naux) * 0.1
|
|
34
|
+
eps_occ = -torch.sort(torch.rand(nocc))[0] - 0.5
|
|
35
|
+
eps_vir = torch.sort(torch.rand(nvir))[0] + 0.1
|
|
36
|
+
|
|
37
|
+
E = trntensor.mp2_energy(B, eps_occ, eps_vir)
|
|
38
|
+
print(f"E_MP2 = {E.item():.6f}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Backend behaviour
|
|
42
|
+
|
|
43
|
+
- **CPU, and small problems on native TorchNeuron**: aten ops on the
|
|
44
|
+
operands' device. `T[i, j, a, b]` is contracted with `torch.einsum` in
|
|
45
|
+
chunks over `i`, which bounds peak memory to about 64 MiB of `T` at a time.
|
|
46
|
+
The `T`-weighted sum is vectorized.
|
|
47
|
+
- **Native TorchNeuron (Trn2/Trn3): the row kernel.** `mp2_energy_row_kernel`
|
|
48
|
+
is launched once per occupied row `i`. It covers `j = i + d (mod nocc)` for
|
|
49
|
+
`d = 0..nocc/2` and uses the pair symmetry `e_ij = e_ji`. Per pair, one
|
|
50
|
+
GEMM fills `T = B[i] B[j]ᵀ` in SBUF, and the `Tᵀ` blocks come from Tensor
|
|
51
|
+
Engine transposes. `T` never reaches device memory. At LNC=2 each physical
|
|
52
|
+
core takes half of the `j` window. The design follows trnblas
|
|
53
|
+
`pair_energy_sym` (trnblas#55, #58).
|
|
54
|
+
- `"auto"` uses the row kernel when `B` is on the device and each row
|
|
55
|
+
launch carries at least 3 GFLOP, counted on the padded sizes as
|
|
56
|
+
`2·(nocc//2+1)·nvir²·naux`. Below that, the ~nocc custom-op launch
|
|
57
|
+
floors lose to aten.
|
|
58
|
+
- `set_backend("nki")` uses it whenever its SBUF working set fits.
|
|
59
|
+
- `TRNTENSOR_MP2_ENERGY=auto|rows|aten|legacy` overrides the choice.
|
|
60
|
+
`legacy` is the original single-launch kernel, which is deprecated on
|
|
61
|
+
the native path.
|
|
62
|
+
|
|
63
|
+
Measured on Trn2 (fp32, one LNC=2 core, device-resident, warm, #51):
|
|
64
|
+
|
|
65
|
+
| nocc, nvir, naux | aten | row kernel |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| 16, 128, 512 | 1.5 ms | 5.1 ms |
|
|
68
|
+
| 32, 256, 768 | 12.1 ms | 11.6 ms |
|
|
69
|
+
| 32, 320, 1024 | 24.9 ms | 19.8 ms |
|
|
70
|
+
| 48, 384, 1152 | 75.7 ms | 39.2 ms |
|
|
71
|
+
| 64, 448, 1536 | 240 ms | 120 ms |
|
|
72
|
+
| 96, 672, 2304 | 1.30 s | 0.69 s |
|
|
73
|
+
|
|
74
|
+
Holding 12 or 18 GB of the core's 24 GB on the device did not slow either
|
|
75
|
+
path. The aten path's 64 MiB `T` chunks stay clear of the slowdown in
|
|
76
|
+
aws-neuron-sdk#1447. The row kernel's input staging is about 1.5× the
|
|
77
|
+
padded `B`, so near a full device it needs more memory than aten does.
|
|
78
|
+
`"auto"` therefore keeps aten when this process's live device allocations
|
|
79
|
+
plus that staging would leave less than 3 GiB free. A shape whose kernel
|
|
80
|
+
launch fails anyway falls back to aten and is remembered for the rest of
|
|
81
|
+
the process.
|
|
82
|
+
- **PyTorch/XLA path (Trn1)**: the original single-launch `mp2_energy_kernel`.
|
|
83
|
+
It accumulates `T` in PSUM, builds `Δ` on the Vector Engine, and writes one
|
|
84
|
+
partial per `(i, j)` pair.
|
|
85
|
+
|
|
86
|
+
## `trntensor.ao_to_mo_transform(eri, C_occ, C_vir) -> Tensor`
|
|
87
|
+
|
|
88
|
+
Fused 4-index AO→MO integral transform. The canonical quantum-chemistry primitive.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
B[i, a, P] = Σ_{μ, ν} C_occ[μ, i] · C_vir[ν, a] · eri[μ, ν, P]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Arguments
|
|
95
|
+
|
|
96
|
+
- `eri: (nbasis, nbasis, naux) tensor` — density-fitted two-electron integrals in the AO basis
|
|
97
|
+
- `C_occ: (nbasis, nocc) tensor` — occupied molecular orbital coefficients
|
|
98
|
+
- `C_vir: (nbasis, nvir) tensor` — virtual molecular orbital coefficients
|
|
99
|
+
|
|
100
|
+
### Returns
|
|
101
|
+
|
|
102
|
+
A `(nocc, nvir, naux)` tensor of transformed DF coefficients B_ia^P.
|
|
103
|
+
|
|
104
|
+
### Example — full DF-MP2 pipeline
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import trntensor
|
|
108
|
+
|
|
109
|
+
B = trntensor.ao_to_mo_transform(eri, C_occ, C_vir)
|
|
110
|
+
E = trntensor.mp2_energy(B, eps_occ, eps_vir)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Backend behaviour
|
|
114
|
+
|
|
115
|
+
- **CPU**: two explicit aten steps on the operands' device:
|
|
116
|
+
`einsum("mi,mnP->inP")` then `einsum("na,inP->iaP")`.
|
|
117
|
+
- **Native TorchNeuron (Trn2/Trn3): `ao_to_mo_trn2_kernel`.** Both stages are
|
|
118
|
+
plain GEMMs with the contraction index on the partition axis, so there are
|
|
119
|
+
no DMA transposes.
|
|
120
|
+
- Stage 1: `X[i, ν, P] = Σ_μ C_occ[μ, i] eri[μ, ν, P]`.
|
|
121
|
+
- Stage 2: `B[i, a, P] = Σ_ν C_vir[ν, a] X[i, ν, P]`.
|
|
122
|
+
- The auxiliary index is split across both physical cores at LNC=2.
|
|
123
|
+
- `"auto"` uses the kernel for device-resident `eri` with `nocc ≤ 128` when
|
|
124
|
+
its SBUF working set fits and the stage-1 work `nocc·nbasis²·naux` is at
|
|
125
|
+
least 5e8 multiply-adds (about nbasis ≥ 256). Smaller problems lose to
|
|
126
|
+
the launch floor and the padding copies; otherwise it runs the two aten
|
|
127
|
+
steps.
|
|
128
|
+
`TRNTENSOR_AO_TO_MO=auto|trn2|aten|legacy` overrides the choice.
|
|
129
|
+
|
|
130
|
+
Measured on Trn2 (fp32, one LNC=2 core, device-resident, warm, #51):
|
|
131
|
+
|
|
132
|
+
| nbasis, nocc, naux | two aten einsums | Trn2 kernel (op) | `ao_to_mo_transform`, `"auto"` | original kernel |
|
|
133
|
+
|---|---|---|---|---|
|
|
134
|
+
| 64, 8, 128 | 0.41 ms | — | 0.41 ms (aten) | 6.0 ms (#49) |
|
|
135
|
+
| 128, 16, 256 | 0.49 ms | 0.43 ms | 0.49 ms (aten) | 16.1 ms |
|
|
136
|
+
| 256, 32, 512 | 1.23 ms | 1.11 ms | 0.92 ms | 112 ms (#49) |
|
|
137
|
+
| 512, 32, 1024 | 7.30 ms | 4.56 ms | 4.72 ms | — |
|
|
138
|
+
| 768, 48, 1536 | 18.3 ms | 11.8 ms | 12.0 ms | — |
|
|
139
|
+
|
|
140
|
+
The op column takes tile-multiple inputs (the raw kernel). The `"auto"`
|
|
141
|
+
column includes the padding of `nvir` to 128 and is the shipped route. At
|
|
142
|
+
nbasis 64 and 128 it measured 0.79 and 0.57 ms with the kernel forced,
|
|
143
|
+
which is why `"auto"` keeps aten there.
|
|
144
|
+
|
|
145
|
+
- **PyTorch/XLA path (Trn1)**: the original kernel. It loops over P, with the
|
|
146
|
+
`(i, ν)` intermediate round-tripping through kernel-scratch HBM. On the
|
|
147
|
+
native path it is deprecated.
|
|
148
|
+
|
|
149
|
+
### Current limitations
|
|
150
|
+
|
|
151
|
+
- Both NKI kernels need `nocc ≤ 128`; larger `nocc` runs the aten steps (native) or raises `NotImplementedError` (XLA).
|
|
152
|
+
- The `(nocc, nbasis, naux)` intermediate goes through kernel-scratch HBM between the two stages, as it does between the two aten einsums. Stage 1 leaves `i` on the partition axis, and stage 2 needs `ν` there.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Kernel-level A/B on TorchNeuron: trntensor NKI kernels vs aten (#51).
|
|
2
|
+
|
|
3
|
+
Times, on device-resident ``neuron`` tensors created on the host and copied
|
|
4
|
+
over (aws-neuron-sdk#1439), the warm median latency of:
|
|
5
|
+
|
|
6
|
+
aten the aten formulation trntensor's native ``auto`` uses
|
|
7
|
+
nki_trn1 the original kernel (the XLA-path kernel, as on main)
|
|
8
|
+
tt_auto the public trntensor entry point under set_backend("auto")
|
|
9
|
+
nki_trn2 the native Trn2 kernel (program_id split, no DMA transposes);
|
|
10
|
+
AO→MO and MP2 only. The Trn2 matmul candidate measured in #51
|
|
11
|
+
lost to aten at every size and was not kept.
|
|
12
|
+
|
|
13
|
+
Families: matmul, ao2mo, mp2. ``--hold-gb G`` keeps G GB allocated on the
|
|
14
|
+
device during the run (the memory-pressure regime of aws-neuron-sdk#1447).
|
|
15
|
+
|
|
16
|
+
python scripts/bench_native_kernels.py --family mp2 --hold-gb 12
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import statistics
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
os.environ.setdefault("NKI_ENABLE_TRACE_CACHE", "0")
|
|
28
|
+
|
|
29
|
+
import torch # noqa: E402
|
|
30
|
+
|
|
31
|
+
from trntensor import quantum # noqa: E402
|
|
32
|
+
from trntensor.nki import dispatch # noqa: E402
|
|
33
|
+
|
|
34
|
+
DEV = "neuron"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _sync():
|
|
38
|
+
torch.neuron.synchronize()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _time(fn, warm, reps, budget_s=20.0):
|
|
42
|
+
for _ in range(warm):
|
|
43
|
+
fn()
|
|
44
|
+
_sync()
|
|
45
|
+
t0 = time.perf_counter()
|
|
46
|
+
fn()
|
|
47
|
+
_sync()
|
|
48
|
+
one = time.perf_counter() - t0
|
|
49
|
+
if one > 0.05:
|
|
50
|
+
reps = max(3, min(reps, int(budget_s / one)))
|
|
51
|
+
ts = []
|
|
52
|
+
for _ in range(reps):
|
|
53
|
+
t0 = time.perf_counter()
|
|
54
|
+
fn()
|
|
55
|
+
_sync()
|
|
56
|
+
ts.append(time.perf_counter() - t0)
|
|
57
|
+
return statistics.median(ts) * 1e3
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _relerr(x, ref):
|
|
61
|
+
x = x.detach().cpu().double()
|
|
62
|
+
ref = ref.double()
|
|
63
|
+
return float((x - ref).norm() / ref.norm().clamp_min(1e-300))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ops():
|
|
67
|
+
return dispatch._get_native_ops()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cases_matmul(dtype, sizes):
|
|
71
|
+
sizes = sizes or [(n, n, n) for n in (512, 1024, 2048, 4096)] + [(512, 2048, 512)]
|
|
72
|
+
for M, K, N in sizes:
|
|
73
|
+
|
|
74
|
+
def make(M=M, K=K, N=N):
|
|
75
|
+
g = torch.Generator().manual_seed(0)
|
|
76
|
+
a = torch.randn(M, K, generator=g).to(dtype)
|
|
77
|
+
b = torch.randn(K, N, generator=g).to(dtype)
|
|
78
|
+
return (a, b), (a.double() @ b.double())
|
|
79
|
+
|
|
80
|
+
yield (
|
|
81
|
+
f"matmul {M}x{K}x{N} {str(dtype)[6:]}",
|
|
82
|
+
make,
|
|
83
|
+
{
|
|
84
|
+
"aten": lambda a, b: torch.matmul(a, b),
|
|
85
|
+
"nki_trn1": lambda a, b: _ops()["matmul"](a, b),
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cases_ao2mo(dtype, sizes):
|
|
91
|
+
# (nbasis, nocc, naux), nvir = nbasis - nocc; nbasis/nvir/naux tile-padded below.
|
|
92
|
+
sizes = sizes or [(128, 16, 256), (256, 32, 512), (512, 32, 1024), (768, 48, 1536)]
|
|
93
|
+
for nb, no, nx in sizes:
|
|
94
|
+
nv = nb - no
|
|
95
|
+
|
|
96
|
+
def make(nb=nb, no=no, nv=nv, nx=nx):
|
|
97
|
+
g = torch.Generator().manual_seed(0)
|
|
98
|
+
co = torch.randn(nb, no, generator=g).to(dtype)
|
|
99
|
+
cv = torch.randn(nb, nv, generator=g).to(dtype)
|
|
100
|
+
eri = torch.randn(nb, nb, nx, generator=g).to(dtype)
|
|
101
|
+
ref = torch.einsum(
|
|
102
|
+
"na,inP->iaP", cv.double(), torch.einsum("mi,mnP->inP", co.double(), eri.double())
|
|
103
|
+
)
|
|
104
|
+
return (eri, co, cv), ref
|
|
105
|
+
|
|
106
|
+
modes = {
|
|
107
|
+
"aten": lambda eri, co, cv: quantum._cpu_ao_to_mo_transform(eri, co, cv),
|
|
108
|
+
"nki_trn2": lambda eri, co, cv: dispatch._nki_ao_to_mo_trn2(eri, co, cv),
|
|
109
|
+
"tt_auto": lambda eri, co, cv: quantum.ao_to_mo_transform(eri, co, cv),
|
|
110
|
+
}
|
|
111
|
+
if nb <= 128: # the original kernel compiles for many minutes beyond this
|
|
112
|
+
modes["nki_trn1"] = lambda eri, co, cv: dispatch._nki_ao_to_mo_transform(eri, co, cv)
|
|
113
|
+
yield (f"ao2mo nb={nb} no={no} nv={nv} nx={nx}", make, modes)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cases_mp2(dtype, sizes):
|
|
117
|
+
sizes = sizes or [
|
|
118
|
+
(16, 128, 512),
|
|
119
|
+
(32, 128, 512),
|
|
120
|
+
(32, 256, 768),
|
|
121
|
+
(48, 384, 1152),
|
|
122
|
+
(64, 448, 1536),
|
|
123
|
+
(96, 672, 2304),
|
|
124
|
+
]
|
|
125
|
+
for no, nv, nx in sizes:
|
|
126
|
+
|
|
127
|
+
def make(no=no, nv=nv, nx=nx):
|
|
128
|
+
g = torch.Generator().manual_seed(0)
|
|
129
|
+
B = (torch.randn(no, nv, nx, generator=g) * 0.05).to(dtype)
|
|
130
|
+
eo = -torch.rand(no, generator=g) - 1.0
|
|
131
|
+
ev = torch.rand(nv, generator=g) + 0.5
|
|
132
|
+
ref = quantum._cpu_mp2_energy(B.double(), eo.double(), ev.double())
|
|
133
|
+
return (B, eo, ev), ref
|
|
134
|
+
|
|
135
|
+
modes = {
|
|
136
|
+
"aten": lambda B, eo, ev: quantum._cpu_mp2_energy(B, eo, ev),
|
|
137
|
+
"nki_trn2": lambda B, eo, ev: dispatch._nki_mp2_energy_rows(B, eo, ev),
|
|
138
|
+
"tt_auto": lambda B, eo, ev: quantum.mp2_energy(B, eo, ev),
|
|
139
|
+
}
|
|
140
|
+
if no <= 32:
|
|
141
|
+
modes["nki_trn1"] = lambda B, eo, ev: dispatch._nki_mp2_energy(B, eo, ev)
|
|
142
|
+
yield (f"mp2 no={no} nv={nv} nx={nx}", make, modes)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
FAMILIES = {"matmul": cases_matmul, "ao2mo": cases_ao2mo, "mp2": cases_mp2}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
ap = argparse.ArgumentParser()
|
|
150
|
+
ap.add_argument("--family", required=True, choices=list(FAMILIES))
|
|
151
|
+
ap.add_argument("--dtype", default="float32")
|
|
152
|
+
ap.add_argument("--modes", default="aten,nki_trn1,nki_trn2")
|
|
153
|
+
ap.add_argument("--sizes", default="", help="semicolon list of comma tuples")
|
|
154
|
+
ap.add_argument("--hold-gb", type=float, default=0.0)
|
|
155
|
+
ap.add_argument("--warm", type=int, default=2)
|
|
156
|
+
ap.add_argument("--reps", type=int, default=10)
|
|
157
|
+
ap.add_argument("--json", default="")
|
|
158
|
+
args = ap.parse_args()
|
|
159
|
+
dtype = getattr(torch, args.dtype)
|
|
160
|
+
sizes = [tuple(int(x) for x in s.split(",")) for s in args.sizes.split(";") if s]
|
|
161
|
+
want = args.modes.split(",")
|
|
162
|
+
|
|
163
|
+
held = []
|
|
164
|
+
if args.hold_gb > 0:
|
|
165
|
+
# Hold the working set in 1 GiB host-made blocks (device randn is
|
|
166
|
+
# shape-sensitive, #1439; zeros are enough to occupy the memory).
|
|
167
|
+
for _ in range(int(args.hold_gb)):
|
|
168
|
+
held.append(torch.ones(256 * 1024 * 1024, dtype=torch.float32).to(DEV))
|
|
169
|
+
_sync()
|
|
170
|
+
print(f"# family={args.family} dtype={args.dtype} hold_gb={args.hold_gb}", flush=True)
|
|
171
|
+
for label, make, modes in FAMILIES[args.family](dtype, sizes):
|
|
172
|
+
host, ref = make()
|
|
173
|
+
dev = tuple(t.to(DEV) for t in host)
|
|
174
|
+
_sync()
|
|
175
|
+
row = {"case": label, "hold_gb": args.hold_gb}
|
|
176
|
+
for m in want:
|
|
177
|
+
if m not in modes:
|
|
178
|
+
continue
|
|
179
|
+
fn = lambda f=modes[m], o=dev: f(*o) # noqa: E731
|
|
180
|
+
try:
|
|
181
|
+
t0 = time.perf_counter()
|
|
182
|
+
out = fn()
|
|
183
|
+
_sync()
|
|
184
|
+
row[f"{m}_first_s"] = round(time.perf_counter() - t0, 2)
|
|
185
|
+
row[f"{m}_relerr"] = _relerr(out, ref)
|
|
186
|
+
del out
|
|
187
|
+
row[m] = _time(fn, args.warm, args.reps)
|
|
188
|
+
except Exception as exc: # noqa: BLE001
|
|
189
|
+
row[m] = None
|
|
190
|
+
row[f"{m}_err"] = repr(exc)[:400]
|
|
191
|
+
del dev
|
|
192
|
+
print(json.dumps(row), flush=True)
|
|
193
|
+
if args.json:
|
|
194
|
+
with open(args.json, "a") as f:
|
|
195
|
+
f.write(json.dumps(row) + "\n")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|