# Graphical tensor notation for interpretability

\[ This post is now on arXiv too: [https://arxiv.org/abs/2402.01790](https://arxiv.org/abs/2402.01790) \]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/a59be0e8f5f66cfe05c1b414e2bd86ada87f87686dfae0c4.png)

Some examples of graphical tensor notation from the [QUIMB python package](https://quimb.readthedocs.io/en/latest/index.html)

Deep learning consists almost entirely of operations on or between tensors, so easily understanding tensor operations is pretty important for interpretability work.[^bwj2f888xxm] It's often easy to get confused about which operations are happening between tensors and lose sight of the overall structure, but graphical notation[^bytowdrb3ck] makes it easier to parse things at a glance and see interesting equivalences.

The first half of this post introduces the notation and applies it to some decompositions (SVD, CP, Tucker, and tensor-network decompositions), while the second half applies it to [A Mathematical Framework for Transformer Circuits](https://transformer-circuits.pub/2021/framework/index.html). Most of the first half is adapted from my physics PhD thesis introduction, which is partly based on existing explanations such as in [the math3ma blog](https://www.math3ma.com/blog/matrices-as-tensor-network-diagrams), [Simon Verret's blog](https://simonverret.github.io/2019/02/16/tensor-network-diagrams-of-typical-neural-network.html), [tensornetwork.org](https://tensornetwork.org/diagrams/), [tensors.net](https://www.tensors.net/p-tutorial-1), [Hand-waving and Interpretive Dance: An Introductory Course on Tensor Networks](https://arxiv.org/abs/1603.03039), [An Intuitive Framework for Neural Learning Systems](https://arxiv.org/abs/2303.13565), or [a talk I gave in 2021](https://drive.google.com/file/d/1ycctAkXlN3-JrsTZM8TaAZDzTJum_EqX/view). Feel free to scroll around and look at interesting diagrams rather than reading this post start to finish.

Tensors
=======

Practically, tensors in our context can just be treated as arrays of numbers.[^ytaychzpt3] In graphical notation (first [introduced by Roger Penrose in 1971](http://homepages.math.uic.edu/~kauffman/Penrose.pdf#page=5)), tensors are represented as shapes with "legs" sticking out of them. A vector can be represented as a shape with one leg, a matrix can be represented as a shape with two legs, and so on. I'll also represent everything in PyTorch code for clarity.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/435aa244c2a3a8fafed9774d50b24e3c91a33023bbb66347.png)

Each leg corresponds to an index of the tensor - specifying an integer value for each leg of the tensor addresses a number inside of it:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/89bfd35e6ed2f7b5e33730d9d513f0f32fb03f725fa2ee3a.png)

where $0.157$ happens to be the number in the $(i=0,\,j=2,\,k=0)$ position of the tensor $A$. In python, this would be `A[0,2,0]`. The amount of memory required to store a tensor grows exponentially with the number of legs,[^13ccdi9zt3v] so tensors with lots of legs are usually represented only implicitly: decomposed as operations between many smaller tensors.

Operations
==========

The notation only really becomes useful when things get more complicated, but let's start as simple as possible. Multiplying two numbers together (`y = a * b`) in graphical tensor notation just involves drawing them nearby:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/644e328e026190d6a9b66d561046ca1a7395e4100587a5f5.png)

The next easiest thing to represent in this notation is a bit more obscure: the outer product between two vectors (`t.outer(a, b)` or `einsum(a, b, 'i, j, -> i j')`). Known more generally as a tensor product, this operation forms a matrix out of the vectors, where each element in the matrix is a product of two numbers: $Y_{i\,j}=a_ib_j$ or `Y[i,j] = a[i] * b[j]` (for example `Y[0,0] = a[0] * b[0]`,  `Y[1,0] = a[1] * b[0]` and so on). Simply drawing two tensors nearby implies a tensor product:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/c793725d998b246d0ea59647b813599db262e0fc18270377.png)

The next simplest example will probably be more familiar: the dot product between two vectors: `t.dot(a,b)` or `a @ b` or `einsum(a, b, 'i, i, -> ')`, which can be represented by connecting the legs of two vectors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/6bf42bdf8f02c80a5ce925f90eacf06d86b82a7dbc01f69a.png)

Connected legs like this indicate that two tensors share the same index, and a summation is taken over that index. Here the result is a single number, formed from a sum of products: $y = \sum_i a_ib_i$ or `y = a[0] * b[0]  +  a[1] * b[1]  +  a[2] * b[2] + ...`

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/c7bf3e9661d5e86703a44c997eba5fae6115868b8bfe68cb.png)

Connecting legs like this is known more generally as tensor contraction or Einstein summation.  Let's take a look at all of the most common kinds of contractions between vectors and matrices:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/3fc26abfb5710700640104ff838148e1c60a18c9d50faa9a.png)

In every case you can tell how many legs the resulting tensor will have by the number of uncontracted "free'' legs on the left. 

We can also represent single-tensor operations, such as the transpose of a matrix:

| Graphical notation | einops / PyTorch |
| --- | --- |
| ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/744f134fd0fdb6b1bc56a065cf66fcf846a49490d14186ae.png) | `rearrange(A, 'i j -> j i')` or `A.transpose(0, 1)` |

the rearranging of tensor indices:

| Graphical notation | einops / PyTorch |
| --- | --- |
| ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/b3ea4bdccfaa61fc15d3516833c602f7b3bfb95824365956.png) | `rearrange(T, 'i j k l -> i k j l')` or `T.transpose(1, 2)` |

and the reshaping (flattening) of a tensor into a matrix by grouping some of its legs:

