FlashAttention with Trainable Bias Terms

Background

Until now, the official FlashAttention implementation does not support a bias term. FlexAttention in torch is trying to support the bias term now. In this blog, I will show how to implement a minimal FlashAttention with a trainable bias term.

Attention

The attention with gradient-enabled bias term is defined as:

๐‘ถ=softmax(๐‘ธ๐‘ฒ๐‘‡๐‘‘+๐‘ฉ)๐‘ฝ

where

  • ๐‘ฉ is the bias term and the shape is (๐‘›,โ„Ž,๐‘™,๐‘™)
  • The shape of ๐‘ธ,๐‘ฒ,๐‘ฝ is (๐‘›,โ„Ž,๐‘™,๐‘‘)
  • ๐‘› is batch size, โ„Ž is number of heads, ๐‘™ is sequence length, ๐‘‘ is hidden dimension.

The gradient of ๐‘ฉ is accumulated during the training process.

Backprop Derivation

Let

๐‘บ=๐‘ธ๐‘ฒ๐‘‡๐‘‘+๐‘ฉ๐‘จ=softmax(๐‘บ)=softmax(๐‘ธ๐‘ฒ๐‘‡๐‘‘+๐‘ฉ)๐‘ถ=๐‘จ๐‘ฝ=softmax(๐‘บ)๐‘ฝ=softmax(๐‘ธ๐‘ฒ๐‘‡๐‘‘+๐‘ฉ)๐‘ฝ

We already have the gradient with respect to ๐‘ถ:

๐œ•โ„’๏ธ€๐œ•๐‘ถ([๐‘›,โ„Ž,๐‘™,๐‘‘]).

In the following, we think of each (๐‘›,โ„Ž) slice as a separate matrix multiply.

Gradient of ๐‘ฝ and ๐‘จ

Since

๐‘ถ=๐‘จ๐‘ฝ([๐‘›,โ„Ž,๐‘™,๐‘‘]=[๐‘›,โ„Ž,๐‘™,๐‘™]ร—[๐‘›,โ„Ž,๐‘™,๐‘‘])

, we get

๐œ•โ„’๏ธ€๐œ•๐‘จ=๐œ•โ„’๏ธ€๐œ•๐‘ถbmm(๐‘ฝ๐‘‡),([๐‘›,โ„Ž,๐‘™,๐‘™]=[๐‘›,โ„Ž,๐‘™,๐‘™]ร—[๐‘›,โ„Ž,๐‘™,๐‘‘])๐œ•โ„’๏ธ€๐œ•๐‘ฝ=๐‘จ๐‘‡bmm๐œ•โ„’๏ธ€๐œ•๐‘ถ,([๐‘›,โ„Ž,๐‘™,๐‘‘]=[๐‘›,โ„Ž,๐‘™,๐‘™]ร—[๐‘›,โ„Ž,๐‘™,๐‘‘])

Gradient of ๐‘บ

It is easy to get the gradient of ๐‘บ based on chain rule:

๐œ•โ„’๏ธ€(๐œ•๐‘บ)๐‘–๐‘—๐‘˜๐‘™=โˆ‘๐‘š,๐‘›๐œ•๐‘จ๐‘–๐‘—๐‘š๐‘›๐œ•๐‘บ๐‘–๐‘—๐‘˜๐‘™๐œ•โ„’๏ธ€๐œ•๐‘จ๐‘–๐‘—๐‘š๐‘›

where ๐œ•๐‘จ๐œ•๐‘บ is the Jacobian of softmax function and has size (๐‘›,โ„Ž,๐‘™,๐‘™,๐‘™,๐‘™). ๐‘–,๐‘—,๐‘˜,๐‘™: Indices of the target tensor ๐œ•โ„’๏ธ€๐œ•๐‘บ. ๐‘š,๐‘›: Summation indices, specifying contraction over these dimensions. The โˆ‘๐‘š,๐‘› explicitly indicates summation over the indices ๐‘š and ๐‘›.

For efficiency, we can rewrite the above equation as:

๐œ•โ„’๏ธ€๐œ•๐‘บ=๐‘จโˆ—(๐œ•โ„’๏ธ€๐œ•๐‘จโˆ’(๐‘จโˆ—๐œ•โ„’๏ธ€๐œ•๐‘จ)rowsum)([๐‘›,โ„Ž,๐‘™,๐‘™]=[๐‘›,โ„Ž,๐‘™,๐‘™]โˆ—([๐‘›,โ„Ž,๐‘™,๐‘™]โˆ’[๐‘›,โ„Ž,๐‘™,1]))

where โˆ— denotes element-wise multiplication and rowsum sums over the last dimension.

Gradient of ๐‘ฉ

The gradient of ๐‘ฉ is the same as the gradient of ๐‘บ, which is:

๐œ•โ„’๏ธ€๐œ•๐‘ฉ=๐œ•โ„’๏ธ€๐œ•๐‘บ

Gradient of ๐‘ธ, ๐‘ฒ

The gradient of ๐‘ธ and ๐‘ฒ is:

๐œ•โ„’๏ธ€๐œ•๐‘ธ=๐œ•โ„’๏ธ€๐œ•๐‘บโ‹…๐‘ฒ๐œ•โ„’๏ธ€๐œ•๐‘ฒ=๐œ•โ„’๏ธ€๐œ•๐‘บโ‹…๐‘ธ

All gradients

