Skip to content

← Matmul and einsum step 3 of 6

Medium Primitives

Subscripts instead of transposes

Jump to the task

einsum describes a tensor calculation by naming its axes. For example, this expression computes the dot product of every query and key in each batch item:

q: (batch, queries, features)
k: (batch, keys, features)

torch.einsum("bqd,bkd->bqk", q, k)
result: (batch, queries, keys)

Read the equation

The labels on each input correspond to its axes in order. In this example:

  • b identifies the same batch item in both inputs and the output.
  • q and k keep query and key positions separate.
  • d identifies matching features. It is absent from the output, so those products are summed over the feature axis.

The letters are labels you choose, not reserved names. The right side of -> specifies the output axes and their order. You do not need to transpose k yourself because the equation pairs the two feature axes explicitly.

import torch

q = torch.tensor([[[1.0, 0.0], [0.0, 2.0]]])
k = torch.tensor([[[1.0, 1.0], [2.0, 0.0], [0.0, 3.0]]])
out = torch.einsum("bqd,bkd->bqk", q, k)
print(out.tolist())  # [[[1.0, 2.0, 0.0], [2.0, 0.0, 6.0]]]

This is a dot-product score calculation without attention scaling or softmax. For these inputs it is equivalent to torch.bmm(q, k.transpose(1, 2)).

Other uses of the notation

An axis can be summed even if its label occurs only once: ij->i sums matrix rows. Repeating a label within one input selects a diagonal: ii->i returns a square matrix's diagonal. Neither case requires multiplying two tensors. Explicit output labels make these differences easier to see.

Choosing an implementation

Use the expression that makes the calculation clear, then benchmark if it is performance-sensitive. Eager einsum may perform several internal view or permutation operations around a matrix multiplication. An operation count alone does not tell you elapsed time or GPU kernel count.

Performance depends on shapes, memory layout, device and execution mode. Compilation can change the implementation, but does not guarantee that all overhead disappears or that einsum is faster than a direct matrix product.

Your task

def contract(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor

a has shape (batch, n, d) and b has shape (batch, m, d). Use torch.einsum to return shape (batch, n, m), where [x, i, j] is the dot product of a[x, i] and b[x, j].

The starter sums over n instead of d, then slices the feature axis. Its output has the requested shape when d >= n, but the values are still wrong in general. When d < n, even the shape is wrong.