| Graphical notation | einops / PyTorch |
| --- | --- |
| ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/c7c845d96943f881d2e2d56631715ff7404f5c8bbcf9471c.png) | `rearrange(T, 'i j k l -> (i l) (k j)')` or `T.transpose(1,3).reshape((T.shape[0]*T.shape[1], T.shape[2]*T.shape[3]))` |

where thicker lines are used to represent legs with a larger dimension. Of course you can also split legs rather than grouping them:

| Graphical notation | einops |
| --- | --- |
| ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/e0eb0faa3df2e34dbd1e3bc6f77b263aec03f03bf22f6c17.png) | `rearrange(M, 'i (j k) -> i j k', j=int(np.sqrt(M.shape[-1]))))` |

Various relationships also become intuitive in graphical notation, such as the cyclic property of the trace  $\mathrm{Tr}(AB)=\mathrm{Tr}(BA)$:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/705dc2a5b5ad0a230b3d3bf5a1a790dea8ee441c55347331.png)

Or if you prefer transposes rather than upside-down tensors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/388cff923240f55f9972f6b455d6126e631043276a21daf4.png)

But graphical notation is most useful for representing unfamiliar operations between many tensors. One example in this direction is  $\sum_{\alpha\beta} A_{i\alpha\beta} \, v_{\beta} \, B_{\alpha \beta j} = M_{ij},$ which can be represented in graphical notation as

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/aa74e96e3116f9e85409364edacb0455ed75c3bf20cc2d57.png)

or in einops as `M = einsum(A,v,B,'i α β, β, β α j -> i j')`. The middle part of the graphical notation here shows that the number in each $i$, $j$ position of the final matrix can be calculated with a sum over every possible indexing of the internal legs $\alpha$ and $\beta$, where each term in the sum consists of three numbers being multiplied (though in practice the contraction should be calculated in a much more efficient way). 

Graphical notation really comes into its own when dealing with larger networks of tensors. For example, consider the contraction 

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/2e8728c99bcb54904ab50d816ae32ab5d5fca33ee12314b2.png)

which is tedious to parse: indices must be matched up across tensors, and it is not immediately clear what kind of tensor (eg. number, vector, matrix ...) the result will be. Needless to say, the einsum code is about as bad: `einsum(A,V,B,W,C,X,D,Y,E,Z,'i j, i r, j k l, r k s, l m n, s m t, n o p, t o u, p q, u q -> ')`. But in graphical notation this is 

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/4a3945bc88a0ee3d0d014ed660f9ddc720872708fe19f0bb.PNG)

and we can immediately see which tensors are to be contracted, and that the result will be a single number. Contractions like this can be performed in any order. Some ways are much more efficient than others,[^h1buayt8oo] but they all get the same answer eventually.

Tensor networks (einsums) like this also have a nice property that, if the tensors are independent (not copies or functions of each other), then a derivative of the final result with respect to one of the tensors can be calculated just by "poking a hole" and removing that tensor:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/08b85c4f24ed9244fcc7ef110e2118fe04eed06367fb45df.PNG)

this is because einsums are entirely linear (or multilinear, at least).

Special kinds of tensors
========================

Different kinds of tensors are often drawn using different shapes. Firstly, it's common to represent an identity matrix as a single line with no shape in the middle:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/67225ea851451f8f1e6ab4e3c72c0dcae8d85c2f5397186a.png)

(or `t.eye(2)`) where we have used the delta notation $\delta_{ij}$ because the elements of the identity matrix are equivalent to the Kronecker delta of the indices: $1$ if $i=j$ and zero otherwise. You can also extend this notation to the three-leg delta tensor, which has ones only along the $i=j=k$ diagonal:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/2757b625a0be53eb2a9fcd12ab3893831cbae5a36d92ddad.png)

and so on for delta tensors with more legs. Among other things, this lets us represent diagonal matrices using vectors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/2d8bb7ac1d8f111bda16c24f36506ed537594915010dfa77.png)

where the vector in grey contains just the elements on the diagonal. Typically though, it would be inefficient to actually code a contraction with a delta tensor made of actual numbers: it's much faster to just rearrange or reindex the relevant data directly. Still, whenever you see any line in a tensor network diagram, you can imagine a delta tensor implicitly sitting there. 

Triangles are often used to represent isometric matrices: linear maps which preserve the lengths of vectors (eg. performing a rotation), even if they might embed these vectors into a larger-dimensional space.  A matrix $V$ is isometric if it can be contracted with its own (conjugate) transpose to yield the identity matrix. Graphically, 

| Graphical notation | Math | PyTorch |
| --- | --- | --- |
| ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/1e1d7f98bb48265932128790c3420dd24db9aa6261bf4b13.png) | $V^\\dagger V = I$ | `t.transpose(t.conj(V)) @ V ` `== t.eye(V.shape[-1])` |

where the tip of the triangle points towards the smaller dimension. However the reverse is not true when the matrix is not square, because some vectors will inevitably get squashed when mapping from high to low dimensions:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/4757ecfe6be7d7e8bc1385c428c41c5020e82f572d534f66.png)

Square isometries are known as orthogonal matrices (or unitary matrices if they contain complex numbers), and are often represented with squares or rectangles:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/767ea5bfd4c45ae1594fe6a9e98596bae77dc479a614aab1.png)

When isometries have more than just two legs, their legs can be grouped by whether they go into the edge or the tip of the triangle, and similar relationships hold:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/0856edeb725bbf94bbddd273f1b69aa2cc4dfbb3157c58e0.png)

Finally, here's a silly looking related graphical equation:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/0b3d8906cf896dca174d808b069bd0b298a36d1f12d682e1.png)

