mrkeyoor.com_
Wed 23 Sept 00:34 UTC
PyPIAI / MLupdated 21 Sept 2026

opt-einsum review

opt-einsum 3.4.0 chooses an execution order for Einstein summations, then dispatches the pairwise contractions to the array backend already holding your tensors. Changing that order can reduce arithmetic and the largest intermediate without changing the equation. Our import took 0.12 seconds, and the pure-Python package installed with no dependencies. Version 3.4.0 removed NumPy as a required dependency, added type annotations, moved the docs to MkDocs, accepted `backend=None`, added a `jaxlib` alias, and fixed dynamic-programming path failures involving very low memory limits and scalar-only contractions.

Verdict

opt-einsum 3.4.0 installed in 0.3 seconds as one 1 MB package and imported in 0.12 seconds in our sandbox, so path planning adds almost no installation baggage to an existing tensor stack. Use it for repeated 3-plus-operand contractions whose path and intermediate size you can measure; leave 2-operand math and whole-graph compiler scheduling to the backend.

We installed it

Lab card: what happened when we installed opt-einsumScreenshot of opt-einsum documentation
Install✓ · 0.3s1 package on disk · 1 MB
Importimport opt_einsum in 0.12s · pure Python · requires Python >=3.8
Known vulns0(pip-audit)

Answers from our run

Does opt-einsum install cleanly?

Yes. In a fresh container with an empty cache, pip install opt-einsum finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does opt-einsum need to run?

Python >=3.8, and nothing compiled: it is pure Python. In our run import opt_einsum succeeded in 0.12s.

opt-einsum or numpy: which should you use?

numpy: Use its built-in optimized einsum when every operand is already a NumPy array. opt-einsum 3.4.0 installed in 0.3 seconds as one 1 MB package and imported in 0.12 seconds in our sandbox, so path planning adds almost no installation baggage to an existing tensor stack.

When should you not use opt-einsum?

The work is ordinary matrix multiplication or a 2-operand einsum. There is no meaningful multi-step path to search.

API stability5/5Version 3.4.0 keeps the established `contract`, `contract_path`, `contract_expression`, explicit path, backend, `memory_limit`, and shared-intermediate contracts. Its release added annotations and allowed `backend=None` without changing equation syntax. The main caution is that cached paths depend on the supplied shape sizes, so API compatibility does not guarantee the same performance after dimensions change.
Docs5/5The MkDocs site separates path concepts, optimizer choices, reusable expressions, constant operands, backend dispatch, shared intermediates, custom optimizers, and the API reference. Examples print `PathInfo` so readers can see scaling, operation estimates, and intermediate counts. It also states that `optimal` search is factorial and that memory constraints may produce far more expensive paths.
Maintenance4/5GitHub reports 989 stars, 38 open issues and pull requests, an unarchived repository, and a push on March 19, 2026. PyPI 3.4.0 was released on September 26, 2024. That release removed the hard NumPy dependency, added annotations, updated docs, and fixed several path-parser and dynamic-programming errors, showing maintenance beyond packaging churn.
Ecosystem5/5The documented dispatch list includes NumPy, Dask, PyTorch, TensorFlow, JAX, CuPy, Sparse, Autograd, and other compatible arrays. NumPy has incorporated related einsum path optimization, and opt-einsum can accept external optimizer objects such as those from cotengra. The package supplies planning and dispatch only, so users still own each backend installation, compiler, and accelerator setup.

Use it if

  • An einsum has at least 3 operands and several legal pairwise orders with different intermediate sizes.
  • The same equation and shapes repeat often enough to cache a path or a `ContractExpression`.
  • One contraction API must dispatch across NumPy, PyTorch, JAX, TensorFlow, Dask, or CuPy arrays.
  • You need estimated operation counts and largest-intermediate figures before committing to an expensive run.
Skip it if

Setup reality

We installed opt-einsum 3.4.0 in 0.3 seconds with Python 3.12 in a fresh Bookworm sandbox. It left 1 package and 1 MB on disk. The wheel is pure Python, declares 0 direct dependencies, requires Python 3.8 or newer, and carries the MIT License. import opt_einsum worked in 0.12 seconds, while pip-audit found 0 known vulnerabilities. The distribution does not include a py.typed marker.

Version 3.4.0 no longer installs NumPy. You must supply NumPy, PyTorch, JAX, TensorFlow, CuPy, Dask, or another supported array system, plus any CUDA runtime and device configuration it needs. Backend inference normally follows operand types. No credentials or config file are required by opt-einsum itself.

Path search has its own cost. greedy is the cheap heuristic; optimal grows factorially as operands are added. Save the returned path or create a ContractExpression when shapes repeat. A path remains executable for compatible inputs, but changed dimension sizes can make its earlier ordering a poor choice.

