class TinyCharLM(nn.Module):
"""Attention-only character-level language model."""
def __init__(self, vocab_size, num_hiddens=128, num_heads=4, num_blks=2,
pos='rope', max_len=512, bias=False):
super().__init__()
self.num_heads, self.pos = num_heads, pos
self.token_emb = nn.Embedding(vocab_size, num_hiddens)
nn.init.normal_(self.token_emb.weight, std=0.02)
if pos == 'learned':
self.pos_emb = nn.Embedding(max_len, num_hiddens)
nn.init.normal_(self.pos_emb.weight, std=0.02)
if pos == 'sinusoidal':
theta = torch.arange(max_len)[:, None] / 10000 ** (
torch.arange(0, num_hiddens, 2) / num_hiddens)
P = torch.stack([torch.sin(theta), torch.cos(theta)], -1)
self.register_buffer('P', P.reshape(max_len, num_hiddens))
self.blks = nn.ModuleList([nn.ModuleDict(dict(
qkv=nn.Linear(num_hiddens, 3 * num_hiddens, bias=bias),
proj=nn.Linear(num_hiddens, num_hiddens, bias=bias)))
for _ in range(num_blks)])
def _rope(self, x):
d = x.shape[-1]
pos = torch.arange(x.shape[-2], dtype=torch.float32, device=x.device)
inv_freq = 10000.0 ** (-torch.arange(0, d, 2, device=x.device) / d)
theta = pos[:, None] * inv_freq[None, :]
cos, sin = torch.cos(theta), torch.sin(theta)
x1, x2 = x[..., 0::2], x[..., 1::2]
return torch.stack([x1 * cos - x2 * sin,
x1 * sin + x2 * cos], -1).flatten(-2)
def _alibi(self, T, device):
h = torch.arange(1, self.num_heads + 1, device=device)
slopes = 2.0 ** (-8.0 * h / self.num_heads)
pos = torch.arange(T, device=device, dtype=torch.float32)
return slopes[:, None, None] * (pos[None, :] - pos[:, None])
def _attend(self, blk, H):
B, T, D = H.shape
q, k, v = blk['qkv'](H).chunk(3, -1)
q, k, v = (u.reshape(B, T, self.num_heads, -1).transpose(1, 2)
for u in (q, k, v))
if self.pos == 'rope':
q, k = self._rope(q), self._rope(k)
scores = q @ k.transpose(-2, -1) / math.sqrt(q.shape[-1])
if self.pos == 'alibi':
scores = scores + self._alibi(T, H.device)
mask = torch.triu(torch.ones(T, T, dtype=torch.bool,
device=H.device), 1)
scores = scores.masked_fill(mask, torch.finfo(scores.dtype).min)
weights = F.softmax(scores, dim=-1)
out = (weights @ v).transpose(1, 2).reshape(B, T, D)
return blk['proj'](out), weights
def _embed(self, X):
H = self.token_emb(X)
if self.pos == 'learned':
H = H + self.pos_emb(torch.arange(X.shape[1], device=X.device))
if self.pos == 'sinusoidal':
H = H + self.P[:X.shape[1]]
return H
def forward(self, X):
H = self._embed(X)
for blk in self.blks:
out, _ = self._attend(blk, H)
H = H + out
return F.linear(H, self.token_emb.weight) # Tied output head
def attention_weights(self, X):
"""Per-block attention maps, each (batch, num_heads, T, T)."""
H, maps = self._embed(X), []
for blk in self.blks:
out, weights = self._attend(blk, H)
maps.append(weights)
H = H + out
return maps