It says that the flattened tensor product (Kronecker product) of two identity matrices is another identity matrix. In pytorch / einops, this is   
`t.kron(t.eye(5), t.eye(3)) == t.eye(5*3)` or   
`einsum(t.eye(5), t.eye(3), 'i j, k l -> (i k) (j l)') == t.eye(5*3)`.[^9fzq2gk6z18]   
 

Decompositions (SVD, CP, Tucker)
================================

*\[Feel free to skip to the* [*section on neural networks*](https://www.lesswrong.com/posts/BQKKQiBmc63fwjDrj/graphical-tensor-notation-for-interpretability#Neural_networks)*\]*

The Singular Value Decomposition (SVD) allows any matrix $M$ to be decomposed as $M=UDV^\dagger$, where $U$ and $V$ are isometric matrices, and $D$ is a diagonal matrix:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/a3d5562396c7b7995d559c0dcca95bd4097466aef3763053.png)

where $\lambda$ is the vector of non-negative *singular values* making up the diagonal elements of $D$. Or in torch / einops: `U, λ, V = t.svd(M)` and  
`t.allclose(M, einsum(U, λ, t.conj(V), ' i j, j, k j -> i k' ))`. 

There are many (at least [six and a half](https://www.lesswrong.com/posts/iupCxk3ddiJBAJkts/six-and-a-half-intuitions-for-svd)) intuitive ways of thinking about the SVD. Geometrically, the SVD is often thought of as decomposing the linear transformation $M$ into a "rotation'' $V^\dagger$, followed by a scaling $D$ of the new basis vectors in this rotated basis, followed by another "rotation'' $U$. However in the general case where $U$ and $V^\dagger$ are complex-valued isometries rather than just rotation matrices, this geometric picture becomes harder to visualize.

Instead, it is also useful to think of the SVD as sum of outer products of vectors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/8e7ef18eb663ef87482b48a61b238df2fe83aba8545a3b09.png)

where $\textbf{u}_1,\, \textbf{u}_2,\, \dots$ are the orthonormal vectors from the columns of $U$, and $\textbf{v}^\dagger_1,\, \textbf{v}^\dagger_2,\, \dots$ are the orthonormal vectors from the rows of $V^\dagger$.   
  
The size of each singular value $\lambda_i$ corresponds to the importance of each corresponding outer product. The number of nonzero singular values is known as the "rank" of the matrix $M$. When some singular values are sufficiently close to zero, their terms can be omitted from the sum, lowering the rank of $M$. The effects of this low-rank approximation can be seen by treating an image as a matrix, and compressing it by performing an SVD and discarding the small singular values:[^m5ocsq6ut5j]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/d281a37f7b6c913e77a311d6179571512281b44ca371bec0.png)

A matrix can be compressed by performing a singular value decomposition and discarding the smallest singular values. Here I treat an image as a matrix, and perform various levels of truncation, with the discarded singular values shown in the red shaded regions of the plot. (a) shows just one singular value kept: the matrix is approximated as a single outer-product of two vectors, scaled by the first singular value. (b) shows 7 singular values, (c) 30, and (d) 100 kept out of the 200 singular values in the full decomposition.

In fact, performing an SVD and keeping only the largest $k$ singular values $\lambda_1 \cdots \lambda_k$provides the best possible rank-$k$ approximation of the original matrix $M$. This is known as the Eckart–Young theorem, and is true regardless of whether the "best'' approximation is defined by the spectral norm, the Frobenius norm, or any other unitarily invariant norm.[^tsvsfrkezh] The error in this approximation is determined by the total weight of the singular values thrown away.

General tensors can also be decomposed with the SVD by grouping their legs, forming a bipartition of the tensor:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/10fa694c2ef32858637ad8b13c1153766972de0295c33dc8.png)

The SVD also has some higher-order generalizations, such as the CP and Tucker decompositions. These decompositions work directly on tensors with any number of legs, without requiring that legs be grouped into an effective matrix. The simplest generalization of the SVD is the CP (Canonical Polyadic or CANDECOMP/PARAFAC [^gub7c5mcstg]) decomposition, which extends the SVD pattern naturally to more legs

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/480d3305fa459726d90997151914826c413d94a427e6b546.png)

`T = t.rand((2,3,4))`  
`λ, [U, V, W] = tensorly.decomposition.parafac(T, rank=9, tol=1e-12)`  
`O = einsum(λ, U, V, W, 's, i s, j s, k s -> i j k')`  
`t.allclose(T, O, rtol=1e-3)`  
  
Whereas the Tucker decomposition is a relaxation of the CP decomposition where the core tensor is not restricted to be diagonal[^jxtzw5zwo8]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/7447abe3865ff08c6a926b072313061a1edac2d8c469d459.png)

`T = t.rand((10,10,10))`  
`C, [U, V, W] = tensorly.decomposition.tucker(T, rank=(5,5,5), n_iter_max=10000)`  
`O = einsum(C, U, V, W, 'a b c, i a, j b, k c -> i j k')`  
The restriction to isometric matrices is also often relaxed in these decompositions (replacing the triangles with circles).

Sadly, these decompositions are not as well behaved as the SVD. Even determining the CP-rank of a tensor (the minimum number of nonzero singular values $\lambda_i$) is an NP-hard problem,[^nw86qlujp7] so the rank must usually be manually chosen rather than automatically found. Calculating these decompositions usually also requires iterative optimization, rather than just a simple LAPACK call.  
 

Tensor network decompositions
=============================

