Skip to content

Commit ef4b183

Browse files
committed
feat: alpha/beta scaling for einsum (#20)
Extend einsum() with keyword-only alpha, beta, out parameters matching cuTENSOR's GEMM-style interface: einsum(s, A, B, alpha=α, beta=β, out=C) returns α*contract(A,B) + β*C. Defaults (1.0, 0.0, None) preserve all existing behaviour exactly; recursive calls from _execute_path are unaffected since scaling is applied only at the outer einsum() boundary.
1 parent 065fd20 commit ef4b183

3 files changed

Lines changed: 78 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
where the three-center integral tensor `B` feeds many pair
2626
contractions. Falls back to the existing per-contraction loop on CPU.
2727

28+
- **Subscript and shape validation with descriptive errors** (#26) —
29+
`plan_contraction()` now validates subscripts up-front and raises
30+
`ValueError` with precise messages: wrong operand count, rank
31+
mismatch, and inconsistent index sizes are all caught before any
32+
torch operation runs. Eliminates cryptic downstream errors from
33+
`torch.einsum`.
34+
35+
- **PEP 561 `py.typed` marker** (#25) — `trntensor` now ships a
36+
`py.typed` file so type checkers (mypy, pyright, etc.) recognise
37+
the package as typed and apply inline annotations.
38+
39+
- **`alpha`/`beta` scaling for `einsum`** (#20) — matches cuTENSOR's
40+
GEMM-style interface:
41+
`einsum(subscripts, A, B, alpha=α, beta=β, out=C)` returns
42+
`α * contract(A, B) + β * C`. Defaults (`alpha=1, beta=0, out=None`)
43+
preserve existing behaviour exactly. Useful for accumulation patterns
44+
and in-place gradient updates without an extra allocation.
45+
2846
## [0.2.0] — 2026-04-15
2947

3048
### Added

tests/test_einsum.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,38 @@ def test_flop_estimate(self):
211211
B = torch.randn(20, 30)
212212
flops = trntensor.estimate_flops("ij,jk->ik", A, B)
213213
assert flops == 10 * 20 * 30
214+
215+
216+
class TestAlphaBeta:
217+
"""Tests for alpha/beta scaling interface (#20)."""
218+
219+
def test_alpha_scaling(self):
220+
A = torch.randn(4, 3)
221+
B = torch.randn(3, 5)
222+
result = trntensor.einsum("ij,jk->ik", A, B, alpha=2.0)
223+
expected = 2.0 * (A @ B)
224+
np.testing.assert_allclose(result.numpy(), expected.numpy(), atol=1e-5)
225+
226+
def test_beta_accumulate(self):
227+
A = torch.randn(4, 3)
228+
B = torch.randn(3, 5)
229+
C = torch.randn(4, 5)
230+
result = trntensor.einsum("ij,jk->ik", A, B, beta=0.5, out=C)
231+
expected = A @ B + 0.5 * C
232+
np.testing.assert_allclose(result.numpy(), expected.numpy(), atol=1e-5)
233+
234+
def test_alpha_beta_combined(self):
235+
A = torch.randn(4, 3)
236+
B = torch.randn(3, 5)
237+
C = torch.randn(4, 5)
238+
result = trntensor.einsum("ij,jk->ik", A, B, alpha=2.0, beta=0.5, out=C)
239+
expected = 2.0 * (A @ B) + 0.5 * C
240+
np.testing.assert_allclose(result.numpy(), expected.numpy(), atol=1e-5)
241+
242+
def test_defaults_unchanged(self):
243+
"""Default alpha=1, beta=0, out=None is identical to plain einsum."""
244+
A = torch.randn(4, 3)
245+
B = torch.randn(3, 5)
246+
result = trntensor.einsum("ij,jk->ik", A, B, alpha=1.0, beta=0.0, out=None)
247+
expected = A @ B
248+
np.testing.assert_allclose(result.numpy(), expected.numpy(), atol=1e-5)

trntensor/einsum.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,46 @@
2424
from .plan import ContractionPlan, _parse_subscripts, plan_contraction
2525

2626

27-
def einsum(subscripts: str, *operands: torch.Tensor) -> torch.Tensor:
27+
def einsum(
28+
subscripts: str,
29+
*operands: torch.Tensor,
30+
alpha: float = 1.0,
31+
beta: float = 0.0,
32+
out: torch.Tensor | None = None,
33+
) -> torch.Tensor:
2834
"""Einstein summation with contraction planning.
2935
3036
Supports the same subscript notation as torch.einsum and numpy.einsum.
3137
38+
Args:
39+
subscripts: Einstein summation subscript string, e.g. ``"ij,jk->ik"``.
40+
*operands: Input tensors.
41+
alpha: Scalar multiplier applied to the contraction result (default 1.0).
42+
beta: Scalar multiplier applied to ``out`` before accumulation (default 0.0).
43+
out: Optional accumulation tensor. When provided the return value is
44+
``alpha * contract(operands) + beta * out``. Must have the same
45+
shape as the contraction result.
46+
3247
Examples:
3348
# Matrix multiply
3449
einsum("ij,jk->ik", A, B)
3550
51+
# Scaled GEMM: 2*A@B + 0.5*C (cuTENSOR-style alpha/beta)
52+
einsum("ij,jk->ik", A, B, alpha=2.0, beta=0.5, out=C)
53+
3654
# Batched matrix multiply
3755
einsum("bij,bjk->bik", A, B)
3856
39-
# Trace
40-
einsum("ii->", A)
41-
42-
# Outer product
43-
einsum("i,j->ij", x, y)
44-
4557
# DF-MP2 energy contraction
4658
einsum("iap,jbp->ijab", B, B)
47-
48-
# Tensor contraction (4-index transform)
49-
einsum("mi,mnP->inP", C, integrals)
5059
"""
5160
plan = plan_contraction(subscripts, *operands)
52-
return _execute_contraction(subscripts, operands, plan)
61+
result = _execute_contraction(subscripts, operands, plan)
62+
if alpha != 1.0:
63+
result = result.mul(alpha)
64+
if out is not None:
65+
result = result.add(out, alpha=beta)
66+
return result
5367

5468

5569
def _execute_contraction(subscripts: str, operands: tuple, plan: ContractionPlan) -> torch.Tensor:

0 commit comments

Comments
 (0)