class TinyCharLM(nnx.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, rngs=None):
rngs = nnx.Rngs(0) if rngs is None else rngs
self.num_heads, self.pos = num_heads, pos
init = nnx.initializers.normal(0.02)
self.token_emb = nnx.Embed(vocab_size, num_hiddens,
embedding_init=init, rngs=rngs)
if pos == 'learned':
self.pos_emb = nnx.Embed(max_len, num_hiddens,
embedding_init=init, rngs=rngs)
if pos == 'sinusoidal':
theta = jnp.arange(max_len)[:, None] / 10000 ** (
jnp.arange(0, num_hiddens, 2) / num_hiddens)
P = jnp.stack([jnp.sin(theta), jnp.cos(theta)], -1)
self.P = nnx.Cache(P.reshape(max_len, num_hiddens))
self.blks = nnx.List([nnx.Dict(
qkv=nnx.Linear(num_hiddens, 3 * num_hiddens, use_bias=bias,
rngs=rngs),
proj=nnx.Linear(num_hiddens, num_hiddens, use_bias=bias,
rngs=rngs))
for _ in range(num_blks)])
def _rope(self, x):
d = x.shape[-1]
pos = jnp.arange(x.shape[-2], dtype=jnp.float32)
inv_freq = 10000.0 ** (-jnp.arange(0, d, 2) / d)
theta = pos[:, None] * inv_freq[None, :]
cos, sin = jnp.cos(theta), jnp.sin(theta)
x1, x2 = x[..., 0::2], x[..., 1::2]
return jnp.stack([x1 * cos - x2 * sin,
x1 * sin + x2 * cos], -1).reshape(x.shape)
def _alibi(self, T):
h = jnp.arange(1, self.num_heads + 1)
slopes = 2.0 ** (-8.0 * h / self.num_heads)
pos = jnp.arange(T, dtype=jnp.float32)
return slopes[:, None, None] * (pos[None, :] - pos[:, None])
def _attend(self, blk, H):
B, T, D = H.shape
q, k, v = jnp.split(blk['qkv'](H), 3, axis=-1)
q, k, v = (u.reshape(B, T, self.num_heads, -1).swapaxes(1, 2)
for u in (q, k, v))
if self.pos == 'rope':
q, k = self._rope(q), self._rope(k)
scores = q @ k.swapaxes(-2, -1) / math.sqrt(q.shape[-1])
if self.pos == 'alibi':
scores = scores + self._alibi(T)
mask = jnp.triu(jnp.ones((T, T), dtype=bool), 1)
scores = jnp.where(mask, jnp.finfo(scores.dtype).min, scores)
weights = jax.nn.softmax(scores, axis=-1)
out = (weights @ v).swapaxes(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(jnp.arange(X.shape[1]))
if self.pos == 'sinusoidal':
H = H + self.P[:X.shape[1]]
return H
def __call__(self, X):
H = self._embed(X)
for blk in self.blks:
out, _ = self._attend(blk, H)
H = H + out
return self.token_emb.attend(H) # 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