trntensor 0.17.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 (81) hide show
  1. {trntensor-0.17.0 → trntensor-0.19.0}/.github/workflows/ci.yml +6 -4
  2. {trntensor-0.17.0 → trntensor-0.19.0}/CHANGELOG.md +127 -0
  3. {trntensor-0.17.0 → trntensor-0.19.0}/CLAUDE.md +4 -1
  4. {trntensor-0.17.0/trntensor.egg-info → trntensor-0.19.0}/PKG-INFO +35 -3
  5. trntensor-0.17.0/PKG-INFO → trntensor-0.19.0/README.md +26 -32
  6. trntensor-0.19.0/docs/api/nki.md +187 -0
  7. trntensor-0.19.0/docs/api/quantum.md +152 -0
  8. {trntensor-0.17.0 → trntensor-0.19.0}/docs/contraction_patterns.md +12 -10
  9. {trntensor-0.17.0 → trntensor-0.19.0}/pyproject.toml +25 -7
  10. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/autotune_dispatch.py +162 -12
  11. trntensor-0.19.0/scripts/bench_native_einsum.py +396 -0
  12. trntensor-0.19.0/scripts/bench_native_kernels.py +199 -0
  13. {trntensor-0.17.0 → trntensor-0.19.0}/tests/conftest.py +6 -0
  14. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_einsum.py +2 -2
  15. trntensor-0.19.0/tests/test_native_dispatch.py +946 -0
  16. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_nki_compile.py +28 -0
  17. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_nki_kernels.py +74 -45
  18. trntensor-0.19.0/tests/test_nki_native.py +238 -0
  19. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_plan.py +17 -1
  20. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_quantum.py +24 -0
  21. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_sr_runtime.py +3 -0
  22. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/__init__.py +4 -2
  23. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/einsum.py +26 -14
  24. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/nki/_kernels.py +224 -0
  25. trntensor-0.19.0/trntensor/nki/_native.py +114 -0
  26. trntensor-0.19.0/trntensor/nki/dispatch.py +1273 -0
  27. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/plan.py +17 -6
  28. trntensor-0.19.0/trntensor/quantum.py +236 -0
  29. trntensor-0.17.0/README.md → trntensor-0.19.0/trntensor.egg-info/PKG-INFO +64 -1
  30. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor.egg-info/SOURCES.txt +5 -0
  31. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor.egg-info/requires.txt +10 -1
  32. {trntensor-0.17.0 → trntensor-0.19.0}/uv.lock +1 -1
  33. trntensor-0.17.0/docs/api/nki.md +0 -78
  34. trntensor-0.17.0/docs/api/quantum.md +0 -99
  35. trntensor-0.17.0/trntensor/nki/dispatch.py +0 -547
  36. trntensor-0.17.0/trntensor/quantum.py +0 -173
  37. {trntensor-0.17.0 → trntensor-0.19.0}/.github/workflows/notify-umbrella.yml +0 -0
  38. {trntensor-0.17.0 → trntensor-0.19.0}/.github/workflows/publish.yml +0 -0
  39. {trntensor-0.17.0 → trntensor-0.19.0}/.gitignore +0 -0
  40. {trntensor-0.17.0 → trntensor-0.19.0}/.pre-commit-config.yaml +0 -0
  41. {trntensor-0.17.0 → trntensor-0.19.0}/CODE_OF_CONDUCT.md +0 -0
  42. {trntensor-0.17.0 → trntensor-0.19.0}/CONTRIBUTING.md +0 -0
  43. {trntensor-0.17.0 → trntensor-0.19.0}/LICENSE +0 -0
  44. {trntensor-0.17.0 → trntensor-0.19.0}/benchmarks/__init__.py +0 -0
  45. {trntensor-0.17.0 → trntensor-0.19.0}/benchmarks/bench_einsum.py +0 -0
  46. {trntensor-0.17.0 → trntensor-0.19.0}/docs/api/decompose.md +0 -0
  47. {trntensor-0.17.0 → trntensor-0.19.0}/docs/api/einsum.md +0 -0
  48. {trntensor-0.17.0 → trntensor-0.19.0}/docs/api/index.md +0 -0
  49. {trntensor-0.17.0 → trntensor-0.19.0}/docs/api/plan.md +0 -0
  50. {trntensor-0.17.0 → trntensor-0.19.0}/docs/architecture.md +0 -0
  51. {trntensor-0.17.0 → trntensor-0.19.0}/docs/aws_setup.md +0 -0
  52. {trntensor-0.17.0 → trntensor-0.19.0}/docs/benchmarks.md +0 -0
  53. {trntensor-0.17.0 → trntensor-0.19.0}/docs/design/matmul_dispatch_findings.md +0 -0
  54. {trntensor-0.17.0 → trntensor-0.19.0}/docs/design/matmul_kernel_profile.md +0 -0
  55. {trntensor-0.17.0 → trntensor-0.19.0}/docs/developing_kernels.md +0 -0
  56. {trntensor-0.17.0 → trntensor-0.19.0}/docs/index.md +0 -0
  57. {trntensor-0.17.0 → trntensor-0.19.0}/docs/installation.md +0 -0
  58. {trntensor-0.17.0 → trntensor-0.19.0}/docs/quickstart.md +0 -0
  59. {trntensor-0.17.0 → trntensor-0.19.0}/docs/tutorial_df_mp2.md +0 -0
  60. {trntensor-0.17.0 → trntensor-0.19.0}/examples/df_mp2_einsum.py +0 -0
  61. {trntensor-0.17.0 → trntensor-0.19.0}/infra/terraform/README.md +0 -0
  62. {trntensor-0.17.0 → trntensor-0.19.0}/infra/terraform/main.tf +0 -0
  63. {trntensor-0.17.0 → trntensor-0.19.0}/mkdocs.yml +0 -0
  64. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/bench_to_md.py +0 -0
  65. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/run_benchmarks.sh +0 -0
  66. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/run_neuron_profile.sh +0 -0
  67. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/run_neuron_tests.sh +0 -0
  68. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/run_reduce_parallel_multirank.py +0 -0
  69. {trntensor-0.17.0 → trntensor-0.19.0}/scripts/run_simulator_tests.sh +0 -0
  70. {trntensor-0.17.0 → trntensor-0.19.0}/setup.cfg +0 -0
  71. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_decompose.py +0 -0
  72. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_nki.py +0 -0
  73. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_nki_sim.py +0 -0
  74. {trntensor-0.17.0 → trntensor-0.19.0}/tests/test_parallel.py +0 -0
  75. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/decompose.py +0 -0
  76. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/nki/__init__.py +0 -0
  77. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/nki/collectives.py +0 -0
  78. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/parallel.py +0 -0
  79. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor/py.typed +0 -0
  80. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor.egg-info/dependency_links.txt +0 -0
  81. {trntensor-0.17.0 → trntensor-0.19.0}/trntensor.egg-info/top_level.txt +0 -0
