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.
Files changed (78) hide show
  1. {trntensor-0.18.0 → trntensor-0.19.0}/CHANGELOG.md +36 -0
  2. {trntensor-0.18.0 → trntensor-0.19.0}/PKG-INFO +1 -1
  3. {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/nki.md +22 -9
  4. trntensor-0.19.0/docs/api/quantum.md +152 -0
  5. {trntensor-0.18.0 → trntensor-0.19.0}/pyproject.toml +1 -1
  6. trntensor-0.19.0/scripts/bench_native_kernels.py +199 -0
  7. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_native_dispatch.py +219 -5
  8. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_compile.py +28 -0
  9. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_native.py +63 -1
  10. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/__init__.py +1 -1
  11. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/_kernels.py +224 -0
  12. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/_native.py +29 -0
  13. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/dispatch.py +368 -6
  14. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/quantum.py +64 -29
  15. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/PKG-INFO +1 -1
  16. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/SOURCES.txt +1 -0
  17. trntensor-0.18.0/docs/api/quantum.md +0 -109
  18. {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/ci.yml +0 -0
  19. {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/notify-umbrella.yml +0 -0
  20. {trntensor-0.18.0 → trntensor-0.19.0}/.github/workflows/publish.yml +0 -0
  21. {trntensor-0.18.0 → trntensor-0.19.0}/.gitignore +0 -0
  22. {trntensor-0.18.0 → trntensor-0.19.0}/.pre-commit-config.yaml +0 -0
  23. {trntensor-0.18.0 → trntensor-0.19.0}/CLAUDE.md +0 -0
  24. {trntensor-0.18.0 → trntensor-0.19.0}/CODE_OF_CONDUCT.md +0 -0
  25. {trntensor-0.18.0 → trntensor-0.19.0}/CONTRIBUTING.md +0 -0
  26. {trntensor-0.18.0 → trntensor-0.19.0}/LICENSE +0 -0
  27. {trntensor-0.18.0 → trntensor-0.19.0}/README.md +0 -0
  28. {trntensor-0.18.0 → trntensor-0.19.0}/benchmarks/__init__.py +0 -0
  29. {trntensor-0.18.0 → trntensor-0.19.0}/benchmarks/bench_einsum.py +0 -0
  30. {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/decompose.md +0 -0
  31. {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/einsum.md +0 -0
  32. {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/index.md +0 -0
  33. {trntensor-0.18.0 → trntensor-0.19.0}/docs/api/plan.md +0 -0
  34. {trntensor-0.18.0 → trntensor-0.19.0}/docs/architecture.md +0 -0
  35. {trntensor-0.18.0 → trntensor-0.19.0}/docs/aws_setup.md +0 -0
  36. {trntensor-0.18.0 → trntensor-0.19.0}/docs/benchmarks.md +0 -0
  37. {trntensor-0.18.0 → trntensor-0.19.0}/docs/contraction_patterns.md +0 -0
  38. {trntensor-0.18.0 → trntensor-0.19.0}/docs/design/matmul_dispatch_findings.md +0 -0
  39. {trntensor-0.18.0 → trntensor-0.19.0}/docs/design/matmul_kernel_profile.md +0 -0
  40. {trntensor-0.18.0 → trntensor-0.19.0}/docs/developing_kernels.md +0 -0
  41. {trntensor-0.18.0 → trntensor-0.19.0}/docs/index.md +0 -0
  42. {trntensor-0.18.0 → trntensor-0.19.0}/docs/installation.md +0 -0
  43. {trntensor-0.18.0 → trntensor-0.19.0}/docs/quickstart.md +0 -0
  44. {trntensor-0.18.0 → trntensor-0.19.0}/docs/tutorial_df_mp2.md +0 -0
  45. {trntensor-0.18.0 → trntensor-0.19.0}/examples/df_mp2_einsum.py +0 -0
  46. {trntensor-0.18.0 → trntensor-0.19.0}/infra/terraform/README.md +0 -0
  47. {trntensor-0.18.0 → trntensor-0.19.0}/infra/terraform/main.tf +0 -0
  48. {trntensor-0.18.0 → trntensor-0.19.0}/mkdocs.yml +0 -0
  49. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/autotune_dispatch.py +0 -0
  50. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/bench_native_einsum.py +0 -0
  51. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/bench_to_md.py +0 -0
  52. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_benchmarks.sh +0 -0
  53. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_neuron_profile.sh +0 -0
  54. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_neuron_tests.sh +0 -0
  55. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_reduce_parallel_multirank.py +0 -0
  56. {trntensor-0.18.0 → trntensor-0.19.0}/scripts/run_simulator_tests.sh +0 -0
  57. {trntensor-0.18.0 → trntensor-0.19.0}/setup.cfg +0 -0
  58. {trntensor-0.18.0 → trntensor-0.19.0}/tests/conftest.py +0 -0
  59. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_decompose.py +0 -0
  60. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_einsum.py +0 -0
  61. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki.py +0 -0
  62. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_kernels.py +0 -0
  63. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_nki_sim.py +0 -0
  64. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_parallel.py +0 -0
  65. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_plan.py +0 -0
  66. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_quantum.py +0 -0
  67. {trntensor-0.18.0 → trntensor-0.19.0}/tests/test_sr_runtime.py +0 -0
  68. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/decompose.py +0 -0
  69. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/einsum.py +0 -0
  70. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/__init__.py +0 -0
  71. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/nki/collectives.py +0 -0
  72. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/parallel.py +0 -0
  73. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/plan.py +0 -0
  74. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor/py.typed +0 -0
  75. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/dependency_links.txt +0 -0
  76. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/requires.txt +0 -0
  77. {trntensor-0.18.0 → trntensor-0.19.0}/trntensor.egg-info/top_level.txt +0 -0
  78. {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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trntensor
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Tensor contractions for AWS Trainium via NKI
5
5
  Author-email: Scott Friedman <scttfrdmn@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -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
- Under `"auto"` on the native path, the fused quantum kernels (`mp2_energy`,
150
- `ao_to_mo_transform`) also run as aten, because both measured slower than
151
- aten on the device:
152
-
153
- - AO→MO was 6–20× slower.
154
- - MP2 energy was 1.0–1.5× slower, reaching parity at best.
155
-
156
- `set_backend("nki")` still runs the NKI kernels. The kernels are unchanged,
157
- and the XLA path keeps them under `"auto"`.
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trntensor"
7
- version = "0.18.0"
7
+ version = "0.19.0"
8
8
  description = "Tensor contractions for AWS Trainium via NKI"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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()