Tensor networks are low-rank decompositions in exponentially-large dimensional spaces. The most common example is a Matrix Product State, also known as a tensor train.[^fmyqacdyvzw] It consists of a line of tensors, each tensor having one free "physical'' leg, as well as "bond'' legs connected its neighbors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/0a0225a6a9f1fa2427ab28faaeb4f069702954ac68f90105.png)

Tensor trains are low rank decompositions of exponentially large dimensional spaces: contracting and flattening a tensor train produces an exponentially large dimensional vector. 

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/cda5297da08704fe8a240dfca0eaae8e7048583c1dc79380.png)

A single large tensor like this can be decomposed into a tensor train using a series of SVDs:  
![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/41573d01c47dbc9d17cd5de7b1844a0185f825a40da72724.png)  
 where each SVD needs only to be taken on the grey tensor rather than the whole network - the isometries in blue already form an orthonormal basis from the left, so taking an SVD of the grey tensor alone is equivalent to taking an SVD of the full network. In practice however, it's common to work directly in tensor network format from the start, rather than starting with a single extremely large tensor. Regardless, if all of the tensors are made isometric[^7wayrajbdzx] so that they point towards some spot, then the whole network is equivalent to an SVD around that spot:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/eae7dd18feec183fac817658fc2b84f231ba20d84d822cc2.png)

and you get all of the benefits that come with the singular value decomposition, such as (sometimes) interpretable dominant singular vectors, optimal compression by discarding small singular values, etc. Having a well defined orthogonality center like this also has many other advantages, such as fixing the "gauge freedom'' in a tensor network.[^vvx1l7fra8]

Of course, this low-rank tensor train decomposition is only possible when only a few dominant singular vectors in each SVD are important - most singular values in each SVD must be small enough to be discarded. Otherwise, the bond dimension will grow exponentially away from the edges of the network. 

As a result, these decompositions work best when "correlations" between different sites far apart in the tensor network (that can't be explained by correlations with nearby sites) are relatively weak. In quantum physics for example, tensor networks are good at representing wavefunctions which don't have too much long-range entanglement.[^k90gzolb6j]

There are many tensor networks commonly used in physics:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/529414ce3ab623f4d76c4094084c8dbfe68238fbc0be7601.png)

Image adapted from [tensornetwork.org](https://tensornetwork.org/) 

Neural networks
===============

The problem with tensor network notation is that it was developed for quantum physics, where things are extremely simple. No, seriously: in quantum mechanics, `einsum` is all you need - no nonlinearities are allowed, [not even copying](https://en.wikipedia.org/wiki/No-cloning_theorem). So neural networks require going slightly beyond the standard graphical tensor notation, in order to represent nonlinearities.[^d3bbi58ho5]

Dense neural networks 
----------------------

Without nonlinearities, dense neural networks are equivalent to a bunch of matrices multiplied together - one for each weight layer. The data $x$ is input as a vector, which contracts with the matrices to yield the output vector $y$:[^1g1fyzr8cdph]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/28a8c3f49e0d70b3cd0b4f02b7375085bbc2f1da6d579c4b.png)

Without nonlinearities, this contraction can be performed in any order. In fact it's equivalent to multiplying just a single weight matrix, as the contraction of weight matrices can be computed independent of the input vector $x$:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/bd42548f3123fafa8a96f21b5a3f0d6d7f95a6d777eb42e3.png)

Adding nonlinear functions, we introduce "bubbles": everything within the bubble must first be contracted, and then the nonlinear function can be applied to the single remaining tensor inside the bubble. As a result, these nonlinearities induce a fixed contraction order:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/3c643e9e26cc2d4ea4fb79a3bf2c25053b26507d6d850cb7.png)

First the input vector $x$ must be contracted with the first weight matrix $W_1$, and only then can the elementwise nonlinearity $\sigma_1$ be applied, and so on.   
 

Transformers
============

Transformers are being used for the largest and most capable AI systems, so they're an important focus for interpretability work. Transformers are traditionally used for sequence-to-sequence tasks, such as turning one string of words (or "tokens") into another. We'll explore them in graphical notation, with a focus on illustrating some of properties elucidated in [A Mathematical Framework for Transformer Circuits](https://transformer-circuits.pub/2021/framework/index.html).

So here's a tensor network diagram for GPT-2, with non-einsum operations shown in pink and green:![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/d2185a72f7ba4cbd09793418e1baa289de56cc2565a5584c.png)

We see that the structure of the transformer is a series of distinct parts or "blocks". First, an embedding block, then "Attention" and "MLP" blocks alternate for many layers, then an unembedding block. Let's flip the diagram around and view it vertically so we have more room to label the legs and see what's going on:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/af7066a6d3182a5ccfaa901cbc8dda150d9658dc4b74c076.png)

Here we've also changed how we denote the elementwise addition of tensors (in blue), to emphasize the most dominant part of a transformer: the vertical "backbone" on the left known as the residual stream. This is the main communication channel of the network. Each layer copies out information from the residual stream, uses it, and then adds new transformed information into (or subtracts it out of) the residual stream. 

Here are the dimensions and descriptions of each leg in the above diagram for GPT-2: 

| Leg | Dimension in GPT-2 | Description |
| --- | --- | --- |
| `** seq pos **` | up to context length (1024) | The number of tokens in the input text. (Indexes which token in the input text) |
| `** vocab **` | 50257 | The number of tokens in the vocabulary of all possible tokens (Indexes tokens by their token ID). |
| `** hidden **` | 768 | The dimension of the residual stream on each token (space for information stored on that token) |
| `** num heads **` | 12  | The number of attention heads per attention layer |
| ` **head size **` | 64  | The compressed dimension of each attention head |

Input and output
----------------