@@ -30,8 +30,10 @@ jobs:
30
30
  - uses: astral-sh/setup-uv@v6
31
31
  with:
32
32
  python-version: ${{ matrix.python-version }}
33
- - run: uv sync --extra dev
34
- - run: uv run pytest tests/ -v -x --tb=short -m "not neuron and not nki_simulator" --cov=trntensor --cov-report=xml
33
+ # uv pip (not uv sync): sync resolves every extra, and [torchneuron]'s nki>=0.6
34
+ # lives only on the Neuron index.
35
+ - run: uv venv && uv pip install -e ".[dev]"
36
+ - run: uv run --no-sync pytest tests/ -v -x --tb=short -m "not neuron and not nki_simulator" --cov=trntensor --cov-report=xml
35
37
 
36
38
  - name: Upload coverage reports to Codecov
37
39
  uses: codecov/codecov-action@v5
@@ -50,10 +52,10 @@ jobs:
50
52
  - uses: astral-sh/setup-uv@v6
51
53
  with:
52
54
  python-version: "3.12"
53
- - run: uv sync --extra dev
55
+ - run: uv venv && uv pip install -e ".[dev]"
54
56
  - name: Install NKI simulator
55
57
  run: uv pip install --extra-index-url https://pip.repos.neuron.amazonaws.com "nki>=0.3.0"
56
58
  - name: Run simulator-backed kernel tests
57
59
  env:
58
60
  TRNTENSOR_USE_SIMULATOR: "1"
