Skip to content

Commit 25de104

Browse files
committed
feat: dtype mixed-precision override for einsum (#22)
1 parent eb766a6 commit 25de104

3 files changed

Lines changed: 90 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`dtype` mixed-precision override for `einsum`** (#22) — accepts string
13+
aliases (`"bf16"`, `"fp16"`, `"f32"`) or `torch.dtype` instances. When set,
14+
all operands are cast to the requested dtype before contracting and the
15+
result is returned in that dtype. Matches Neuron SDK autocast recommendations;
16+
use `dtype="bf16"` to route fp32 models through the NKI bf16 matmul path
17+
without changing the model's weight dtype.
18+
1019
## [0.3.0] — 2026-04-16
1120

1221
### Added

tests/test_einsum.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,48 @@ def test_defaults_unchanged(self):
246246
result = trntensor.einsum("ij,jk->ik", A, B, alpha=1.0, beta=0.0, out=None)
247247
expected = A @ B
248248
np.testing.assert_allclose(result.numpy(), expected.numpy(), atol=1e-5)
249+
250+
251+
class TestDtype:
252+
"""Tests for the dtype= mixed-precision override (#22)."""
253+
254+
def test_dtype_bf16_result(self):
255+
A = torch.randn(4, 3)
256+
B = torch.randn(3, 5)
257+
result = trntensor.einsum("ij,jk->ik", A, B, dtype="bf16")
258+
assert result.dtype == torch.bfloat16
259+
260+
def test_dtype_fp16_result(self):
261+
A = torch.randn(4, 3)
262+
B = torch.randn(3, 5)
263+
result = trntensor.einsum("ij,jk->ik", A, B, dtype="fp16")
264+
assert result.dtype == torch.float16
265+
266+
def test_dtype_torch_type(self):
267+
"""Passing a torch.dtype directly is equivalent to the string alias."""
268+
A = torch.randn(4, 3)
269+
B = torch.randn(3, 5)
270+
result = trntensor.einsum("ij,jk->ik", A, B, dtype=torch.bfloat16)
271+
assert result.dtype == torch.bfloat16
272+
273+
def test_dtype_none_unchanged(self):
274+
"""dtype=None leaves input dtypes unmodified."""
275+
A = torch.randn(4, 3)
276+
B = torch.randn(3, 5)
277+
result = trntensor.einsum("ij,jk->ik", A, B, dtype=None)
278+
assert result.dtype == torch.float32
279+
280+
def test_dtype_invalid_raises(self):
281+
with pytest.raises(ValueError, match="unknown dtype"):
282+
trntensor.einsum("ij,jk->ik", torch.randn(4, 3), torch.randn(3, 5), dtype="foo")
283+
284+
def test_dtype_correctness_bf16(self):
285+
"""bf16 matmul result is numerically close to fp32 reference."""
286+
import numpy as np
287+
288+
torch.manual_seed(0)
289+
A = torch.randn(8, 6)
290+
B = torch.randn(6, 10)
291+
result_bf16 = trntensor.einsum("ij,jk->ik", A, B, dtype="bf16").float()
292+
result_fp32 = trntensor.einsum("ij,jk->ik", A, B)
293+
np.testing.assert_allclose(result_bf16.numpy(), result_fp32.numpy(), atol=0.05)

trntensor/einsum.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,38 @@
2323

2424
from .plan import ContractionPlan, _parse_subscripts, plan_contraction
2525

26+
# Mapping of user-friendly dtype strings to torch.dtype
27+
_DTYPE_MAP: dict[str, torch.dtype] = {
28+
"bf16": torch.bfloat16,
29+
"bfloat16": torch.bfloat16,
30+
"fp16": torch.float16,
31+
"float16": torch.float16,
32+
"f32": torch.float32,
33+
"float32": torch.float32,
34+
"f64": torch.float64,
35+
"float64": torch.float64,
36+
}
37+
38+
39+
def _resolve_dtype(dtype: str | torch.dtype | None) -> torch.dtype | None:
40+
"""Resolve a dtype argument to a ``torch.dtype`` or ``None``."""
41+
if dtype is None:
42+
return None
43+
if isinstance(dtype, torch.dtype):
44+
return dtype
45+
try:
46+
return _DTYPE_MAP[dtype.lower()]
47+
except KeyError:
48+
raise ValueError(f"unknown dtype {dtype!r}; valid strings: {sorted(_DTYPE_MAP)}") from None
49+
2650

2751
def einsum(
2852
subscripts: str,
2953
*operands: torch.Tensor,
3054
alpha: float = 1.0,
3155
beta: float = 0.0,
3256
out: torch.Tensor | None = None,
57+
dtype: str | torch.dtype | None = None,
3358
) -> torch.Tensor:
3459
"""Einstein summation with contraction planning.
3560
@@ -43,20 +68,28 @@ def einsum(
4368
out: Optional accumulation tensor. When provided the return value is
4469
``alpha * contract(operands) + beta * out``. Must have the same
4570
shape as the contraction result.
71+
dtype: Optional compute dtype. When set, all operands are cast to this
72+
dtype before contracting and the result is returned in that dtype.
73+
Accepts ``torch.dtype`` instances or strings: ``"bf16"``,
74+
``"bfloat16"``, ``"fp16"``, ``"float16"``, ``"f32"``, ``"float32"``.
75+
Matches Neuron SDK autocast recommendations for Trainium.
4676
4777
Examples:
4878
# Matrix multiply
4979
einsum("ij,jk->ik", A, B)
5080
81+
# Force bf16 compute (e.g. to hit NKI bf16 matmul kernel)
82+
einsum("ij,jk->ik", A, B, dtype="bf16")
83+
5184
# Scaled GEMM: 2*A@B + 0.5*C (cuTENSOR-style alpha/beta)
5285
einsum("ij,jk->ik", A, B, alpha=2.0, beta=0.5, out=C)
5386
54-
# Batched matrix multiply
55-
einsum("bij,bjk->bik", A, B)
56-
5787
# DF-MP2 energy contraction
5888
einsum("iap,jbp->ijab", B, B)
5989
"""
90+
target = _resolve_dtype(dtype)
91+
if target is not None:
92+
operands = tuple(op.to(target) for op in operands)
6093
plan = plan_contraction(subscripts, *operands)
6194
result = _execute_contraction(subscripts, operands, plan)
6295
if alpha != 1.0:

0 commit comments

Comments
 (0)