The input data ![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/fa17bc62009f48228b9bb9bda54a10aae0aebcb354cccf1d.png) can be seen as a map from each position in the input text to the ID of the token at that position: $x =\delta_{\text{seq_pos},\; \text{corresponding_token_ID}}$. Like most delta-tensors it's computationally cheaper to use clever indexing rather than actually representing it as a tensor, but here's what it looks like regardless:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/95f69324b2cc1f1239b1348085cc0489ef4ff2c00e250f90.png)

The output tensor $y$ (found by contracting the input with the whole network) has the same shape, but in general every entry will be nonzero, as it represents the log probability that the model predicts for every possible next token at every position. The vector at the final token position represents the log probability distribution for the unknown next word, which can be sampled to turn the predictive model into a generative model.

The embedding layer is responsible for getting the input into the model, by compressing (projecting) the input vector on each token from a $\dim($`**vocab**`$)\approx50,000$ dimensional space down to a $\dim($`**hidden**`$)\approx768$ dimensional space known as the residual stream of each token:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/ca0f80da3c60d49e43a6d716b880ef046b6c0ed31104529b.png)

The total dimension of the residual stream is $\dim($`**seq pos**`$)\times \dim($`**hidden**`$)$: there is one stream for each input token, and each token's stream is of dimension $\dim($`**hidden**`$)$. 

Presumably this process of embedding involves creating a vector representing the token's meaning independent of any context from the tokens around it, by packing similar tokens into similar parts of residual-stream space (though [superposition](https://dynalist.io/d/n2ZWtnoYHrU1s4vnFSAQ519J#z=3br1psLRIjQCOv2T4RN3V6F2) may also be involved). 

The embedding also has another component: the positional embedding. This is a simple fixed vector for each position, added to each residual stream independent of the token at that position. It lets each residual stream have some information about where in the sequence it is located, as long as a subspace in the residual stream is reserved to store and use this positional information.

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/cbc595f261931b911a0d15e88596a7ff9e752e6a21233b52.png)

However different transformer architectures tend to use very different kinds of positional embedding techniques, so for simplicity throughout the rest of the sections we'll contract the input and the embeddings into a single tensor:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/9fd9c852b0a3abfa0dc6f781cb1d29cd8ead964c4847e3d3.png)

but feel free to substitute in the expanded form whenever you like.

The unembedding matrix works similarly, but without any positional component.

The structure of the transformer from here is a series of "blocks" which copy from, then add back into the residual stream. There are two kinds of blocks: Attention and MLP, which alternate down through the layers. 

MLP
---

The MLP layer is most like a dense neural network, though it is residual (copying from and adding into the residual stream, rather than modifying it directly), and acts only on the `**hidden**` index. Unlike an MLP layer, a general transformation would be able to move information between token positions, by also acting on the `**seq pos**` index like so:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/edf99419facb0f4be33c5e2b961fcd622885abd5ee58e497.png)

but MLP layers in transformers only involve contractions onto the `**hidden**` leg, and so act independently on each token, meaning that they can't move information from one residual stream to another: 

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/07e8def66c7ecc0bd6a01027f3bd9a9c60c43d92f2a67abf.png)

Despite acting independently on each residual stream, MLPs make up the vast majority of the parameters in a transformer because they project up to a higher-dimensional space with the $W_\uparrow$ matrix[^jt0n10eo4b] before applying a GELU nonlinearity and projecting back down again with $W_\downarrow$. The MLP parameters in $W_\uparrow$ and especially $W_\downarrow$ seem to be where most of the trained facts and information are stored, as evidenced by [Transformer Feed-Forward Layers Are Key-Value Memories](https://aclanthology.org/2021.emnlp-main.446/), the [ROME (Rank One Model Editing) paper](https://rome.baulab.info/) and subsequent work on [activation patching](https://dynalist.io/d/n2ZWtnoYHrU1s4vnFSAQ519J#z=qeWBvs-R-taFfcCq-S_hgMqx) for locating and editing facts inside transformers.

Attention
---------

Each attention layer consists of a number of heads, which are responsible for moving relevant data from the residual stream of preceding tokens, transforming it, and copying it into the residual stream of later tokens. Each head acts effectively independently - we can see this in our diagram by replacing the `**num heads**` leg with a sum, and seeing that each term contributes independently to the residual stream:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/5ab66bcdd35b4147de5bc0fd1094f5ddcc783f8ab23fda0a.png)

Attention heads host the most easily interpretable parts of a transformer: the attention patterns. These are low-rank matrices (one matrix for each attention head, as indexed by the `**num heads**` leg) calculated like so:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/3e1fcb710f74e1ef7e270dd0bd272ae5c758bbe870ed1812.png)

Attention patterns determine how information is moved between tokens. For example, this attention pattern seems to move information from earlier tokens matching a pattern onto tokens immediately preceding the equivalent token in a new language:[^yxxu4agslhr]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/2f031fef5bce72b954053c7791ff12a0b44cf83ed7555b13.PNG)

You can play with [the interactive version](https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html#performing-translation) of this in the [In-context Learning and Induction Heads paper](https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html#performing-translation).

We'll see an example of how an attention pattern like this can come about in the final section of this post ([Example: toy induction head](https://www.lesswrong.com/posts/BQKKQiBmc63fwjDrj/graphical-tensor-notation-for-interpretability#Example__toy_induction_head)). But for now, we can just take attention patterns for granted. 

Rather than computing attention patterns on the fly based on the current context in the residual stream (the two "pasted" tensors in pink), the attention pattern can be "frozen" for easier interpretability, fixing the $A$ tensor and therefore fixing a specific pattern of information movement. When this is done, the attention block simplifies to

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/23c24185d647121ec09d7214278b7f1d34d4e3e983811911.png)

