Skip to content

Commit 5319be3

Browse files
committed
docs: tell the fused mp2_energy story + honest perf note
- docs/architecture.md: new section explaining the fused contract+elementwise+reduce pattern as trntensor's architectural core, with the mp2_energy kernel as the canonical demonstration - docs/api/quantum.md (new): mp2_energy reference - docs/api/index.md + mkdocs.yml: Quantum primitives nav entry - docs/tutorial_df_mp2.md: 'Fusing the whole calculation' section - docs/benchmarks.md: add fused vs Python-loop row with honest note that dispatch+compile overhead still dominates at current sizes
1 parent eb54a3b commit 5319be3

6 files changed

Lines changed: 114 additions & 0 deletions

File tree

docs/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ trntensor's public API is organized into four modules:
55
- **[einsum](einsum.md)**`einsum`, `multi_einsum`
66
- **[plan](plan.md)**`plan_contraction`, `ContractionPlan`, `estimate_flops`
77
- **[decompose](decompose.md)**`cp_decompose`, `tucker_decompose`, and reconstructors
8+
- **[quantum](quantum.md)** — fused chemistry primitives (`mp2_energy`)
89
- **[nki](nki.md)** — backend dispatch (`set_backend`, `get_backend`, `HAS_NKI`)

docs/api/quantum.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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**: falls back to a Python loop over `(i, j)` pairs composing `torch.einsum`
44+
and element-wise ops. Same as `examples/df_mp2_einsum.py`.
45+
- **Trainium (NKI)**: dispatches a single `@nki.jit` program that
46+
- accumulates `T` in PSUM via `nisa.nc_matmul`,
47+
- builds `Δ` on the Vector Engine from SBUF-resident `ε` tiles,
48+
- folds the energy into a scalar accumulator in SBUF,
49+
- writes one partial per `(i, j)` pair to HBM.
50+
51+
The host sums the `(nocc, nocc)` partial matrix into the final scalar. No intermediate four-index `T` tensor is ever materialized.
52+
53+
### Current limitations
54+
55+
- Single-tile path only: `nvir ≤ 128` and `naux ≤ 128`. Larger systems raise `NotImplementedError` — K/M tiling is a follow-up.
56+
- Dispatch overhead still dominates at small sizes; see [Benchmarks](../benchmarks.md) for the honest comparison against the Python loop.

docs/architecture.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ The planner analyzes einsum subscripts and selects a dispatch target:
2626

2727
`ContractionPlan` carries the dispatch decision, the FLOPs estimate, and any reshape/transpose preamble so the same plan can be re-executed cheaply.
2828

