Adding a Core Layer
This guide walks through adding a new building block to core/ — for example, a new attention variant, MLP type, or normalization layer.
Step 1: Implement the NNX module in core/
All core modules are Flax NNX modules. Add a new file or extend an existing one:
# core/attention.py (or a new core/my_attention.py)
from flax import nnx
import jax.numpy as jnp
class LinearAttention(nnx.Module):
"""Linear (O(T)) attention via kernel feature maps.
Args:
dim: Hidden dimension.
n_heads: Number of attention heads.
rngs: Flax NNX random state.
"""
def __init__(self, dim: int, n_heads: int, rngs: nnx.Rngs) -> None:
self.q = nnx.Linear(dim, dim, use_bias=False, rngs=rngs)
self.k = nnx.Linear(dim, dim, use_bias=False, rngs=rngs)
self.v = nnx.Linear(dim, dim, use_bias=False, rngs=rngs)
self.n_heads = n_heads
def __call__(self, x: jnp.ndarray) -> jnp.ndarray:
"""Apply linear attention.
Args:
x: Input tensor of shape ``[B, T, D]``.
Returns:
Output tensor of shape ``[B, T, D]``.
"""
q = nnx.elu(self.q(x)) + 1.0 # positive feature map
k = nnx.elu(self.k(x)) + 1.0
v = self.v(x)
# O(T) linear attention
kv = jnp.einsum("btd, bte -> bde", k, v)
return jnp.einsum("btd, bde -> bte", q, kv)
Checklist:
Google-style docstring on the class and every public method
Type annotations on
__init__and__call__No mutable Python state (side-effect-free, XLA-compatible)
Step 2: Add a config field
Config fields are the single source of truth for architecture choices:
# core/config.py
@dataclass
class ModelConfig:
...
use_linear_attn: bool = False # Enable O(T) linear attention
Step 3: Wire it into Block or Transformer
# core/block.py
from dantinox.core.attention import Attention, LinearAttention
class Block(nnx.Module):
def __init__(self, config: ModelConfig, rngs: nnx.Rngs) -> None:
if config.use_linear_attn:
self.attn = LinearAttention(config.dim, config.n_heads, rngs=rngs)
else:
self.attn = Attention(config, rngs=rngs)
...
Step 4: Write a unit test
# tests/test_linear_attention.py
import pytest
import jax
import jax.numpy as jnp
from flax import nnx
from dantinox.core.attention import LinearAttention
def test_linear_attention_output_shape():
rngs = nnx.Rngs(0)
layer = LinearAttention(dim=64, n_heads=4, rngs=rngs)
x = jnp.ones((2, 16, 64))
y = layer(x)
assert y.shape == (2, 16, 64)
def test_linear_attention_deterministic():
rngs = nnx.Rngs(0)
layer = LinearAttention(dim=64, n_heads=4, rngs=rngs)
x = jax.random.normal(jax.random.PRNGKey(1), (1, 8, 64))
assert jnp.allclose(layer(x), layer(x))
Run: pytest tests/test_linear_attention.py -v
Step 5: Update the default config YAML (optional)
# configs/default_config.yaml
model:
use_linear_attn: false # New field — default off for backward compat
Checklist summary
NNX module with Google docstrings + type annotations
Config field with a sensible default
Wired into
BlockorTransformervia the config fieldAt least one unit test covering shape + determinism
YAML config updated with the new field
interrogatepasses:make doccheck