which is completely linear: the only non-einsum operation is now just a single copy and paste from the residual stream.[^npy451jmyv] We can also see that the attention pattern $A$ is the only transformation in the whole network which ever acts on the `**seq pos**` index: every other tensor is contracted into the `**hidden**` index, so every other linear transformation can be described independently for each token. This is why the attention pattern is the only part of the network that can move data between tokens. 

With attention frozen, we can represent the sum over heads in a number of ways:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/f75e8e72196a9e2597c7d07cede19617b3b18f938bb86709.png)

these are all equivalent, just placing different emphasis on the independence of each head. You can even go in the opposite direction and emphasize the matrix-multiply nature of the `**num heads**` leg, reminiscent of low rank decompositions like SVD but for operators:[^z5zs6jysta]

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/14d66a6aa5ff177dad23ef63e61fbc2d84e47c3751111003.png)

though this low-rank-operator way of looking at attention is probably less useful than the sum-of-heads way of looking at it. 

Composition and path expansion
==============================

Layers of Attention and MLP blocks don't just act in isolation: they all copy from and write to the same residual stream, so later layers can use information computed in earlier ones. Still following [A Mathematical Framework for Transformer Circuits](https://transformer-circuits.pub/2021/framework/index.html), I'll ignore MLP blocks and focus only on the ways that attention heads in an earlier layer can "compose" with those in a later layer. I'll also ignore small but annoying nonlinearities like [layer normalization](https://transformer-circuits.pub/2021/framework/index.html#technical-details).

So here's a two layer attention-only transformer:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/d588d07348f814874565c97d1a9e0f2ef1facd5227227640.png)

where in the middle we've gone ahead and copied the input into the first attention layer, and on the right we've contracted the attention patterns in the first layer.

When the second layer copies from the residual stream, it will copy the a sum of terms from earlier layers. Rather than treating the result of this sum as a single complicated object, we can keep the sum expanded as two separate terms: the original input $x$ plus a "perturbation" caused by the first layer:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/8ecd322cd83ddad56abaa47477fce74631178a9e982ca457.png)

Now we can expand the output of the whole network as a sum of terms like this, in something reminiscent of a [perturbation theory](https://en.wikipedia.org/wiki/Perturbation_theory):

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/9590b66e8889434c04f0a00d14984f7b802770d73a665ec3.png)

Note: this expansion is not technically true as written due to the nonlinearities (green ellipses) not acting independently on each term in the sum: this is what it means to be nonlinear. However these nonlinearities are pretty linear - consisting only of a mask (which is linear), a re-scale, and a softmax to ensure that the elements in each row of the attention pattern add to 1. The layer normalization nonlinearities (not shown) [can also be treated as approximately linear](https://transformer-circuits.pub/2021/framework/index.html#technical-details) with sufficient care.

So we see that there are three simplest kinds of nontrivial attention composition: Q-composition, K-composition, and V-composition. We can simplify the V-composition and non-composition terms slightly by noticing when the attention patterns in the second layer can also be frozen:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/ef02f5ea21afbb9b517310d5c49a9e2f0d16cd478ed0c15d.png)

so we can see that V-composition has a simpler iteration structure than Q or K composition - the attention pattern is just formed by matrix-multiplying two attention patterns, and likewise for the $W_{O}$ and $W_V$ transformations.

There are ten terms which contribute to the final answer for two layers: three non-compositions (shown), three single compositions (shown), and four higher-order compositions (not shown). But we can (and usually should) also expand out the `**num heads**` indices as sums, and have a separate term for each head and combination of heads. Likewise for the `**seq pos**` indices if we want to consider the contributions to or from specific token positions. [MLP layers could also be incorporated into this expansion sum](https://transformer-circuits.pub/2021/framework/index.html#additional-intuition), though their stronger nonlinearities would require some kind of linearization, and they only have one kind of nontrivial composition anyway. Regardless, the number of terms grows exponentially with the number of layers, so this kind of trick will only be useful if we have some reason to think that most of the work is being done by relatively few terms (preferably low order ones). 

Some intuition for thinking that relatively few terms are important comes from noticing that each head can only write to a relatively small subspace of the residual stream, because the `**head size**` dimension is small compared to the `**hidden**` dimension, and so each head is putting a relatively small dimensional vector into a relatively high dimensional space with $W_O$ and selecting small parts of a high dimensional residual stream with with $W_Q$ and $W_K$, leaving sufficient room in the residual stream for most heads in different layers to act mostly independently if they want.[^qyg1iv81uas] Of course its an empirical matter if low order terms *actually* explain most of the relevant behaviors, so this should be [checked empirically](https://transformer-circuits.pub/2021/framework/index.html#term-importance-analysis). There may also be much more effective ways of decomposing the computations of a transformer into a series of terms or circuits like this, such that more of the relevant behavior is explained by fewer more interpretable terms. One recent method attempting to find them is the [Automated Circuit DisCovery (ACDC)](https://arxiv.org/abs/2304.14997) algorithm. 

Example: toy induction head
===========================

We'll finish off by constructing one toy example of an induction head: a circuit detecting what should come next in a repeated string of tokens. For example, consider predicting the next word in some text like "The quick brown fox \[...\] the quick brown". It seems like " fox" should come next to fit the pattern. Information from the earlier " fox" token should be copied into the final " brown" token's residual stream, so that the model can predict " fox" for the next word there:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/4d4c7ff11695613289338dfc928f4919c565e3863adf2cff.png)

This is known as induction, which is a type of in-context learning. There are many ways to make these induction heads, and real induction heads are likely to be messy, but they [have been found](https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html) in models of all sizes. We'll construct a handcrafted toy example of an induction circuit, by forming a "virtual induction head" from the K-composition of two heads in different layers:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/679e4bbccf55503b6b9d4960edfd95bff2f424a7c1406991.png)

Everything in this diagram is now a matrix, so we could draw it to emphasize that it's just a sequence of matrix multiplications:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/1715cf0c31f4e10306868df4b624e975a599b4215c854eb1.png)

but we'll stick with the previous format so you can more easily see how the circuit fits into the rest of the network. 

In order for this to act like an induction head, the last "brown" input token (![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/a18ed8ca4fb4b747feed7fdd0eb84879325fb03325d35b2a.png)) should output a "fox" token:[^axzrr2nf90q]  

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/df25f3b253b4cb011cde87c6d66f2b64897d5470891736fb.png)

