einexpr
An experimental Python library for named tensor dimensions and Einstein-style array programming.
einexpr is a Python framework I wrote for working with named tensor dimensions. You can write the dimension names directly on arrays and use them to align, contract, rename and group dimensions.
import einexpr as ei
a = ei.array([1, 2])
b = ei.array([3, 4])
X = ei.array([[1, 2], [3, 4]])
Y = ei.array([[5, 6], [7, 8]])
dot = (a['i'] * b['i'])['']
outer = (a['i'] * b['j'])['i j']
product = (X['i j'] * Y['j k'])['i k']
The names determine how dimensions line up and which ones are contracted. In the matrix product, j appears in both inputs and disappears from the result. The expression does not depend on j occupying a particular axis.
Multi-head attention
Here is multi-head attention written both ways. PyTorch uses reshapes, transposes and axis numbers. einexpr names the dimensions on each array instead. t->s renames the key/value position before the contraction, and the result names say which dimensions remain.
The package also supported grouped dimensions. For example, context['b t (h d)'] joins the head and per-head feature dimensions while keeping the grouping explicit in the expression.
q = (x @ w_q.reshape(C, H * D)).reshape(B, T, H, D).transpose(1, 2)
k = (x @ w_k.reshape(C, H * D)).reshape(B, T, H, D).transpose(1, 2)
v = (x @ w_v.reshape(C, H * D)).reshape(B, T, H, D).transpose(1, 2)einexpr q = (x['b t c'] * w_q['c h d'])['b h t d']
k = (x['b t c'] * w_k['c h d'])['b h t d']
v = (x['b t c'] * w_v['c h d'])['b h t d']scores = (q @ k.transpose(-2, -1)) / math.sqrt(D)einexpr scores = (q['b h t d'] * k['b h t->s d'])['b h t s'] / math.sqrt(D)max_scores = scores.amax(dim=-1, keepdim=True)
weights = torch.exp(scores - max_scores)
weights = weights / weights.sum(dim=-1, keepdim=True)einexpr max_scores = ei.max(scores, axis='s')
weights = ei.exp(scores - max_scores)
weights = weights / ei.sum(weights, axis='s')context = (weights @ v).transpose(1, 2).reshape(B, T, H * D)
y = context @ w_o.reshape(H * D, C)einexpr context = (weights['b h t s'] * v['b h t->s d'])['b t h d']
y = (context['b t h d'] * w_o['h d c'])['b t c']Coda
A whole Transformer block, for fun
The EinsteinTensors notebooks attempted something like this in 2021, but the equations were unfinished. This is a corrected reconstruction: a post-layer-normalisation causal Transformer block, with dropout omitted.
The mathematics and code below are each meant to stand on their own. Hover—or keyboard-focus—any equation or code line to mark its counterpart in the other block.
einexpr
# Attention
q = (x['b t c'] * w_q['c h d'])['b h t d'] + b_q['h d']
k = (x['b t c'] * w_k['c h d'])['b h t d'] + b_k['h d']
v = (x['b t c'] * w_v['c h d'])['b h t d'] + b_v['h d']
scores = (
q['b h t d'] * k['b h t->s d']
)['b h t s'] / math.sqrt(D)
scores = scores + mask['t s']
weights = ei.exp(scores - ei.max(scores, axis='s'))
weights = weights / ei.sum(weights, axis='s')
context = (
weights['b h t s'] * v['b h t->s d']
)['b t h d']
attention = (
context['b t h d'] * w_o['h d c']
)['b t c'] + b_o['c']
# Residual and layer norm
u1 = x + attention
mean_u = ei.mean(u1, axis='c')
var_u = ei.mean((u1 - mean_u) ** 2, axis='c')
u0 = (
gamma_u['c'] * (u1 - mean_u) / ei.sqrt(var_u + eps)
+ beta_u['c']
)
# Feed-forward network
hidden = gelu(
(u0['b t c'] * w_1['c m'])['b t m'] + b_1['m']
)
ff = (hidden['b t m'] * w_2['m c'])['b t c'] + b_2['c']
# Residual and layer norm
z1 = u0 + ff
mean_z = ei.mean(z1, axis='c')
var_z = ei.mean((z1 - mean_z) ** 2, axis='c')
z0 = (
gamma_z['c'] * (z1 - mean_z) / ei.sqrt(var_z + eps)
+ beta_z['c']
)Parameter initialisation and setup Show the code hidden behind the main block
import math
import numpy as np
import einexpr as ei
B, T, C = 2, 8, 64
H, D, M = 4, 16, 256
eps = 1e-5
rng = np.random.default_rng(0)
def tensor(shape, dims):
return ei.asarray(rng.normal(size=shape), dims=dims)
def gelu(x):
return 0.5 * x * (
1.0
+ ei.tanh(
math.sqrt(2.0 / math.pi)
* (x + 0.044715 * x**3)
)
)
x = tensor((B, T, C), 'b t c')
w_q = tensor((C, H, D), 'c h d')
b_q = tensor((H, D), 'h d')
w_k = tensor((C, H, D), 'c h d')
b_k = tensor((H, D), 'h d')
w_v = tensor((C, H, D), 'c h d')
b_v = tensor((H, D), 'h d')
w_o = tensor((H, D, C), 'h d c')
b_o = tensor((C,), 'c')
gamma_u = ei.ones((C,), dims='c')
beta_u = ei.zeros((C,), dims='c')
w_1 = tensor((C, M), 'c m')
b_1 = tensor((M,), 'm')
w_2 = tensor((M, C), 'm c')
b_2 = tensor((C,), 'c')
gamma_z = ei.ones((C,), dims='c')
beta_z = ei.zeros((C,), dims='c')
mask = np.where(
np.arange(T)[None, :] <= np.arange(T)[:, None],
0.0,
-np.inf,
)
mask = ei.asarray(mask, dims='t s')
The displayed program was run against the archived package and compared with an independent NumPy implementation of the same block; the maximum difference was 1.27e-14.