๐œ•โ„’๏ธ€๐œ•๐‘ธ=๐œ•โ„’๏ธ€๐œ•๐‘บโ‹…๐‘ฒ๐œ•โ„’๏ธ€๐œ•๐‘ฒ=๐œ•โ„’๏ธ€๐œ•๐‘บโ‹…๐‘ธ๐œ•โ„’๏ธ€๐œ•๐‘ฝ=๐‘จ๐‘‡bmm๐œ•โ„’๏ธ€๐œ•๐‘ถ๐œ•โ„’๏ธ€๐œ•๐‘จ=๐œ•โ„’๏ธ€๐œ•๐‘ถbmm(๐‘ฝ๐‘‡)๐œ•โ„’๏ธ€๐œ•๐‘บ=๐‘จโˆ—(๐œ•โ„’๏ธ€๐œ•๐‘จโˆ’(๐‘จโˆ—๐œ•โ„’๏ธ€๐œ•๐‘จ)rowsum)๐œ•โ„’๏ธ€๐œ•๐‘ฉ=๐œ•โ„’๏ธ€๐œ•๐‘บ

PyTorch implementation

import torch

def forward(Q, K, V, B, d):
    S = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d, dtype=torch.float32)) + B
    A = torch.softmax(S, dim=-1)
    O = torch.matmul(A, V)
    return O, A, S

@torch.no_grad
def compute_gradients(Q, K, V, B, d, dO):
    # Compute forward pass
    S = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d, dtype=torch.float32)) + B
    A = torch.softmax(S, dim=-1)
    O = torch.matmul(A, V)

    # Gradient of V and A
    dA = torch.matmul(dO, V.transpose(-2, -1))
    dV = torch.matmul(A.transpose(-2, -1), dO)

    # Gradient of S using Jacobian-vector product (JVP)
    dS = dA * A - (A * dA).sum(dim=-1, keepdim=True) * A
    # dS = dA * A - torch.matmul(dA * A, A.transpose(-2, -1))

    # Gradient of B (same as dS)
    dB = dS.clone()

    # Gradient of Q and K
    dQ = torch.matmul(dS, K) / torch.sqrt(torch.tensor(d, dtype=torch.float32))
    dK = torch.matmul(dS.transpose(-2, -1), Q) / torch.sqrt(torch.tensor(d, dtype=torch.float32))

    return dQ, dK, dV, dB


# Example usage
n, h, l, d = 2, 4, 8, 16
torch.manual_seed(0)
Q = torch.randn(n, h, l, d, requires_grad=True)
K = torch.randn(n, h, l, d, requires_grad=True)
V = torch.randn(n, h, l, d, requires_grad=True)
B = torch.randn(n, h, l, l, requires_grad=True)
dO = torch.randn(n, h, l, d)

O, A, S = forward(Q, K, V, B, d)
dQ, dK, dV, dB = compute_gradients(Q, K, V, B, d, dO)

# Verify correctness with autograd
O.backward(dO, retain_graph=True)




print( V.grad[0][0][0])
print( dV[0][0][0]  )

print( B.grad[0][0][0])
print( dB[0][0][0]  )

print( Q.grad[0][0][0])
print( dQ[0][0][0]  )



assert torch.allclose(V.grad, dV, atol=1e-5), "dV mismatch"
assert torch.allclose(B.grad, dB, atol=1e-5), "dB mismatch"
assert torch.allclose(Q.grad, dQ, atol=1e-5), "dQ mismatch"
assert torch.allclose(K.grad, dK, atol=1e-5), "dK mismatch"


print("Autograd verification passed.")

print("O:", O.shape)
print("dQ:", dQ.shape)
print("dK:", dK.shape)
print("dV:", dV.shape)
print("dB:", dB.shape)

Output:

tensor([-0.9583, -0.7990, -0.7401,  0.4045, -1.1326, -0.8535,  0.9846,  0.8070,
        -0.6478, -0.0538,  0.6266,  1.0380, -0.9200,  0.5653,  0.9200, -0.0638])
tensor([-0.9583, -0.7990, -0.7401,  0.4045, -1.1326, -0.8535,  0.9846,  0.8070,
        -0.6478, -0.0538,  0.6266,  1.0380, -0.9200,  0.5653,  0.9200, -0.0638])
tensor([-8.4880e-02, -6.7330e-01, -5.2291e-04,  3.3246e-02, -2.7012e-02,
         5.0888e-01,  2.4558e-01, -1.9837e-03])
tensor([-8.4880e-02, -6.7330e-01, -5.2293e-04,  3.3246e-02, -2.7012e-02,
         5.0888e-01,  2.4558e-01, -1.9838e-03])
tensor([-0.1274, -0.2580,  0.2316,  0.1266, -0.3056,  0.0579, -0.2824,  0.2191,
        -0.0199,  0.2176, -0.0755, -0.1700,  0.1564,  0.2221, -0.0909,  0.0172])
tensor([-0.1274, -0.2580,  0.2316,  0.1266, -0.3056,  0.0579, -0.2824,  0.2191,
        -0.0199,  0.2176, -0.0755, -0.1700,  0.1564,  0.2221, -0.0909,  0.0172])
Autograd verification passed.
O: torch.Size([2, 4, 8, 16])
dQ: torch.Size([2, 4, 8, 16])
dK: torch.Size([2, 4, 8, 16])
dV: torch.Size([2, 4, 8, 16])
dB: torch.Size([2, 4, 8, 8])