Induction involves exploiting repeated patterns, so induction heads had better be able to pattern match. Here's the subcomponent of our induction circuit which will do that:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/34eceef7bcece8be7f8ed6b0d5e72dbdaccb3702a2dcd727.png)

So $W_O$ and $W_V$ in layer 1 (shown in yellow) and $W_Q$ and $W_K$ in layer 2 (shown in blue) can effectively just behave like identity matrices in the relevant subspace, or anything else producing something like a "semantic delta tensor" when you compose them together. We denote this tensor as "Match" because it should be near zero when contracted with any two vectors unless the vector on the left semantically "matches" with the vector on the right. 

Now, we don't really care about the matching token per se, we just want to know which token came after it. We can see that the "key" side of the attention pattern $A$ from layer 1 is going to be indexed at whatever token positions we get a match on (and multiplied by a number depending on strong the match is):

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/0b79bc5c36f186eba7fae60531308c58c7d0f807bd766810.png)

We know that the token we want is in the position directly after  `1st brown pos`, so $A$ should map an index of `pos` on its key side to `pos + 1` on its query side so that we can index that token. Then we'll have

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/999c72caca7d0f0f27f57bccdb6d9c825d12cca854e269a6.png)

as we desire. We see that an attention pattern $A$ doing this can just be a fixed off-diagonal delta-tensor $A_{k,\,q}=\delta_{k,\,q+1}$: 

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/5c5a3af3196beec43a470dc6059ee81c4ef02c435180b487.png)

This is also equivalent to mapping `pos` on its query side to `pos - 1` on its key side: always just attending to the previous token, so heads with this attention pattern are known as "previous token heads". This attention pattern also removes the unwanted `2nd brown pos` term, because this term indexes the final column of the attention matrix where all entries are zero. 

Putting this attention pattern $A_{k,\,q}=\delta_{k,\,q+1}$ into our circuit therefore simplifies it to:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/d145e222869cd218331aca3fc06689744144994576e65f65.png)

so we can get the desired "fox" token out, so long as it gets properly handled by $W_{V}$ and $W_O$ (which are in charge of putting the "fox" information into the residual stream), and by the unembedding $W_U$ (which is in charge of getting the "fox" information out of the residual stream and turning it into the correct "fox" token). 

Putting everything together, we see that it's possible for virtual induction heads to have a very simple approximate form, composed almost entirely of delta-like tensors:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/6f63ff70e43591b4173166c4a20465a1ed18e14223f36358.png)

We can sanity check this toy induction head by computing its attention pattern numerically on a repeating sequence of random vectors. We see that it looks like a real induction-head pattern, attending to the tokens which followed the current token previously in the sequence:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/08222bededfb4f6ce858a2ae527aa2ba1c0be99881402dad.png)

here's the code for that:

    import torch as t; import matplotlib.pyplot as plt
    from einops import einsum, repeat
    hidden_dim = 768
    pattern_len = 6
    
    # Generate a three times repeated sequence of 6 random vectors
    x = t.rand((pattern_len, hidden_dim))
    x = repeat(x, 'seq hidden -> (repeat seq) hidden', repeat=3)
    seq_len = x.shape[0]
    
    # Calculate the toy attention pattern
    prev_token_head = t.diag(t.tensor([1.0]*(seq_len-1)), diagonal=-1)
    match = t.eye(hidden_dim)
    attn_pattern = einsum(x, match, x, prev_token_head, 'seq0 hidden0, hidden0 hidden1, seq1 hidden1, seq1 seq2 -> seq2 seq0')
    
    # Apply the mask and softmax to the attention pattern  
    attn_pattern = (t.tril(attn_pattern)-t.triu(t.ones_like(attn_pattern)*1e5))
    attn_pattern = attn_pattern.softmax(dim=-1)
    print('attn_pattern = '); plt.imshow(abs(attn_pattern), cmap='Blues'); plt.show()

However this virtual induction head formed by K-composition is just one term in the path expansion, so we'd need to make sure that this is actually the dominant term by suppressing the others. This is where the $W_{Q/K/O/V/U}$ matrices are important, because they selectively determine what gets taken from and added into the residual stream, allowing them to suppress unwanted terms in the path expansion (if they so choose) so that only this virtual induction head is important.  

Finally, we can put these $W_{O/V/U}$ matrices back in and see the full induction network rather than just the "virtual attention head" term in the path expansion:

