Transformer, explained · Part 4 of 7 · Covers §4, Table 1

Why Self-Attention: Cost, Steps and Paths

Section 4 compares self-attention with recurrent and convolutional layers on three measures: work per layer, steps that must wait for each other, and the distance a signal must travel. We compute every column of Table 1 in Python, check the counts with a FLOP counter, and build the restricted attention window in code.

Attention Is All You Need. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin. NeurIPS 2017, 2017. arXiv:1706.03762

Parts 2 and 3 showed what the Transformer is. Section 4 answers a different question: why build a layer out of attention, and not out of a recurrent or a convolutional network? The paper gives three measures and one table, and we put real numbers into each cell.

Three measures for a layer

Table 1, cell by cell

The first column: work per layer

Where do the two formulas come from? Count the multiplications.

A recurrent layer updates its memory once for each word. Each update multiplies a vector of dd numbers by a d×dd \times d matrix, which is d2d^2 multiplications. With nn words, that is n⋅d2n \cdot d^2.

Self-attention compares each word with each other word. That is n×nn \times n pairs. Each comparison is a dot product of two vectors of dd numbers, so it costs dd multiplications. The mix of the values costs the same again. So the core of attention costs about n2⋅dn^2 \cdot d.

self-attention: n2⋅drecurrent: n⋅d2n2⋅dn⋅d2=nd\text{self-attention: } n^2 \cdot d \qquad \text{recurrent: } n \cdot d^2 \qquad \frac{n^2 \cdot d}{n \cdot d^2} = \frac{n}{d}

Read the ratio on the right. The two costs share a factor n⋅dn \cdot d, so it cancels. What is left is n/dn / d: sentence length over vector size. When nn is smaller than dd, attention does less work. When nn is larger, the recurrent layer does less.

A tiny example you can check by hand. Take n=4n = 4 words and vectors of d=3d = 3 numbers. The attention scores are a 4×44 \times 4 grid of 16 dot products, each with 3 multiplications: 48 in all. The recurrent layer does 4 updates of 3×3=93 \times 3 = 9 multiplications: 36 in all. The ratio is 48/36=4/348 / 36 = 4 / 3, which is exactly n/dn / d.

Now the real sizes, with d=512d = 512 as in the base model. Our script tx_part4.py prints the counts:

nnself-attention n2dn^2 drecurrent nd2n d^2convolution knd2k n d^2, k=3k=3restricted rndr n d, r=16r=16ratio n/dn/d
1051,2002,621,4407,864,32081,9200.020
27373,2487,077,88821,233,664221,1840.053
1005,120,00026,214,40078,643,200819,2000.195
512134,217,728134,217,728402,653,1844,194,3041.000
2,0482,147,483,648536,870,9121,610,612,73616,777,2164.000
8,19234,359,738,3682,147,483,6486,442,450,94467,108,86416.000

For a sentence of 27 tokens, the attention core needs about 19 times less work than a recurrent layer. The two meet at exactly n=512=dn = 512 = d. At 8,192 tokens, attention needs 16 times more.

One honest note. Table 1 counts only the part that differs between the layers. A real attention layer also multiplies every vector by the matrices WQW^Q, WKW^K, WVW^V and WOW^O of Part 3. That adds about 4⋅n⋅d24 \cdot n \cdot d^2, the same size as a recurrent layer. A real recurrent layer, such as an LSTM, also has several matrices. So the table compares the parts that grow differently, and leaves out the shared n⋅d2n \cdot d^2 terms.

Counting the work in code

The counts above came from formulas. Now we count them from code, in two ways. Method 1 reads the shapes and multiplies, with one comment for each term. Method 2 runs the real layers under FlopCounterMode, a tool of PyTorch that watches every matrix product and adds up its cost.