29+
## Fused contraction-reduction: the architectural core
30+
31+
What makes trntensor different from `torch.einsum` or `trnblas.gemm` is **fusion across the boundary between contraction and reduction**. On Trainium:
32+
33+
- the Tensor Engine does the contraction, accumulating into PSUM
34+
- the Vector Engine does the elementwise reshaping, reading from SBUF
35+
- a small SBUF scalar accumulates the reduction
36+
- nothing lands in HBM until the whole program finishes
37+
38+
A cuTENSOR clone would compose these three phases as separate ops with HBM round-trips between them. NKI lets us write them as one `@nki.jit` program with the same data living in PSUM → SBUF → accumulator across phases. That's the architectural lever.
39+
40+
The canonical demonstration is `trntensor.mp2_energy` — the DF-MP2 correlation energy is
41+
42+
```
43+
T_{i,j,a,b} = Σ_P B_{i,a,P} B_{j,b,P} (contraction)
44+
term = T * (2T - T^T) / Δ_{i,j,a,b} (elementwise)
45+
E = Σ_{i,j,a,b} term (reduction)
46+
```
47+
48+
The fused kernel in `trntensor/nki/_kernels.py::mp2_energy_kernel` does all three in one program, iterating over `(i, j)` pairs in `affine_range`. No intermediate `T` tensor is ever materialized to HBM.
49+
50+
This pattern — fused contract + elementwise + reduce with SBUF-resident intermediates — is what trntensor extends across the suite. Generic `einsum` dispatch to `matmul` / `bmm` kernels is the foundation; primitives like `mp2_energy` are the architectural flagships.
51+
52+
**Performance honest note.** The fused kernel is architecturally correct but currently carries ~15–40 ms of XLA dispatch + compile overhead per call, which means at small chemistry sizes the CPU fallback path still wins. The fusion work pays off once per-call overhead is amortized (tracked in #33/#34). See [Benchmarks](benchmarks.md) for the current numbers.
53+
2954
## Decompositions for quantum chemistry
3055

3156
- **CP (CANDECOMP/PARAFAC)** — Tensor hypercontraction (THC) of two-electron integrals. Reduces $O(N^4)$ storage to $O(N^2 R)$.

docs/benchmarks.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ Both columns ran on the same trn1.2xlarge instance — CPU (Intel Xeon 8375C) vs
3333

3434
**NKI wins**: 2048×2048 matmul (1.6× faster than CPU). All other sizes still favor CPU on this hardware.
3535

36+
## DF-MP2 energy — fused vs Python-loop
37+
38+
Fused `trntensor.mp2_energy` (single NKI program) vs the reference loop from `examples/df_mp2_einsum.py` (25–256 einsum calls), both on trn1.2xlarge:
39+
40+
| Workload | Python loop | Fused NKI | Winner |
41+
|---|---:|---:|---|
42+
| `(nocc, nvir, naux) = (5, 19, 72)` | **1.5 ms** | 15.6 ms | Loop 10× |
43+
| `(nocc, nvir, naux) = (16, 128, 128)` | **25.5 ms** | 41.3 ms | Loop 1.6× |
44+
45+
The fused kernel is **architecturally** what we want — one program, no intermediate HBM materialization of the four-index `T` tensor, PSUM/SBUF-resident across contract → elementwise → reduce. But NKI's per-call dispatch + compile overhead (~15–40 ms floor) still eats the win at current sizes. The gap closes with scale (10× → 1.6× as workload grows), consistent with the overhead being a fixed cost the compute amortizes. See [#33][i33] and [#34][i34].
46+
47+
48+
3649
## Size-based dispatch threshold
3750

3851
Because per-call NKI dispatch currently carries ~1 ms of XLA launch overhead, `nki_matmul` and `nki_batched_matmul` short-circuit to the PyTorch path when the contraction is below `TRNTENSOR_MIN_NKI_FLOPS` (default **2 GFLOPs**, calibrated at ≈ half the smallest NKI-winning size). The `plan.backend` field reflects this: it reports `"nki"` only when the dispatch will actually invoke a kernel, and `"pytorch"` otherwise.
@@ -54,3 +67,4 @@ Recommendation for users:
5467
- Until #33 lands, tight loops of small contractions (DF-MP2 pair-energy style) see no NKI benefit and are served by the PyTorch path.
5568

5669
[i33]: https://github.com/trnsci/trntensor/issues/33
70+
[i34]: https://github.com/trnsci/trntensor/issues/34

docs/tutorial_df_mp2.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,23 @@ for i in range(nocc):
9292

9393
will reveal exactly where NKI is or isn't being used.
9494

95+
## Fusing the whole calculation
96+
97+
The Python loop above does 25 einsum calls, 25 elementwise passes, and 25 host reductions — each landing in HBM between steps. On Trainium, trntensor can compile the entire DF-MP2 energy into **one NKI program**:
98+
99+
```python
100+
E = trntensor.mp2_energy(B, eps_occ, eps_vir)
101+
```
102+
103+
Inside that single call:
104+
105+
1. `T_ab = Σ_P B[i,a,P] B[j,b,P]` accumulates in PSUM via `nc_matmul`
106+
2. The spin-adapted numerator `(2T − T^T)` and the denominator `Δ_ab = ε_i + ε_j − ε_a − ε_b` are built on the Vector Engine from SBUF-resident ε tiles
107+
3. `(T·(2T − T^T) / Δ).sum()` folds into a scalar accumulator in SBUF
108+
4. One HBM partial per `(i, j)` pair; host sums to the final scalar
109+
110+
No intermediate `T` tensor is ever materialized to HBM. See [API: quantum](api/quantum.md) and [Architecture](architecture.md) for the full description.
111+
95112
## Current limitations
96113

97114
- The energy denominator is a separate element-wise op. A fused kernel that folds the division into the PSUM accumulation — `E = Σ T²/Δ` in one kernel — is tracked in [#13][i13]. Landing it collapses the pair-energy loop into a single tensor-engine invocation per `(i,j)`.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ nav:
2525
- einsum: api/einsum.md
2626
- Planning: api/plan.md
2727
- Decompositions: api/decompose.md
28+
- Quantum primitives: api/quantum.md
2829
- NKI dispatch: api/nki.md
2930
- Architecture: architecture.md
3031
- Benchmarks: benchmarks.md

0 commit comments

Comments
 (0)