![](https://39669.cdn.cke-cs.com/rQvD3VnunXZu34m86e5f/images/f57e5c1a39baa257f8add86b0e180afe53de2e2615c6ba27.png)

Conclusion
==========

Overall I think graphical tensor notation is a really useful tool for understanding and communicating interpretability results, especially for anything involving operations between more than a few tensors. It surfaces dualities and interesting equivalences more easily than other notation, and remains intuitive without necessarily losing any mathematical rigor. It may just be my preference, but I continually run into papers where I wished this notation was used. 

Please suggest corrections and changes. I can also give you editing access if you'd like to expand this document or make more substantial rewrites. Editable SVG files for all diagrams are available [here](https://drive.google.com/drive/folders/16uuvkG1NtjhpAbchJ2R1Zp96IoZYKtk5?usp=drive_link).  Please also let me know whether you think the impact of this post due to capabilities externalities will be net-negative despite its potential use for interpretability work, or use [this poll](https://strawpoll.com/w4nWrXkzdyA) if you have an opinion either way.

[^bwj2f888xxm]: And capabilities work, so this is dual-use information. Please use this poll to let me know whether or not you think the impact of this post will be net negative. 

[^bytowdrb3ck]: also known as tensor-network notation, Penrose graphical notation, ZX-calculus, or string-diagram notation depending on the context.  

[^ytaychzpt3]: Technically tensors are abstract multilinear maps, rather than just arrays of numbers. However the two are equivalent once a basis for the multilinear space has been chosen. 

[^13ccdi9zt3v]: A tensor with \(N\) legs each of dimension \(d\) contains \(d^N\) numbers.  

[^h1buayt8oo]: Consider contracting along the top line first:which has a cost exponential in the number of tensors, because an intermediate tensor is created with \(N/2\) legs. A much more efficient order is which limits the the intermediate tensors to no more than three legs, and scales linearly with the number of tensors. In general, finding the optimal order in which to contract a tensor network is an NP-hard problem, let alone actually performing the contraction, which is #P-hard in general. Usually though, fairly simple contraction order heuristics and approximation techniques get you relatively far.  

[^9fzq2gk6z18]: technically rearrange(einsum(t.eye(5), t.eye(3), 'i j, k l -> i k j l'), 'i k j l -> (i k) (j l)') as shape rearrangement isn't yet supported within an einsum call. 

[^m5ocsq6ut5j]: This is not the most natural way of representing a matrix or the effects of an SVD, because images have a notion of locality between nearby pixels, whereas nearby entries in a matrix are treated as unrelated. Still, it's intuitive and easy to visualize. See part 6B of Six and a half intuitions for a more natural SVD compression example.  

[^tsvsfrkezh]: A sketch of the proof can be found in the appendix of Six and a half intuitions The original references are: Schmidt 1907 Eckart and Young 1936 Mirsky 1960   

[^gub7c5mcstg]: Hitchcock 1927Harshman 1970Carroll and Chang 1970 

[^jxtzw5zwo8]: Hitchcock 1927Tucker 1966 

[^nw86qlujp7]: Hastad 1990 

[^fmyqacdyvzw]: These originally come from quantum physics. Matrix Product State is the original name used by physicists, while tensor train is a more recent term sometimes used by mathematicians.  

[^7wayrajbdzx]: For example, using a series of local SVDs and local contractions like so: 

[^vvx1l7fra8]: Gauge freedom is the fact that many tensor networks contract to the same tensor, so transformations can be applied which affect the tensors, but don't affect what the network would contract to. For example, a resolution of the identity \(I=XX^{-1}\) can be inserted on any bond. The matrix and its inverse can then be contracted into opposite surrounding tensors:This can even arbitrarily increase the bond dimension of the tensor network without changing what it represents, since the matrices \(X\) and \(X^{-1}\) can be rectangular.As a special case, the tensor network could even be multiplied by an entirely separate tensor network which contracts to the number 1:Making all tensors isometric towards some spot in the network will fix the gauge, since SVDs are unique (up to degeneracies in the singular value spectrum). Likewise, truncating the zero singular values will remove any unnecessarily large bond-dimensions. However SVDs only work for gauge-fixing in tensor networks without loops, such as tensor trains or tree tensor networks.  

[^k90gzolb6j]: Tensor networks work best at representing quantum states which have entanglement scaling with the surface area (rather than volume) of a subsystem. 

[^d3bbi58ho5]: Simon Verret already has a post on representing neural networks such as RNNs and LSTMs in graphical tensor notation, but I'll be using a different approach to the nonlinearities. 

[^1g1fyzr8cdph]: The bias terms can be accommodated into the matrices by concatenating \([1]\) to the input vector \(x\) and expanding the weight matrices appropriately. 

[^jt0n10eo4b]: a \(4\times\) higher dimensional space in the case of GPT-2 

[^yxxu4agslhr]: This specific "induction head" kind of attention pattern will only be seen after the first layer, because it must arise as a result of composition with attention head(s) in previous layers. 

[^npy451jmyv]: Copying and pasting can in general be nonlinear, for example if products are taken between copies of an object, that's the same as raising it to some power: a nonlinear operation. But here with attention frozen there are no products taken between copies: just a sum when the attention result is added back into the residual stream. 

[^z5zs6jysta]: This is reminiscent of other decompositions of tensor operators into sums of rank-one tensor products, such as sums of strings of single-site Pauli operators in quantum error correcting codes, and Matrix Product Operators (MPOs) more generally.  

[^nlxx83ypt3]: I'm ignoring layer norm throughout this section.  

[^qyg1iv81uas]: Though the same isn't true for MLP layers. Additionally, if num heads \(\times\)head size \(\approx\) hidden dim (as is usually true), and the contribution of each head to its subspace is not small, then some decent number of heads per layer must interact with heads in the previous layer.  

[^axzrr2nf90q]: The original "brown" vector will probably also have to be subtracted out of the residual stream somewhere too. This is also ignoring positional embeddings.