python
def count_self_attention(n, d):
    """Multiply-adds of one self-attention layer, from the shapes (one head, as in Table 1)."""
    scores = n * n * d          # Q K^T: n x n dot products, d multiply-adds each
    mix = n * n * d             # weights V: n output rows, each a sum of n value vectors of d numbers
    proj = 4 * n * d * d        # x W_Q, x W_K, x W_V and the output W_O: n vectors times a d x d matrix, four times
    return scores + mix, proj

def count_recurrent(n, d):
    """Multiply-adds of one recurrent layer h_t = tanh(x_t W + h_{t-1} U)."""
    chain = n * d * d           # h_{t-1} U: one d x d product per position, one after another
    inp = n * d * d             # x_t W: the input part, also d x d per position
    return chain, inp

def count_conv(n, d, k):
    """Multiply-adds of one convolution with kernel width k, d channels in and out."""
    return k * n * d * d        # each of n outputs mixes k input vectors, each with a d x d matrix

def count_separable(n, d, k):
    """Multiply-adds of a depthwise separable convolution: a per-channel window, then a d x d mix."""
    depthwise = k * n * d       # each channel has its own k numbers: no mixing across channels
    pointwise = n * d * d       # a 1 x 1 convolution mixes the d channels at each position
    return depthwise + pointwise

def flops(fn, *args):
    """FLOPs that PyTorch's own counter sees (it counts 2 FLOPs per multiply-add)."""
    with FlopCounterMode(display=False) as fc:
        fn(*args)
    return fc.get_total_flops()

The layers themselves are short functions. attention_core is the part of self-attention that Table 1 counts. recurrent_chain is the part of a recurrent layer that must wait. The convolutions come from PyTorch.

python
def attention_core(q, k, v):
    """The part of self-attention that Table 1 counts: the scores and the mix. q, k, v: (n, d)."""
    return torch.softmax(q @ k.T / math.sqrt(q.shape[1]), dim=-1) @ v       # (n, d)

def recurrent_chain(xw, U):
    """The part of a recurrent layer that must wait: h_t = tanh(xw_t + h_(t-1) U), one position after another."""
    h = torch.zeros(xw.shape[1])
    for t in range(xw.shape[0]):        # h_t needs h_(t-1): no way to compute the steps at the same time
        h = torch.tanh(xw[t] + h @ U)
    return h                             # the last memory (d,)

n, d, k = 27, 512, 3                                                       # 27 tokens, the base model's d
x = torch.randn(n, d)                                                      # the input: n vectors of d numbers
Wq, Wk, Wv, Wo, W, U = (torch.randn(d, d) / math.sqrt(d) for _ in range(6))
conv = torch.nn.Conv1d(d, d, k, padding=1, bias=False)                     # contiguous kernel of width 3
depthwise = torch.nn.Conv1d(d, d, k, padding=1, groups=d, bias=False)      # one small window per channel
pointwise = torch.nn.Conv1d(d, d, 1, bias=False)                           # mixes the channels
xc = x.T.unsqueeze(0)                                                      # Conv1d wants (batch, channels d, length n)

Then we count each part both ways, with n=27n = 27 tokens and d=512d = 512:

python
core, proj = count_self_attention(n, d)
chain, inp = count_recurrent(n, d)
with torch.no_grad():
    q, kk, v = x @ Wq, x @ Wk, x @ Wv
    rows = [  # (part of the layer, Method 1: our count, Method 2: what PyTorch's counter sees)
        ('self-attention core (QK^T and weights V)', core, flops(attention_core, q, kk, v)),
        ('self-attention projections W_Q W_K W_V W_O', proj, flops(lambda: (x @ Wq, x @ Wk, x @ Wv, x @ Wo))),
        ('recurrent chain h U (n steps)', chain, flops(recurrent_chain, x @ W, U)),
        ('recurrent input x W', inp, flops(lambda: x @ W)),
        ('convolution, k = 3', count_conv(n, d, k), flops(conv, xc)),
        ('separable convolution, k = 3', count_separable(n, d, k), flops(lambda: pointwise(depthwise(xc)))),
    ]