memory_limit constrains intermediate element counts, not bytes, and the documentation warns that tight limits can greatly increase arithmetic. shared_intermediates() keeps tensor references alive until its cache is released. Backend conversion can also move arrays between host and accelerator, so operation estimates alone do not include transfer time, allocator behavior, dtype width, or compiler fusion.

Patterns

Contract two NumPy arrays contract-tensors

import numpy as np
import opt_einsum as oe

a = np.random.rand(20, 30)
b = np.random.rand(30, 40)
c = oe.contract('ij,jk->ik', a, b)

With 2 operands there is no multi-step order to optimize; use this form mainly for API consistency across larger equations.

Pick the greedy optimizer choose-greedy-path

result = oe.contract(
    'ab,bc,cd,de->ae', a, b, c, d,
    optimize='greedy',
)

`greedy` searches quickly and may miss a cheaper path. Compare `auto-hq` when the same 4-operand equation runs many times.

Print the planned contraction steps inspect-contraction-path

path, info = oe.contract_path(
    'ab,bc,cd->ad', a, b, c,
    optimize='auto-hq',
)
print(path)
print(info)

`PathInfo` contains estimated operations and element counts. It does not measure accelerator time or allocator bytes.

Plan from shape tuples plan-from-shapes

path, info = oe.contract_path(
    'ab,bc,cd->ad',
    (1000, 64), (64, 64), (64, 1000),
    shapes=True, optimize='greedy',
)

When `shapes=True`, pass a tuple for every operand. Do not mix live arrays into the same planning call.

Apply one saved path repeatedly reuse-explicit-path

path, _ = oe.contract_path('ab,bc,cd->ad', a, b, c, optimize='auto-hq')

for a_batch in batches:
    result = oe.contract('ab,bc,cd->ad', a_batch, b, c, optimize=path)

Reusing the list avoids another search. Recompute it when dimension sizes change enough to alter intermediate costs.

Compile a shape-specific expression compile-reusable-expression

expr = oe.contract_expression(
    'ab,bc,cd->ad',
    (100, 64), (64, 64), (64, 20),
    optimize='auto-hq',
)
result = expr(a, b, c)

The callable is planned against these 3 shape tuples; a different size profile can keep working while running inefficiently.

Precompute constant sides of an expression precompute-constant-tensors

expr = oe.contract_expression(
    'ab,bc,cd->ad',
    a, (64, 64), c,
    constants=[0, 2], optimize='auto-hq',
)
result = expr(b)

Positions 0 and 2 receive arrays because they are constants. The expression retains them, which extends their memory lifetime.

Run on CUDA tensors through PyTorch use-torch-backend

import torch
import opt_einsum as oe

a = torch.randn(32, 64, device='cuda')
b = torch.randn(64, 16, device='cuda')
result = oe.contract('ab,bc->ac', a, b, backend='torch')

The opt-einsum wheel has 0 dependencies, so PyTorch and a working CUDA runtime must already be installed.

Write the final result into an array write-into-output

out = np.empty((a.shape[0], c.shape[1]))
oe.contract('ab,bc->ac', a, c, out=out)
assert out.shape == (a.shape[0], c.shape[1])

`out` avoids allocating the final result only. Pairwise steps can still allocate intermediates.

Limit intermediate element counts limit-intermediate-size

result = oe.contract(
    equation, *operands,
    optimize='auto-hq',
    memory_limit='max_input',
)

`max_input` is measured in elements rather than bytes, and the selected path may trade much more arithmetic for that cap.

Share repeated pairwise results share-intermediate-results

with oe.shared_intermediates() as cache:
    left = oe.contract('ab,bc,cd->ad', a, b, c)
    right = oe.contract('ab,bc,ce->ae', a, b, e)
print(f'cached operations: {len(cache)}')

The context keeps intermediate tensors referenced until exit. Inspect the cache length and peak memory before using this in a long batch.

Use integer labels for generated equations use-integer-index-labels

result = oe.contract(
    a, [0, 1],
    b, [1, 2],
    [0, 2],
)

Interleaved syntax pairs each operand with an index list and puts output indexes last, avoiding single-letter label limits.

Alternatives

PackageRegistryPick it when
numpyPyPIUse its built-in optimized einsum when every operand is already a NumPy array.
cotengraPyPIUse it for hypergraph path search on large tensor networks.
einopsPyPIUse it when named reshaping and axis rearrangement are the actual problem.
tensornetworkPyPIUse it when the code should model a tensor network explicitly as well as contract it.

More ai / ml guides

openai · mcp · huggingface-hub · scikit-learn · tiktoken · @modelcontextprotocol/sdk · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.