59
- run: uv run pytest tests/ -v -m nki_simulator --tb=short
61
+ run: uv run --no-sync pytest tests/ -v -m nki_simulator --tb=short
@@ -7,12 +7,139 @@ 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
+
46
+ ## [0.18.0] — 2026-10-30
47
+
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.
49
+
50
+ Headline performance: native `"auto"` routes to aten on the device, and the planner contraction-order fix takes the DF-MP2 AO→MO transform (`mi,na,mnP->iaP`, nb=256) from 70 to 1.16 ms (#49).
51
+
52
+ ### Changed
53
+
54
+ - **Native dispatch thresholds are now measured (#41).** They were measured
55
+ on trn2.3xlarge (SDK 2.33, NKI 0.7.0) with `scripts/bench_native_einsum.py`
56
+ and `scripts/autotune_dispatch.py`.
57
+ - On device-resident operands, aten `torch.matmul` / `torch.bmm` beat the
58
+ NKI matmul and batched-matmul kernels at every size (128³ to 4096³, fp32
59
+ and bf16). The native pinned threshold now defaults to `never`, so
60
+ device work runs as aten on `neuron`.
61
+ - Host operands at or above `TRNTENSOR_MIN_NKI_FLOPS_NATIVE` (now
62
+ 500 M multiply-adds, previously a provisional 1 G) are offloaded to
63
+ aten on the device instead of to the NKI kernel.
64
+ - Thresholds accept `never` / `inf`.
65
+ - The XLA path is unchanged.
66
+ - **Native `"auto"` runs the fused quantum primitives as aten.** This covers
67
+ `mp2_energy` and `ao_to_mo_transform`. On Trn2 the AO→MO kernel measured
68
+ 6–20× slower than aten, and the MP2 kernel 1.0–1.5× slower.
69
+ `set_backend("nki")` still runs both kernels, and the XLA path is
70
+ unchanged.
71
+ - **`mp2_energy` reference path is vectorized.** Without NKI it now uses
72
+ aten ops chunked over `i` instead of a nocc² Python loop. That is 10–40×
73
+ faster on device. On an Apple M-series CPU it is 3× faster at small sizes
74
+ and about even (±10%) at `nocc=32, nvir=128, naux=512`.
75
+ - **`ao_to_mo_transform` reference path** is now two explicit binary
76
+ contractions.
77
+
78
+ ### Fixed
79
+
80
+ - **Greedy contraction path no longer starts with an outer product.** For
81
+ example, `einsum("mi,na,mnP->iaP")` used to contract `mi,na` first,
82
+ building a 4-index intermediate. Pairs that share an index are now
83
+ preferred.
84
+ - **`scripts/autotune_dispatch.py` now reports M·K·N**, the quantity dispatch
85
+ compares, where it used to report 2·M·K·N. On the native path it now
86
+ derives both thresholds: host vs device offload, and NKI vs aten on device.
87
+
10
88
  ## [0.17.0] — 2026-10-28
11
89
 
12
90
  Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron device) — see trnsci/trnsci#35.
13
91
 
14
92
  ### Added
15
93
 
94
+ - **Native PyTorch dispatch on TorchNeuron (#41, trnsci/trnsci#35).** On
95
+ TorchNeuron (Neuron SDK 2.33+, `torch-neuronx` 2.14, PyTorch 2.14,
96
+ Trn2/Trn3) every host-wrapper NKI kernel is a PyTorch custom op registered
97
+ with `torch_neuronx.nki_op` and launched on `device="neuron"`, with no
98
+ `torch_xla` import: `trntensor::matmul`, `batched_matmul`, `mp2_energy`,
99
+ `ao_to_mo_transform`. Kernel bodies are unchanged. The PyTorch/XLA path
100
+ (Trn1) is unchanged.
101
+ - The dispatch path is detected on the first kernel launch, not at
102
+ import: native when `torch_neuronx` exports `nki_op` and
103
+ `torch_neuronx.device_count() > 0`, XLA when `torch_xla` is installed,
104
+ otherwise none. `TRNSCI_NKI_DISPATCH=auto|native|xla` (suite-wide)
105
+ forces a path; forcing an unavailable one raises.
106
+ - The ops are registered lazily (`trntensor/nki/_native.py` is imported
107
+ only on the native path). Every launch goes through one helper,
108
+ `dispatch._launch`. That covers `einsum` (matmul/bmm/path steps),
109
+ `multi_einsum` (batched and per-contraction), `ao_to_mo_transform`,
110
+ `mp2_energy`, and `precision="sr"` (which passes `use_sr=True` to the op).
111
+ `precision="kahan"` stays a host fp64 contraction, and `"dd"` still
112
+ raises on NKI hosts (trnblas#22). The reduce-parallel `nki.collectives`
113
+ all-reduce from Phase 4 is unchanged.
114
+ - New `to_device` / `from_device` place tensors on the active path's
115
+ device (`neuron` or XLA). `to_xla` / `from_xla` stay public. On the
116
+ native path `to_xla` places on `neuron`, so existing residency code keeps
117
+ working. `multi_einsum` pre-pins shared operands on either device.
118
+ - FLOP dispatch thresholds are path-aware. The XLA values are unchanged
119
+ (2 GFLOPs unpinned, 1 GFLOPs pinned). The native path uses provisional
120
+ 1 GFLOPs / 128 MFLOPs, because eager native dispatch costs about 0.2 ms
121
+ per launch. Override them with `TRNTENSOR_MIN_NKI_FLOPS_NATIVE[_PINNED]`
122
+ or the `trntensor_min_nki_flops_native[_pinned]` autotune cache keys.
123
+ `scripts/autotune_dispatch.py` measures and writes the native keys on a
124
+ TorchNeuron host.
125
+ - Dormant deprecation hook: with `TRNSCI_XLA_DEPRECATION=1` (off by
126
+ default), selecting the XLA path and calling `to_xla` / `from_xla` each
127
+ emit a one-time `DeprecationWarning`. Migration: replace `to_xla` with
128
+ `to_device` and `from_xla` with `from_device` (docs/api/nki.md).
129
+ - The `mp2_energy` CPU fallback copies `neuron`-resident operands to the
130
+ host before its scalar loop, then moves the result back.
131
+ - New extras: `[torchneuron]` (`torch>=2.14`, `torch-neuronx>=2.14`,
132
+ `nki>=0.6`) and `[neuron]` for the XLA lineage (`torch-neuronx>=2.9`,
133
+ `nki>=0.3.0`).
134
+ - Tests: `tests/conftest.py` sets `NKI_ENABLE_TRACE_CACHE=0` by default.
135
+ New CPU tests in `tests/test_native_dispatch.py` cover path selection,
136
+ launch routing for every entry point, lazy registration, op schemas,
137
+ fake shapes, thresholds, and the deprecation hook. New
138
+ `@pytest.mark.neuron` tests in `tests/test_nki_native.py` have not yet
139
+ run on hardware. `TestXlaResidency::test_matmul_stays_on_xla` is now
140
+ path-aware: it expects `neuron` on TorchNeuron and `xla` on Trn1, and
141
+ still checks residency.
142
+
16
143
  - **Reduce-parallel sharding on real multi-rank all-reduce (Phase 4 prototype).**
17
144
  Before this change, reduce-parallel and mixed output/reduce-parallel einsums
18
145
  raised `NotImplementedError` whenever `HAS_NKI` was true.
@@ -24,7 +24,10 @@ trntensor/
24
24
  │ ├── decompose.py # cp_decompose, tucker_decompose (HOSVD)
25
25
  │ └── nki/
26
26
  │ ├── __init__.py
27
- │ └── dispatch.py # Fused contraction kernels
27
+ │ ├── _kernels.py # @nki.jit kernels
28
+ │ ├── _native.py # TorchNeuron custom ops (nki_op), loaded lazily
29
+ │ ├── collectives.py # nki.collectives all-reduce (reduce-parallel)
30
+ │ └── dispatch.py # host wrappers, dispatch-path selection, _launch
28
31
  ├── tests/
29
32
  │ ├── test_einsum.py # Matmul, bmm, 4-index transform, DF-MP2 pattern
30
33
  │ └── test_decompose.py # CP rank-1/low-rank, Tucker orthogonality
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trntensor
3
- Version: 0.17.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
@@ -18,8 +18,15 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: torch>=2.1
20
20
  Requires-Dist: numpy>=1.24
21
+ Provides-Extra: neuron
22
+ Requires-Dist: nki>=0.3.0; extra == "neuron"
23
+ Requires-Dist: torch-neuronx>=2.9; extra == "neuron"
24
+ Provides-Extra: torchneuron
25
+ Requires-Dist: torch>=2.14; extra == "torchneuron"
26
+ Requires-Dist: torch-neuronx>=2.14; extra == "torchneuron"
27
+ Requires-Dist: nki>=0.6; extra == "torchneuron"
21
28
  Provides-Extra: dev
22
- Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest<9,>=7.0; extra == "dev"
23
30
  Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
24
31
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
32
  Requires-Dist: ruff>=0.6; extra == "dev"
@@ -59,8 +66,32 @@ Suite-wide tracker: [trnsci/trnsci#1](https://github.com/trnsci/trnsci/issues/1)
59
66
 
60
67
  ```bash
61
68
  pip install trntensor
69
+
70
+ # Trn2/Trn3 with TorchNeuron (native PyTorch, Neuron SDK 2.33+)
71
+ pip install "trntensor[torchneuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
72
+
73
+ # Trn1 / PyTorch-XLA lineage
74
+ pip install "trntensor[neuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
62
75
  ```
63
76
 
77
+ Kernels dispatch on whichever Neuron PyTorch lineage is installed:
78
+
79
+ - **TorchNeuron (native)**: Trn2/Trn3, Neuron SDK 2.33+, Python ≥ 3.12,
80
+ `torch` 2.14, `torch-neuronx` 2.14.*. Each kernel is a PyTorch custom op
81
+ (`torch.ops.trntensor.*`) on the `neuron` device; no `torch_xla`.
82
+ - **PyTorch/XLA**: Trn1 (and any host on the `torch-neuronx` 2.9 lineage).
83
+ Kernels dispatch through `torch_xla`, unchanged.
84
+
85
+ trntensor picks the native path when the native `torch_neuronx` and a
86
+ `neuron` device are present, and the XLA path when `torch_xla` is
87
+ installed; otherwise it runs on PyTorch. Set `TRNSCI_NKI_DISPATCH=native`
88
+ or `xla` (shared by all trnsci libraries) to force a path. Pin operands on
89
+ the device with `to_device` / `from_device`; the older `to_xla` /
90
+ `from_xla` still work on both lineages (see the
91
+ [migration note](docs/api/nki.md#migrating-from-to_xla-from_xla)). With an
92
+ editable install (`pip install -e`), set `NKI_ENABLE_TRACE_CACHE=0`: the NKI
93
+ kernel cache only versions installed wheels and can serve stale kernels.
94
+
64
95
  ## Usage
65
96
 
66
97
  ```python
@@ -103,7 +134,8 @@ core, factors = trntensor.tucker_decompose(tensor, ranks=(5, 5, 5))
103
134
  - [x] Tucker decomposition (HOSVD)
104
135
  - [x] DF-MP2 einsum example
105
136
  - [x] NKI fused contraction kernels (`mp2_energy`, `ao_to_mo_transform`)
106
- - [x] XLA operand residency (`to_xla` / `from_xla`)
137
+ - [x] Device operand residency (`to_device` / `from_device`; `to_xla` / `from_xla` kept)
138
+ - [x] Native TorchNeuron dispatch (Trn2/Trn3, `torch.ops.trntensor.*`) alongside PyTorch/XLA (Trn1)
107
139
  - [x] NKI CPU simulator + `nki-simulator` CI gate
108
140
  - [x] Optimal contraction ordering — greedy path search for 3+ operands
109
141
  - [x] Multi-contraction shared-operand XLA pre-pinning
@@ -1,34 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: trntensor
3
- Version: 0.17.0
4
- Summary: Tensor contractions for AWS Trainium via NKI
5
- Author-email: Scott Friedman <scttfrdmn@gmail.com>
6
- License-Expression: Apache-2.0
7
- Project-URL: Homepage, https://github.com/trnsci/trntensor
8
- Project-URL: Documentation, https://trnsci.dev/trntensor/
9
- Project-URL: Repository, https://github.com/trnsci/trntensor
10
- Project-URL: Issues, https://github.com/trnsci/trntensor/issues
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Science/Research
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Topic :: Scientific/Engineering
15
- Classifier: Topic :: Scientific/Engineering :: Mathematics
16
- Requires-Python: >=3.10
17
- Description-Content-Type: text/markdown
18
- License-File: LICENSE
19
- Requires-Dist: torch>=2.1
20
- Requires-Dist: numpy>=1.24
21
- Provides-Extra: dev
22
- Requires-Dist: pytest>=7.0; extra == "dev"
23
- Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
24
- Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
- Requires-Dist: ruff>=0.6; extra == "dev"
26
- Requires-Dist: pre-commit>=3.7; extra == "dev"
27
- Requires-Dist: scipy>=1.11; extra == "dev"
28
- Requires-Dist: mkdocs>=1.5; extra == "dev"
29
- Requires-Dist: mkdocs-material>=9.0; extra == "dev"
30
- Dynamic: license-file
31
-
32
1
  # trntensor
33
2
 
34
3
  [![CI](https://github.com/trnsci/trntensor/actions/workflows/ci.yml/badge.svg)](https://github.com/trnsci/trntensor/actions/workflows/ci.yml)
@@ -59,8 +28,32 @@ Suite-wide tracker: [trnsci/trnsci#1](https://github.com/trnsci/trnsci/issues/1)
59
28
 
60
29
  ```bash
61
30
  pip install trntensor
31
+
32
+ # Trn2/Trn3 with TorchNeuron (native PyTorch, Neuron SDK 2.33+)
33
+ pip install "trntensor[torchneuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
34
+
35
+ # Trn1 / PyTorch-XLA lineage
36
+ pip install "trntensor[neuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
62
37
  ```
63
38
 
39
+ Kernels dispatch on whichever Neuron PyTorch lineage is installed:
40
+
41
+ - **TorchNeuron (native)**: Trn2/Trn3, Neuron SDK 2.33+, Python ≥ 3.12,
42
+ `torch` 2.14, `torch-neuronx` 2.14.*. Each kernel is a PyTorch custom op
43
+ (`torch.ops.trntensor.*`) on the `neuron` device; no `torch_xla`.
44
+ - **PyTorch/XLA**: Trn1 (and any host on the `torch-neuronx` 2.9 lineage).
45
+ Kernels dispatch through `torch_xla`, unchanged.
46
+
47
+ trntensor picks the native path when the native `torch_neuronx` and a
48
+ `neuron` device are present, and the XLA path when `torch_xla` is
49
+ installed; otherwise it runs on PyTorch. Set `TRNSCI_NKI_DISPATCH=native`
50
+ or `xla` (shared by all trnsci libraries) to force a path. Pin operands on
51
+ the device with `to_device` / `from_device`; the older `to_xla` /
52
+ `from_xla` still work on both lineages (see the
53
+ [migration note](docs/api/nki.md#migrating-from-to_xla-from_xla)). With an
54
+ editable install (`pip install -e`), set `NKI_ENABLE_TRACE_CACHE=0`: the NKI
55
+ kernel cache only versions installed wheels and can serve stale kernels.
56
+
64
57
  ## Usage
65
58
 
66
59
  ```python
@@ -103,7 +96,8 @@ core, factors = trntensor.tucker_decompose(tensor, ranks=(5, 5, 5))
103
96
  - [x] Tucker decomposition (HOSVD)
104
97
  - [x] DF-MP2 einsum example
105
98
  - [x] NKI fused contraction kernels (`mp2_energy`, `ao_to_mo_transform`)
106
- - [x] XLA operand residency (`to_xla` / `from_xla`)
99
+ - [x] Device operand residency (`to_device` / `from_device`; `to_xla` / `from_xla` kept)
100
+ - [x] Native TorchNeuron dispatch (Trn2/Trn3, `torch.ops.trntensor.*`) alongside PyTorch/XLA (Trn1)
107
101
  - [x] NKI CPU simulator + `nki-simulator` CI gate
108
102
  - [x] Optimal contraction ordering — greedy path search for 3+ operands
109
103
  - [x] Multi-contraction shared-operand XLA pre-pinning
@@ -0,0 +1,187 @@
1
+ # NKI dispatch
2
+
3
+ Backend selection between the PyTorch fallback and the NKI-accelerated
4
+ path on Trainium. Kernels live in `trntensor.nki._kernels`; the
5
+ public-facing dispatch is in `trntensor.nki.dispatch`.
6
+
7
+ ## `set_backend(backend: str)`
8
+
9
+ - `"auto"` — use NKI if `nki` is importable, else PyTorch
10
+ - `"pytorch"` — always use `torch.matmul` / `torch.einsum`
11
+ - `"nki"` — require NKI; raises `RuntimeError` on non-Neuron hosts
12
+
13
+ ## `get_backend() -> str`
14
+
15
+ Return the current backend selection.
16
+
17
+ ## `HAS_NKI: bool`
18
+
19
+ Module-level flag set at import time. `True` when `nki` (0.3.0+) is
20
+ importable, otherwise `False`.
21
+
22
+ ## Dispatch path
23
+
24
+ On Neuron hardware, kernels run on one of two `torch-neuronx` lineages,
25
+ detected on the first kernel launch (never at import):
26
+
27
+ - **Native (TorchNeuron, Trn2/Trn3, Neuron SDK 2.33+)** — chosen when
28
+ `torch_neuronx` exports `nki_op` and a `neuron` device is available
29
+ (`torch_neuronx.device_count() > 0`). Each kernel is a PyTorch custom op
30
+ on `device="neuron"`, also callable directly and inside
31
+ `torch.compile(backend="neuron")`: `torch.ops.trntensor.matmul`,
32
+ `batched_matmul`, `mp2_energy`, `ao_to_mo_transform`. The ops are
33
+ registered only once this path is selected, and this path never imports
34
+ `torch_xla`.
35
+ - **XLA (Trn1)** — chosen when `torch_xla` is installed and the native
36
+ path is unavailable. Kernels run on the `torch_xla` device, unchanged.
37
+ - Otherwise there is no device path: the host wrappers fall back to
38
+ `torch` (or raise under `TRNTENSOR_REQUIRE_NKI=1`), as before.
39
+
40
+ `set_backend` is unaffected: it still chooses NKI vs PyTorch. Set the
41
+ suite-wide `TRNSCI_NKI_DISPATCH=native` or `xla` to force a path; forcing an
42
+ unavailable path raises `RuntimeError`.
43
+
44
+ The reduce-parallel all-reduce kernel (`trntensor.nki.collectives`) is
45
+ native-only already; it keeps its `torch_neuronx.wrap_nki` launch because
46
+ its `ReplicaGroup` argument is a compile-time constant that a custom-op
47
+ schema cannot carry.
48
+
49
+ Precision modes follow the same launch: `precision="sr"` passes
50
+ `use_sr=True` to the `matmul` / `batched_matmul` op (stochastic rounding
51
+ still needs `NEURON_RT_STOCHASTIC_ROUNDING_EN=1` at process start and warns
52
+ once otherwise); `precision="kahan"` contracts in true fp64 on the host
53
+ (Trainium has no fp64 datapath) on both paths; `precision="dd"` still
54
+ raises on NKI hosts pending trnblas#22.
55
+
56
+ ## `to_device(tensor) -> Tensor` / `from_device(tensor) -> Tensor`
57
+
58
+ Pin an operand on the Trainium device of the active dispatch path —
59
+ `neuron` on TorchNeuron, the XLA device on Trn1 — so repeated trntensor
60
+ calls skip the host↔device transfer that otherwise dominates dispatch
61
+ overhead.
62
+
63
+ ```python
64
+ import trntensor
65
+
66
+ # One-time transfer onto the accelerator.
67
+ eri_d = trntensor.to_device(eri)
68
+ C_occ_d = trntensor.to_device(C_occ)
69
+ C_vir_d = trntensor.to_device(C_vir)
70
+ eps_occ_d = trntensor.to_device(eps_occ)
71
+ eps_vir_d = trntensor.to_device(eps_vir)
72
+
73
+ # Full DF-MP2 pipeline — B_d never leaves the device.
74
+ B_d = trntensor.ao_to_mo_transform(eri_d, C_occ_d, C_vir_d)
75
+ E_d = trntensor.mp2_energy(B_d, eps_occ_d, eps_vir_d)
76
+
77
+ # Pull the scalar back when we actually need it in Python.
78
+ E = trntensor.from_device(E_d)
79
+ ```
80
+
81
+ - `to_device`: no-op when the tensor is already on a Neuron device.
82
+ Raises `RuntimeError` on hosts without the NKI runtime.
83
+ - `from_device`: no-op when the tensor is already on CPU.
84
+
85
+ When every operand is already on the device, dispatch skips the per-call
86
+ transfer, uses the lower "pinned" FLOP threshold, and returns the result on
87
+ the device — the caller controls when to pull back.
88
+
89
+ ## `to_xla(tensor) -> Tensor` / `from_xla(tensor) -> Tensor`
90
+
91
+ The original names, kept for compatibility. On Trn1 they behave exactly as
92
+ before. On TorchNeuron there is no XLA device, so `to_xla` places the
93
+ tensor on `neuron` instead (residency works the same way) and `from_xla`
94
+ moves it back to CPU.
95
+
96
+ ### Migrating from `to_xla` / `from_xla`
97
+
98
+ Replace `to_xla` with `to_device` and `from_xla` with `from_device`. Same
99
+ behaviour on Trn1; on Trn2/Trn3 the new names say what they do. Checks such
100
+ as `t.device.type == "xla"` become `t.device.type in ("xla", "neuron")`.
101
+
102
+ `to_xla` / `from_xla` stay available. When the suite-wide
103
+ `TRNSCI_XLA_DEPRECATION=1` is set (off by default; trnsci turns it on at
104
+ TorchNeuron GA), each emits a one-time `DeprecationWarning` pointing at the
105
+ new names, and selecting the XLA dispatch path emits: "the PyTorch/XLA path
106
+ is deprecated on Trn2/Trn3; use TorchNeuron (native). Trn1/Inf2 remain
107
+ supported in maintenance mode."
108
+
109
+ ## Environment variables
110
+
111
+ | Variable | Effect |
112
+ |---|---|
113
+ | `TRNTENSOR_REQUIRE_NKI=1` | Re-raise kernel exceptions instead of falling back to PyTorch. Useful in the validation loop to surface silent kernel breakage. |
114
+ | `TRNTENSOR_MIN_NKI_FLOPS=<int>` | XLA path: override the FLOP threshold (M·K·N) below which dispatch skips NKI and uses `torch.matmul` / `torch.bmm` directly. Default: `2_000_000_000`. Set to `0` to force NKI for kernel validation. |
115
+ | `TRNTENSOR_MIN_NKI_FLOPS_PINNED=<int>` | XLA path, operands already on the device. Default: `1_000_000_000`. |
116
+ | `TRNTENSOR_MIN_NKI_FLOPS_NATIVE=<int>` | Native (TorchNeuron) path, operands on CPU: M·K·N at which the work leaves the host for the `neuron` device. Below it, `torch.matmul` / `torch.bmm` run on the host. Above it, the operands are offloaded. The offload runs aten unless the pinned threshold below selects NKI. Default: `500_000_000` (measured on Trn2, see below). |
117
+ | `TRNTENSOR_MIN_NKI_FLOPS_NATIVE_PINNED=<int>\|never` | Native path: M·K·N at which the NKI kernel replaces aten on the device. Below it, device-resident operands run `torch.matmul` / `torch.bmm` (aten) on `neuron`. Default: `never`, because on Trn2 aten beat the NKI kernels at every measured size. |
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
+ | `TRNSCI_XLA_DEPRECATION=1` | Suite-wide, off by default. Emit the one-time XLA-path and `to_xla` / `from_xla` deprecation warnings. |
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). |
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. |
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. |
125
+
126
+ ### Native thresholds: measured on Trn2
127
+
128
+ These were measured on a trn2.3xlarge (Neuron SDK 2.33, NKI 0.7.0), using one
129
+ NeuronCore and two host threads, with `scripts/bench_native_einsum.py` and
130
+ `scripts/autotune_dispatch.py` (#41):
131
+
132
+ - **Pinned operands: NKI never beats aten.** On device-resident operands,
133
+ aten `torch.matmul` / `torch.bmm` beat the trntensor NKI matmul and
134
+ batched-matmul kernels at every size from 128³ to 4096³, in both fp32 and
135
+ bf16. Examples:
136
+ - 4096³ bf16: 1.38 ms with aten, 6.82 ms with NKI.
137
+ - 1024³ fp32: 0.20 ms with aten, 0.55 ms with NKI.
138
+
139
+ The eager launch floor is about 0.13 ms for an aten op and about 0.24 ms for
140
+ an NKI custom op.
141
+ - **Host operands: offload to aten from about 0.5 G multiply-adds.**
142
+ Offloading to aten (upload, run on device, download) beats offloading
143
+ through the NKI kernel at every size. It also beats host torch:
144
+ - fp32: from about 134 M multiply-adds (512³) when the host has two
145
+ threads.
146
+ - bf16: from about 0.5 to 1 G multiply-adds.
147
+
148
+ The default of 500 M sits between these. A host with more threads moves the
149
+ crossover up. Re-measure on your host with `scripts/autotune_dispatch.py`.
150
+
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.
171
+
172
+ ## Kernels (internal)
173
+
174
+ - `matmul_kernel(a, b)` — 2-index matmul with stationary-A tile reuse.
175
+ Tile constants: `TILE_M=128`, `TILE_K=128`, `TILE_N=512`.
176
+ - `batched_matmul_kernel(a, b)` — per-batch-slice matmul. Batch dim
177
+ iterated via `nl.affine_range`; each slice reuses the stationary-A
178
+ tile layout.
179
+
180
+ The DF-MP2 pair pattern `einsum("ap,bp->ab")` is served by
181
+ `matmul_kernel` through the planner's `transB=True` route — no
182
+ dedicated kernel needed. Fused energy-denominator kernel is tracked
183
+ in [#13][i13].
184
+
185
+ [i13]: https://github.com/trnsci/trntensor/issues/13
186
+
187
+ [m2]: https://github.com/trnsci/trntensor/milestone/2
@@ -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.