plain text
part of the layer                               our count  counter / 2  equal
self-attention core (QK^T and weights V)          746,496      746,496  True
self-attention projections W_Q W_K W_V W_O     28,311,552   28,311,552  True
recurrent chain h U (n steps)                   7,077,888    7,077,888  True
recurrent input x W                             7,077,888    7,077,888  True
convolution, k = 3                             21,233,664   21,233,664  True
separable convolution, k = 3                    7,119,360    7,119,360  True
Table 1 terms: n^2 d = 373,248, n d^2 = 7,077,888, k n d^2 = 21,233,664, k n d + n d^2 = 7,119,360

Every row agrees to the last digit. So the formulas of Table 1 are not estimates. They are exact counts of the matrix products, without the constant factors. One of those factors is worth seeing. The attention core costs 2n2d=746,4962 n^2 d = 746{,}496, not n2d=373,248n^2 d = 373{,}248, because it has two products of that size: the scores and the mix. Big-O notation drops the 2.

The second lesson is the honest note above, now in numbers. The four projections of attention cost 28,311,552 multiply-adds, the attention core only 746,496. For a short sentence, the projections are most of the cost of a real attention layer, and the n2n^2 term is small. Table 1 leaves them out, because a recurrent layer has the same kind of term. It is the input product xWx W, the row with 7,077,888.

Terminal output of tx_part4b.py, the code walkthroughs of this page

Counting the waiting steps in code

We can measure this column too. PyTorch records every operation of a computation in a graph, to compute the gradients later. Each operation points to the operations whose results it used. The longest chain in that graph is the number of steps that must run one after another, whatever the hardware.

python
def graph_depth(t):
    """The longest chain of operations from the output back to the inputs, in PyTorch's autograd graph.
    Operations on one chain must run one after another; operations on different chains can run in parallel."""
    memo = {}

    def depth(node):
        if node is None:
            return 0
        if node not in memo:
            parents = [p for p, _ in node.next_functions]
            memo[node] = 1 + max([depth(p) for p in parents] or [0])
        return memo[node]
    return depth(t.grad_fn)

def self_attention_layer(x, Wq, Wk, Wv):
    """A whole self-attention layer (one head): the projections, then the core."""
    return attention_core(x @ Wq, x @ Wk, x @ Wv)                            # (n, d)

def recurrent_layer(x, W, U):
    """A whole recurrent layer: the input part x W for all positions at once, then the chain."""
    return recurrent_chain(x @ W, U)

We give the weights requires_grad=True, so that PyTorch records the graph. Then we measure both layers for four lengths:

python
dd = 64
Aq, Ak, Av, Wr, Ur = (torch.randn(dd, dd, requires_grad=True) for _ in range(5))
for n2 in [4, 16, 64, 256]:
    xs = torch.randn(n2, dd)
    da = graph_depth(self_attention_layer(xs, Aq, Ak, Av))
    dr = graph_depth(recurrent_layer(xs, Wr, Ur))
plain text
== 2. Sequential operations: longest chain in the autograd graph (d = 64)
     n  self-attention  recurrent
     4               7         20
    16               7         80
    64               7        320
   256               7       1280

For self-attention, the longest chain has 7 operations at every length: a few matrix products, the scale and the softmax. A longer sentence makes each product larger, but it does not add a link to the chain. The recurrent chain grows in step with nn: 20 operations for 4 tokens, 1,280 for 256 tokens. This is the second column of Table 1, measured: O(1)O(1) against O(n)O(n). A GPU can run the many multiplications inside one attention product at the same time. It cannot start step 2 of the recurrent chain before step 1 ends.

The tokenizer gives these lengths:

