def attention(x, w_qkv, w_o, n_heads):
B, N, D = x.shape
q, k, v = (x @ w_qkv).view(B, N, 3, n_heads, D // n_heads).permute(2, 0, 3, 1, 4)
att = (q @ k.transpose(-2, -1)) / math.sqrt(D // n_heads)
mask = torch.tril(torch.ones(N, N, dtype=torch.bool, device=x.device))
att = att.masked_fill(~mask, float("-inf")).softmax(dim=-1)
return (att @ v).transpose(1, 2).reshape(B, N, D) @ w_o
ShapeScript:
dim D = 768
dim H = 12
dim Dh = D / H
fn attention(x: [b n D], w_qkv: [D 3*D], w_o: [D D]) -> [b n D]:
q, k, v = x @ w_qkv
|> split(b n (3 H Dh) -> b H n Dh)
att = einsum(q, k: b H n Dh, b H m Dh -> b H n m)
|> scale(1 / sqrt(Dh))
|> masked_fill(!causal[n m], -inf)
|> softmax(over=m)
return einsum(att, v: b H n m, b H m Dh -> b H n Dh)
|> rearrange(b H n Dh -> b n (H Dh))
|> matmul(w_o)
The PyTorch version has a view, a permute, a transpose and a reshape, each of which could be wrong while still running. The ShapeScript version has the same steps, each checked.