plain text
2. Real sentence lengths (Marian en-de tokenizer, d_model = 512)
  n =   8  words =  6  n/d = 0.016  The cat sat on the mat.
  n =   7  words =  5  n/d = 0.014  Attention is all you need.
  n =  13  words = 12  n/d = 0.025  The agreement on the European Economic Area was signed in Au...
  n =  23  words = 17  n/d = 0.045  Self-attention layers are faster than recurrent layers when ...
  n =  33  words = 26  n/d = 0.064  Learning long-range dependencies is a key challenge in many ...
  n =  39  words = 32  n/d = 0.076  The Transformer allows for significantly more parallelizatio...
  tokens of sentence 4: ▁Self - at tention ▁layers ▁are ▁faster ▁than ▁recurrent ▁layers ▁when ▁the ▁sequence ▁length ▁is ▁smaller ▁than ▁the ▁representation ▁dimension ality . </s>

Even the longest sentence, at 39 tokens, has n/d=0.076n / d = 0.076. So for this sentence the attention core does about 13 times less work than a recurrent layer of the same width. Each count includes the end-of-sentence token </s>. These six sentences are our own choice, not the paper's test set. They show the order of size, not an average over a corpus.

Time on a real computer

Counting multiplications is one thing. Does a real computer agree? Our script times one self-attention step against one recurrent loop, both with d=512d = 512. The attention step includes the three projections to queries, keys and values. The recurrent loop does its input projection for all words at once, then runs the chain of nn updates.

Terminal output of tx_part4.py with the work counts, sentence lengths, CPU timings, gradient sizes and layer counts

plain text
3. Wall time on CPU (4 threads), d = 512, median of 7 runs, milliseconds
     n  self-attn  recurrent loop  steps attn  steps rnn
    16      0.059           0.156           1         16
    64      0.147           0.573           1         64
   256      0.578           2.449           1        256
  1024      4.568           8.970           1       1024
  4096     42.882          37.640           1       4096

At 16 tokens, attention takes 0.059 ms and the loop takes 0.156 ms. At 1,024 tokens, attention is still about two times faster. At 4,096 tokens, the n2n^2 term wins and attention becomes the slower one, as the arithmetic predicts.

Two limits apply to this test. It ran on a laptop CPU with 4 threads, in PyTorch, with random weights and one batch of one sentence. A GPU runs the large matrix products of attention much faster, but cannot speed up the 4,096 small waiting steps of the loop. So we expect the gap at short lengths to be larger on a GPU, not smaller. We did not measure a GPU here.

The path between two words

The third column of Table 1 is about learning, not speed. The figure below draws the three layers for a sentence of 8 words. The bold lines show the shortest route from word 1 to the output at word 8.

How many steps from word 1 to the output at word 8?Recurrent: 7 steps, one after anotherDilated convolution (k = 3): 2 layersSelf-attention: 1 step from every wordword 1word 8word 1word 8word 1word 8Bold lines: the shortest path from word 1 to the output at word 8. The longer the path, the harder the signal is to learn.
The maximum path length of Table 1, drawn for 8 words. A recurrent layer passes word 1 through a chain of 7 memory updates. A dilated convolution with kernel 3 reaches it in 2 layers. Self-attention reaches it in 1 step.

Why does a long path hurt? During training, the network learns from the gradient: a signal that says how much each input should change. In a recurrent layer, the gradient from word 8 to word 1 passes through 7 updates. Each update multiplies it by a matrix and by the slope of the tanh function. If these factors are a little smaller than 1, the product shrinks fast, like 0.67≈0.030.6^7 \approx 0.03.

We measured this. Our script builds a recurrent network with tanh and a single self-attention layer, both with random weights. Each has vectors of 64 numbers and reads 50 positions. We compute the size of the gradient of the last output with respect to each input, and take the mean of 20 random networks.

plain text
4. Gradient norm |d out_last / d x_i| (random weights, d = 64, n = 50), mean of 20 seeds
 input i  distance          RNN    self-attn
       1        49     1.55e-09     2.97e-01
      10        40     7.31e-08     2.76e-01
      20        30     5.23e-06     2.13e-01
      30        20     6.06e-04     1.77e-01
      40        10     4.66e-02     2.14e-01
      45         5     5.58e-01     3.11e-01
      49         1     3.33e+00     1.99e-01
      50         0     5.02e+00     1.87e+00
10^-910^-710^-510^-310^-110^101020304049recurrent (tanh), 0: 10^0.7recurrent (tanh), 1: 10^0.5recurrent (tanh), 2: 10^0.3recurrent (tanh), 5: 10^-0.3recurrent (tanh), 10: 10^-1.3recurrent (tanh), 15: 10^-2.3recurrent (tanh), 20: 10^-3.2recurrent (tanh), 25: 10^-4.3recurrent (tanh), 30: 10^-5.3recurrent (tanh), 35: 10^-6.2recurrent (tanh), 40: 10^-7.1recurrent (tanh), 45: 10^-8.1recurrent (tanh), 49: 10^-8.8self-attention, 0: 10^0.3self-attention, 1: 10^-0.7self-attention, 2: 10^-0.7self-attention, 5: 10^-0.5self-attention, 10: 10^-0.7self-attention, 15: 10^-0.8self-attention, 20: 10^-0.8self-attention, 25: 10^-0.8self-attention, 30: 10^-0.7self-attention, 35: 10^-0.6self-attention, 40: 10^-0.6self-attention, 45: 10^-0.5self-attention, 49: 10^-0.5self-attention: 10^-0.5recurrent (tanh): 10^-8.8distance between the input and the last position (in positions)gradient size (log scale)
How strongly the last output depends on each input, against the distance between them (log scale). In the recurrent network the gradient falls by about 10 to the power 9 over 49 positions. In one self-attention layer it stays between about 0.18 and 0.31 at every distance above 0. Random weights, 64 numbers per vector, mean of 20 networks.

The numbers tell the story of the third column. In the recurrent network, the first word has a gradient of 1.55×10−91.55 \times 10^{-9}, about three billion times smaller than the last word. In practice, such a word cannot teach the network anything. In the attention layer, the first word gets 0.297 and the 49th word gets 0.199. Distance does not matter.

This test has limits too. The weights are random, not trained. A trained LSTM keeps gradients much better than our plain tanh network, because its gates were designed for this problem. And the input at distance 0 is larger in attention because it also feeds the query of the last position. The test shows the shape of the effect, not the size it has in a real trained model.

The path length in code

The figure above draws the paths for 8 words. Code can count them for any length. The idea is to describe one layer by a table of links. Entry (i,j)(i, j) is True if output ii reads input jj directly. A stack of layers is the product of their tables, with True and False in place of numbers. We stack layers until the last output depends on the first input.

python
def window_links(n, offsets):
    """links[i, j] = True if output i of one layer reads input j directly, for j = i + offset.
    A one-sided kernel of width 3 has offsets (0, -1, -2); dilated by 3 it has (0, -3, -6)."""
    links = torch.zeros(n, n, dtype=torch.bool)
    for i in range(n):
        for o in offsets:
            if 0 <= i + o < n:
                links[i, i + o] = True
    return links

def layers_to_connect(n, layer_links):
    """Stack layers until the last output depends on the first input; return how many layers that takes.
    layer_links(L) gives the links of layer L. The dependency of a stack is the boolean product of its layers."""
    reach = torch.eye(n, dtype=torch.bool)           # before any layer, each position depends only on itself
    for L in range(1, 10 * n):
        reach = (layer_links(L).int() @ reach.int()) > 0
        if reach[n - 1, 0]:                          # output n now depends on input 1
            return L
    return None

def recurrent_path(n):
    """x_1 enters the memory at h_1. To reach the output z_n = h_n, it must pass h_1 -> h_2 -> ... -> h_n."""
    t, hops = 1, 0                        # x_1 is now inside h_1
    while t < n:
        t += 1                            # one recurrent step: h_t is computed from h_(t-1)
        hops += 1
    return hops

Each layer type is one table of links. Self-attention links every pair. A convolution with kernel 3 links each output to its own position and the two before it. A dilated convolution spreads the same three links further apart in each layer: 1, 3 and 9 positions. Restricted attention links each output to the rr inputs centred on it.

python
k = 3
for n2 in [8, 27, 64, 100]:
    full = torch.ones(n2, n2, dtype=torch.bool)
    p_attn = layers_to_connect(n2, lambda L: full)                                       # every query sees every key
    p_conv = layers_to_connect(n2, lambda L: window_links(n2, [0, -1, -2]))               # kernel 3, contiguous
    p_dil = layers_to_connect(n2, lambda L: window_links(n2, [0, -k ** (L - 1), -2 * k ** (L - 1)]))   # dilation 1, 3, 9, ...
    p_loc = layers_to_connect(n2, lambda L: window_links(n2, range(-2, 3)))               # window of r = 5, centred
    p_loc17 = layers_to_connect(n2, lambda L: window_links(n2, range(-8, 9)))             # window of r = 17, centred
    p_rnn = recurrent_path(n2)
plain text
    n  self-attn  recurrent  conv k=3  dilated k=3  restricted r=5  r=17
    8          1          7         4            2               4     1
   27          1         26        13            3              13     4
   64          1         63        32            4              32     8
  100          1         99        50            5              50    13

Read the row for n=27n = 27. Self-attention needs 1 layer, the recurrent layer 26 steps, the contiguous convolution 13 layers and the dilated convolution 3. These are the counts of Table 1: O(1)O(1), O(n)O(n), O(n/k)O(n/k) and O(log⁡kn)O(\log_k n). The last two columns are restricted attention. With a window of 5, it needs 13 layers, because each layer reaches only 2 positions further. A window of 17 reaches 8 positions per layer and needs 4: the O(n/r)O(n/r) of the last row of Table 1.

Our convolutions are one-sided: each output looks only at its own position and earlier ones. The decoder of ByteNet, reference [18] of the paper, works this way. Their reach matches the log⁡k(n)\log_k(n) of the paper exactly. A centred kernel reaches as many positions, but half of them on each side.

When sentences get long

Restricted attention in code

The window is one more mask, like the decoder mask of Part 3. Query ii may look at key jj only if the two are at most ⌊r/2⌋\lfloor r/2 \rfloor positions apart. Here is the mask for 10 tokens and a window of r=5r = 5, built with two loops:

python
n2, r = 10, 5
half = r // 2
local = torch.zeros(n2, n2, dtype=torch.bool)
for i in range(n2):
    for j in range(n2):
        local[i, j] = abs(i - j) <= half        # key j is inside the window of query i
plain text
window mask (1 = may look):
   1 1 1 0 0 0 0 0 0 0
   1 1 1 1 0 0 0 0 0 0
   1 1 1 1 1 0 0 0 0 0
   0 1 1 1 1 1 0 0 0 0
   0 0 1 1 1 1 1 0 0 0
   0 0 0 1 1 1 1 1 0 0
   0 0 0 0 1 1 1 1 1 0
   0 0 0 0 0 1 1 1 1 1
   0 0 0 0 0 0 1 1 1 1
   0 0 0 0 0 0 0 1 1 1
allowed pairs: 44 of 100 (at most r = 5 per row)

The mask is a band along the diagonal. Each row has at most 5 ones. The rows at the two ends have fewer, because their window runs past the end of the sentence. Our attention function with this mask gives the same output as the library with the same mask.

python
def masked_attention(q, k, v, mask):
    """Equation 1 with a mask: computes all n x n scores, then hides the ones outside the window."""
    scores = q @ k.T / math.sqrt(q.shape[-1])
    scores = scores.masked_fill(~mask, float('-inf'))
    return torch.softmax(scores, dim=-1) @ v

There is a catch. The masked version still computes all n×nn \times n scores, and then throws most of them away. It saves no work at all. To get the r⋅n⋅dr \cdot n \cdot d of Table 1, the code must compute only the scores inside the window. banded_attention does that. For each query, it cuts out the rr keys and values of its window with unfold, and computes rr dot products.

python
def banded_attention(q, k, v, r):
    """The same result, but each query computes only its r scores: n * r work in place of n * n."""
    n, d = q.shape
    half = r // 2
    kp = F.pad(k, (0, 0, half, half))                # (n + 2 half, d): zero rows before the start and after the end
    vp = F.pad(v, (0, 0, half, half))
    k_win = kp.unfold(0, r, 1)                       # (n, d, r): for query i, the keys i - half ... i + half
    v_win = vp.unfold(0, r, 1)                       # (n, d, r)
    scores = (q.unsqueeze(1) @ k_win).squeeze(1) / math.sqrt(d)    # (n, r): r dot products per query
    pos = torch.arange(n).unsqueeze(1) + torch.arange(r) - half      # (n, r): the real position of each window slot
    scores = scores.masked_fill((pos < 0) | (pos >= n), float('-inf'))   # slots outside the sentence are padding
    w = torch.softmax(scores, dim=-1)                # (n, r)
    return (v_win @ w.unsqueeze(2)).squeeze(2)       # (n, d): mix the r values of each window

q, kk, v = torch.randn(n2, 16), torch.randn(n2, 16), torch.randn(n2, 16)
o_mask = masked_attention(q, kk, v, local)
o_lib = F.scaled_dot_product_attention(q, kk, v, attn_mask=local)
o_band = banded_attention(q, kk, v, r)
d_lib = (o_mask - o_lib).abs().max().item()       # Method 1 (our mask) against Method 2 (the library)
d_band = (o_band - o_mask).abs().max().item()     # the banded version against the masked one
plain text
masked (ours) vs F.scaled_dot_product_attention(attn_mask): max abs difference 0.0e+00
banded (n * r scores) vs masked (n * n scores):              max abs difference 1.2e-07
Restricted self-attention, n = 10, window r = 5mask: all n x n = 100 pairsq1q2q3q4q5q6q7q8q9q10k1k2k3k4k5k6k7k8k9k10keep the bandband: n x r = 50 slotsq1123q21234q312345q423456q534567q645678q756789q8678910q978910q108910-2-10+1+2Left: a mask still computes all 100 scores and hides 56. Right: the band computes only the 44 real ones (number = key position);the 6 empty slots at the ends are padding. The work falls from n x n to n x r.
Restricted self-attention for 10 tokens and a window of 5. Left: the mask, a band of 44 allowed pairs in a table of 100. Right: the same band stored as 10 rows of 5 slots, so that only the scores inside the window are computed. The number in each slot is the position of its key; empty slots at the two ends are padding.

The banded version matches the masked one to 1.2×10−71.2 \times 10^{-7}, and the masked one matches the library exactly. Now count the work. We ran both versions under the FLOP counter, with d=64d = 64 and a window of 17:

python
d3, r3 = 64, 17
for n3 in [256, 1024, 4096]:
    q, kk, v = torch.randn(n3, d3), torch.randn(n3, d3), torch.randn(n3, d3)
    mask = (torch.arange(n3).unsqueeze(1) - torch.arange(n3)).abs() <= r3 // 2
    fm = flops(masked_attention, q, kk, v, mask)
    fb = flops(banded_attention, q, kk, v, r3)               # window of 17: 8 on each side and the centre
plain text
FLOPs of the two versions, d = 64, r = 17 (FlopCounterMode, matrix products only)
     n   masked n x n  banded n x r  ratio
   256     16,777,216     1,114,112     15
  1024    268,435,456     4,456,448     60
  4096  4,294,967,296    17,825,792    241

At 256 tokens, the band does 15 times less work than the full table. At 4,096 tokens, it does 241 times less, and the gap keeps growing with nn. That is the gain the authors had in mind. The price is the path: the table of the previous section showed that a window of 17 needs 13 layers to connect 100 tokens.

Why not convolutions?

Our script counts the layers that a kernel of width 3 needs to connect the first and the last word:

plain text
5. Layers needed to connect position 1 and position n (kernel k = 3)
  n =   10: contiguous    5 layers, dilated  3 layers, self-attention 1 layer, RNN 9 steps
  n =   27: contiguous   13 layers, dilated  3 layers, self-attention 1 layer, RNN 26 steps
  n =  100: contiguous   50 layers, dilated  5 layers, self-attention 1 layer, RNN 99 steps
  n =  512: contiguous  256 layers, dilated  6 layers, self-attention 1 layer, RNN 511 steps

The formulas behind the counts: LL contiguous layers reach 1+L(k−1)1 + L(k-1) positions, and LL dilated layers reach kLk^L. For n=27n = 27, contiguous layers need ⌈26/2⌉=13\lceil 26 / 2 \rceil = 13, and dilated layers need log⁡327=3\log_3 27 = 3, because 33=273^3 = 27. Self-attention needs 1 layer for every nn.

The tables of links in the path section are one method to count these layers. A second method uses real layers. We build a stack of random one-sided convolutions and ask autograd whether the last output depends on the first input at all. If the gradient is exactly 0, no path exists.

python
def conv_stack_grad(n, L, dilated=False):
    """Method 2: a real stack of L random one-sided Conv1d layers (kernel 3). Is d out_n / d x_1 zero?"""
    g = torch.Generator().manual_seed(L)
    x = torch.randn(1, 8, n, generator=g, requires_grad=True)         # 8 channels, n positions
    h = x
    for l in range(L):
        dil = 3 ** l if dilated else 1
        w = torch.randn(8, 8, 3, generator=g)
        h = F.pad(h, (2 * dil, 0))                                     # pad on the left only: output t reads t, t - dil, t - 2 dil
        h = torch.tanh(F.conv1d(h, w, dilation=dil))
    h[0, :, n - 1].sum().backward()                                    # the last output ...
    return x.grad[0, :, 0].abs().sum().item()                          # ... and its gradient at the first input

for L, dil in [(12, False), (13, False), (2, True), (3, True)]:
    gsum = conv_stack_grad(27, L, dil)
plain text
Method 2: real stacks of random Conv1d layers, n = 27, |d out_27 / d x_1|
  contiguous 12 layers: 0.000e+00  (not connected)
  contiguous 13 layers: 7.305e-05  (connected)
  dilated     2 layers: 0.000e+00  (not connected)
  dilated     3 layers: 3.317e-02  (connected)

With 12 contiguous layers, the gradient is exactly 0: no path exists from word 1 to the output at word 27. With 13 layers, a path appears. The dilated stack connects in 3 layers and not in 2. Both methods give the counts of the formulas above.

With k=nk = n, the term k⋅n⋅dk \cdot n \cdot d becomes n2⋅dn^2 \cdot d, the cost of self-attention, and n⋅d2n \cdot d^2 is the cost of a point-wise feed-forward layer. The Transformer pays what the cheapest full-width convolution would pay, and gets a path of length 1.

A side benefit: we can look inside

The three wishes, scored

Put the three measures side by side for a typical sentence of 27 tokens and d=512d = 512:

layerwork of the corewaiting stepslongest path
self-attention373,24811
recurrent7,077,8882727
convolution, k=3k = 3, contiguous21,233,664113 layers
convolution, k=3k = 3, dilated21,233,66413 layers

For sentences, self-attention wins all three columns. That is the whole argument of Section 4. It does not say that attention is better for every input. For very long inputs, the n2n^2 cost returns, and the authors say so.

The paper and the code

Paper (arXiv 1706.03762) · Official code (tensor2tensor) · Our scripts for this series