stackformers 3.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. stackformers/__init__.py +136 -0
  2. stackformers/attention/README.md +21 -0
  3. stackformers/attention/__init__.py +16 -0
  4. stackformers/attention/bias.py +12 -0
  5. stackformers/attention/config.py +145 -0
  6. stackformers/attention/cross_attn.py +77 -0
  7. stackformers/attention/ops.py +167 -0
  8. stackformers/attention/protocols.py +44 -0
  9. stackformers/attention/self_attn.py +88 -0
  10. stackformers/config.py +26 -0
  11. stackformers/cross_attender.py +46 -0
  12. stackformers/decoder.py +57 -0
  13. stackformers/encoder.py +26 -0
  14. stackformers/feedforward/README.md +11 -0
  15. stackformers/feedforward/__init__.py +5 -0
  16. stackformers/feedforward/config.py +68 -0
  17. stackformers/feedforward/factory.py +24 -0
  18. stackformers/feedforward/geglu.py +35 -0
  19. stackformers/feedforward/protocols.py +16 -0
  20. stackformers/feedforward/relu_squared.py +28 -0
  21. stackformers/feedforward/swiglu.py +34 -0
  22. stackformers/layers.py +31 -0
  23. stackformers/norm/README.md +11 -0
  24. stackformers/norm/__init__.py +3 -0
  25. stackformers/norm/config.py +20 -0
  26. stackformers/norm/factory.py +18 -0
  27. stackformers/norm/protocols.py +16 -0
  28. stackformers/positional/README.md +13 -0
  29. stackformers/positional/__init__.py +23 -0
  30. stackformers/positional/config.py +65 -0
  31. stackformers/positional/factory.py +28 -0
  32. stackformers/positional/learned.py +49 -0
  33. stackformers/positional/none.py +32 -0
  34. stackformers/positional/protocols.py +32 -0
  35. stackformers/positional/rope1d.py +111 -0
  36. stackformers/positional/rope2d.py +59 -0
  37. stackformers/presets/README.md +63 -0
  38. stackformers/presets/__init__.py +39 -0
  39. stackformers/presets/cross_attender.py +108 -0
  40. stackformers/presets/decoder.py +126 -0
  41. stackformers/presets/encoder.py +150 -0
  42. stackformers/sequence.py +158 -0
  43. stackformers-3.8.0.dist-info/METADATA +204 -0
  44. stackformers-3.8.0.dist-info/RECORD +46 -0
  45. stackformers-3.8.0.dist-info/WHEEL +4 -0
  46. stackformers-3.8.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+
8
+ class YaRNConfig(BaseModel):
9
+ """NTK-by-parts RoPE frequency scaling (Peng et al., 2023).
10
+
11
+ Extends RoPE to longer contexts by scaling low-frequency dimensions
12
+ linearly while leaving high-frequency dimensions unchanged. The
13
+ inv_freq buffer is modified once at construction time so forward()
14
+ stays export-traceable.
15
+
16
+ scale: target_max_seq_len / original_max_seq_len
17
+ original_max_seq_len: context length the base model was trained on
18
+ beta_fast: high-freq threshold (wavelength = original / beta_fast)
19
+ beta_slow: low-freq threshold (wavelength = original / beta_slow)
20
+ """
21
+
22
+ scale: float = Field(gt=1.0)
23
+ original_max_seq_len: int = Field(gt=0)
24
+ beta_fast: float = Field(default=32.0, gt=0.0)
25
+ beta_slow: float = Field(default=1.0, gt=0.0)
26
+
27
+ @model_validator(mode="after")
28
+ def _check_beta_ordering(self) -> YaRNConfig:
29
+ if self.beta_slow >= self.beta_fast:
30
+ raise ValueError(
31
+ f"beta_slow ({self.beta_slow}) must be less than beta_fast ({self.beta_fast}). "
32
+ "Swapped values invert the high/low-frequency partition and silently break scaling."
33
+ )
34
+ return self
35
+
36
+
37
+ class RoPE1DConfig(BaseModel):
38
+ kind: Literal["rope1d"] = "rope1d"
39
+ dim_head: int = Field(gt=0)
40
+ base: int = Field(default=10_000, gt=0)
41
+ yarn: YaRNConfig | None = None
42
+
43
+
44
+ class RoPE2DConfig(BaseModel):
45
+ kind: Literal["rope2d"] = "rope2d"
46
+ dim_head: int = Field(gt=0)
47
+ base: int = Field(default=10_000, gt=0)
48
+
49
+
50
+ class NoPosEncodingConfig(BaseModel):
51
+ kind: Literal["none"] = "none"
52
+
53
+
54
+ class LearnedPosEncodingConfig(BaseModel):
55
+ """Config for learned absolute position embeddings (Vaswani et al., 2017)."""
56
+
57
+ kind: Literal["learned"] = "learned"
58
+ dim_head: int = Field(gt=0)
59
+ max_seq_len: int = Field(gt=0)
60
+
61
+
62
+ PosEncodingConfig = Annotated[
63
+ RoPE1DConfig | RoPE2DConfig | NoPosEncodingConfig | LearnedPosEncodingConfig,
64
+ Field(discriminator="kind"),
65
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from stackformers.positional.config import (
4
+ LearnedPosEncodingConfig,
5
+ NoPosEncodingConfig,
6
+ PosEncodingConfig,
7
+ RoPE1DConfig,
8
+ RoPE2DConfig,
9
+ )
10
+ from stackformers.positional.learned import LearnedPosEncoding
11
+ from stackformers.positional.none import NoPosEncoding
12
+ from stackformers.positional.protocols import PosEncoding
13
+ from stackformers.positional.rope1d import RotaryEmbedding1D
14
+ from stackformers.positional.rope2d import RotaryEmbedding2D
15
+
16
+
17
+ def build_pos_encoding(config: PosEncodingConfig) -> PosEncoding:
18
+ match config:
19
+ case RoPE1DConfig():
20
+ return RotaryEmbedding1D(config)
21
+ case RoPE2DConfig():
22
+ return RotaryEmbedding2D(config)
23
+ case NoPosEncodingConfig():
24
+ return NoPosEncoding(config)
25
+ case LearnedPosEncodingConfig():
26
+ return LearnedPosEncoding(config)
27
+ case _:
28
+ raise AssertionError(f"Unhandled pos encoding config: {type(config)}")
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import torch.nn as nn
4
+ from jaxtyping import Float
5
+ from torch import Tensor
6
+
7
+ from stackformers.positional.config import LearnedPosEncodingConfig
8
+
9
+
10
+ class LearnedPosEncoding(nn.Module):
11
+ """Learned absolute position embeddings added to query and key tensors.
12
+
13
+ Each position index is mapped to a learned dh-dimensional vector, which is
14
+ added to q and k after the head projection. Requires integer position indices
15
+ in positions[..., 0]; out-of-range indices raise an embedding error.
16
+
17
+ Section 3.5 of Vaswani et al., 2017 describes the learned variant alongside
18
+ sinusoidal encodings — https://arxiv.org/abs/1706.03762
19
+ """
20
+
21
+ def __init__(self, config: LearnedPosEncodingConfig) -> None:
22
+ super().__init__()
23
+ self.emb = nn.Embedding(config.max_seq_len, config.dim_head)
24
+ nn.init.normal_(self.emb.weight, std=0.02)
25
+
26
+ def _encode(
27
+ self, q: Tensor, k: Tensor, q_positions: Tensor, k_positions: Tensor
28
+ ) -> tuple[Tensor, Tensor]:
29
+ q_idx = q_positions[..., 0].long()
30
+ k_idx = k_positions[..., 0].long()
31
+ return q + self.emb(q_idx).unsqueeze(1), k + self.emb(k_idx).unsqueeze(1)
32
+
33
+ def forward_padded(
34
+ self,
35
+ q: Float[Tensor, "b h n dh"],
36
+ k: Float[Tensor, "b h s dh"],
37
+ q_positions: Float[Tensor, "b n c"],
38
+ k_positions: Float[Tensor, "b s c"],
39
+ ) -> tuple[Float[Tensor, "b h n dh"], Float[Tensor, "b h s dh"]]:
40
+ return self._encode(q, k, q_positions, k_positions)
41
+
42
+ def forward_packed(
43
+ self,
44
+ q: Float[Tensor, "nt h dh"],
45
+ k: Float[Tensor, "nt h dh"],
46
+ q_positions: Float[Tensor, "nt c"],
47
+ k_positions: Float[Tensor, "nt c"],
48
+ ) -> tuple[Float[Tensor, "nt h dh"], Float[Tensor, "nt h dh"]]:
49
+ return self._encode(q, k, q_positions, k_positions)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import torch.nn as nn
4
+ from jaxtyping import Float
5
+ from torch import Tensor
6
+
7
+ from stackformers.positional.config import NoPosEncodingConfig
8
+
9
+
10
+ class NoPosEncoding(nn.Module):
11
+ """Null object for PosEncoding — passes q, k unchanged regardless of layout."""
12
+
13
+ def __init__(self, _config: NoPosEncodingConfig = NoPosEncodingConfig()) -> None:
14
+ super().__init__()
15
+
16
+ def forward_padded(
17
+ self,
18
+ q: Float[Tensor, "b h n dh"],
19
+ k: Float[Tensor, "b h s dh"],
20
+ q_positions: Float[Tensor, "b n c"],
21
+ k_positions: Float[Tensor, "b s c"],
22
+ ) -> tuple[Float[Tensor, "b h n dh"], Float[Tensor, "b h s dh"]]:
23
+ return q, k
24
+
25
+ def forward_packed(
26
+ self,
27
+ q: Float[Tensor, "nt h dh"],
28
+ k: Float[Tensor, "nt h dh"],
29
+ q_positions: Float[Tensor, "nt c"],
30
+ k_positions: Float[Tensor, "nt c"],
31
+ ) -> tuple[Float[Tensor, "nt h dh"], Float[Tensor, "nt h dh"]]:
32
+ return q, k
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from jaxtyping import Float
6
+ from torch import Tensor
7
+
8
+
9
+ @runtime_checkable
10
+ class PosEncoding(Protocol):
11
+ """Apply positional encoding to projected query and key tensors.
12
+
13
+ Separate methods per layout so implementations contain no dispatch logic
14
+ and callers (which already know their layout) pick the right path directly.
15
+ Null implementation: NoPosEncoding.
16
+ """
17
+
18
+ def forward_padded(
19
+ self,
20
+ q: Float[Tensor, "b h n dh"],
21
+ k: Float[Tensor, "b h s dh"],
22
+ q_positions: Float[Tensor, "b n c"],
23
+ k_positions: Float[Tensor, "b s c"],
24
+ ) -> tuple[Float[Tensor, "b h n dh"], Float[Tensor, "b h s dh"]]: ...
25
+
26
+ def forward_packed(
27
+ self,
28
+ q: Float[Tensor, "nt h dh"],
29
+ k: Float[Tensor, "nt h dh"],
30
+ q_positions: Float[Tensor, "nt c"],
31
+ k_positions: Float[Tensor, "nt c"],
32
+ ) -> tuple[Float[Tensor, "nt h dh"], Float[Tensor, "nt h dh"]]: ...
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from jaxtyping import Float
8
+ from torch import Tensor
9
+
10
+ from stackformers.positional.config import RoPE1DConfig, YaRNConfig
11
+
12
+
13
+ def _rotate_half(x: Tensor) -> Tensor:
14
+ """Split x into [x1 | x2] halves and return [-x2 | x1]."""
15
+ half = x.shape[-1] // 2
16
+ x1, x2 = x[..., :half], x[..., half:]
17
+ return torch.cat((-x2, x1), dim=-1)
18
+
19
+
20
+ def _apply_rope_padded_unbatched(t: Tensor, freqs: Tensor) -> Tensor:
21
+ """t: b h n dh, freqs: n dh — (n dh) broadcasts over (b h n dh) without unsqueeze."""
22
+ cos = freqs.cos()
23
+ sin = freqs.sin()
24
+ return t * cos + _rotate_half(t) * sin
25
+
26
+
27
+ def _apply_rope(t: Tensor, freqs: Tensor) -> Tensor:
28
+ """Works for padded (b h n dh)+(b n dh) and packed (nt h dh)+(nt dh).
29
+
30
+ unsqueeze(1) inserts the head dim in both cases.
31
+ Cast to t.dtype so float32 freqs don't upcast a float16 input.
32
+ """
33
+ cos = freqs.cos().to(dtype=t.dtype).unsqueeze(1)
34
+ sin = freqs.sin().to(dtype=t.dtype).unsqueeze(1)
35
+ return t * cos + _rotate_half(t) * sin
36
+
37
+
38
+ def _yarn_inv_freq(
39
+ inv_freq: Tensor,
40
+ cfg: YaRNConfig,
41
+ ) -> Tensor:
42
+ """Return YaRN-scaled inv_freq (Peng et al., 2023, NTK-by-parts)."""
43
+ wavelen = 2.0 * math.pi / inv_freq
44
+ low_wavelen = cfg.original_max_seq_len / cfg.beta_slow
45
+ high_wavelen = cfg.original_max_seq_len / cfg.beta_fast
46
+ smooth = (
47
+ (cfg.original_max_seq_len / wavelen - cfg.beta_slow) / (cfg.beta_fast - cfg.beta_slow)
48
+ ).clamp(0.0, 1.0)
49
+ scaled = inv_freq / cfg.scale
50
+ blended = smooth * inv_freq + (1.0 - smooth) * scaled
51
+ return torch.where(
52
+ wavelen < high_wavelen, inv_freq, torch.where(wavelen > low_wavelen, scaled, blended)
53
+ )
54
+
55
+
56
+ class RotaryEmbedding1D(nn.Module):
57
+ """1-D Rotary Position Embedding (Su et al., 2021).
58
+
59
+ Optionally accepts YaRNConfig for extended context via NTK-by-parts scaling.
60
+ """
61
+
62
+ def __init__(self, config: RoPE1DConfig) -> None:
63
+ super().__init__()
64
+ assert config.dim_head % 2 == 0, "dim_head must be even for RoPE"
65
+ inv_freq = 1.0 / (
66
+ config.base ** (torch.arange(0, config.dim_head, 2).float() / config.dim_head)
67
+ )
68
+ if config.yarn is not None:
69
+ inv_freq = _yarn_inv_freq(inv_freq, config.yarn)
70
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
71
+
72
+ @torch.no_grad()
73
+ def _freqs_from_positions(self, positions: Tensor) -> Tensor:
74
+ """positions: (..., c) → freqs: (..., dh), uses first coordinate only.
75
+
76
+ float32 cast ensures half-precision inputs don't lose precision in the outer product.
77
+ """
78
+ inv: Tensor = self.inv_freq # type: ignore[assignment]
79
+ pos = positions[..., 0].to(dtype=torch.float32)
80
+ freqs = pos.unsqueeze(-1) * inv.float()
81
+ return torch.cat([freqs, freqs], dim=-1)
82
+
83
+ def _encode(
84
+ self,
85
+ q: Tensor,
86
+ k: Tensor,
87
+ q_positions: Tensor,
88
+ k_positions: Tensor,
89
+ ) -> tuple[Tensor, Tensor]:
90
+ return (
91
+ _apply_rope(q, self._freqs_from_positions(q_positions)),
92
+ _apply_rope(k, self._freqs_from_positions(k_positions)),
93
+ )
94
+
95
+ def forward_padded(
96
+ self,
97
+ q: Float[Tensor, "b h n dh"],
98
+ k: Float[Tensor, "b h s dh"],
99
+ q_positions: Float[Tensor, "b n c"],
100
+ k_positions: Float[Tensor, "b s c"],
101
+ ) -> tuple[Float[Tensor, "b h n dh"], Float[Tensor, "b h s dh"]]:
102
+ return self._encode(q, k, q_positions, k_positions)
103
+
104
+ def forward_packed(
105
+ self,
106
+ q: Float[Tensor, "nt h dh"],
107
+ k: Float[Tensor, "nt h dh"],
108
+ q_positions: Float[Tensor, "nt c"],
109
+ k_positions: Float[Tensor, "nt c"],
110
+ ) -> tuple[Float[Tensor, "nt h dh"], Float[Tensor, "nt h dh"]]:
111
+ return self._encode(q, k, q_positions, k_positions)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ from jaxtyping import Float
6
+ from torch import Tensor
7
+
8
+ from stackformers.positional.config import RoPE2DConfig
9
+ from stackformers.positional.rope1d import _apply_rope
10
+
11
+
12
+ class RotaryEmbedding2D(nn.Module):
13
+ """2-D Rotary Position Embedding for (row, col) positions.
14
+
15
+ Conforms to PosEncoding: positions must have c=2 (row, col per token).
16
+ Splits dim_head evenly: first half encodes row, second half column.
17
+ """
18
+
19
+ def __init__(self, config: RoPE2DConfig) -> None:
20
+ super().__init__()
21
+ assert config.dim_head % 4 == 0, "dim_head must be divisible by 4 for 2-D RoPE"
22
+ half_dh = config.dim_head // 2
23
+ inv_freq = 1.0 / (config.base ** (torch.arange(0, half_dh, 2).float() / half_dh))
24
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
25
+
26
+ @torch.no_grad()
27
+ def _freqs_from_positions(self, positions: Tensor) -> Tensor:
28
+ """positions: (..., 2) → freqs: (..., dh)"""
29
+ inv: Tensor = self.inv_freq # type: ignore[assignment]
30
+ row = positions[..., 0].to(dtype=inv.dtype).unsqueeze(-1) * inv # (..., dh//4)
31
+ col = positions[..., 1].to(dtype=inv.dtype).unsqueeze(-1) * inv
32
+ half = lambda f: torch.cat([f, f], dim=-1) # noqa: E731
33
+ return torch.cat([half(row), half(col)], dim=-1) # (..., dh)
34
+
35
+ def _encode(
36
+ self, q: Tensor, k: Tensor, q_positions: Tensor, k_positions: Tensor
37
+ ) -> tuple[Tensor, Tensor]:
38
+ return (
39
+ _apply_rope(q, self._freqs_from_positions(q_positions)),
40
+ _apply_rope(k, self._freqs_from_positions(k_positions)),
41
+ )
42
+
43
+ def forward_padded(
44
+ self,
45
+ q: Float[Tensor, "b h n dh"],
46
+ k: Float[Tensor, "b h s dh"],
47
+ q_positions: Float[Tensor, "b n c"],
48
+ k_positions: Float[Tensor, "b s c"],
49
+ ) -> tuple[Float[Tensor, "b h n dh"], Float[Tensor, "b h s dh"]]:
50
+ return self._encode(q, k, q_positions, k_positions)
51
+
52
+ def forward_packed(
53
+ self,
54
+ q: Float[Tensor, "nt h dh"],
55
+ k: Float[Tensor, "nt h dh"],
56
+ q_positions: Float[Tensor, "nt c"],
57
+ k_positions: Float[Tensor, "nt c"],
58
+ ) -> tuple[Float[Tensor, "nt h dh"], Float[Tensor, "nt h dh"]]:
59
+ return self._encode(q, k, q_positions, k_positions)
@@ -0,0 +1,63 @@
1
+ # presets
2
+
3
+ Opinionated `nn.Module` subclasses that wire up the building blocks with fixed structural choices. The caller controls all hyperparameters through a typed config — nothing is hardcoded.
4
+
5
+ For custom wiring, use the building blocks directly.
6
+
7
+ ## Class hierarchy
8
+
9
+ Each preset is split into two levels:
10
+
11
+ - **`*Base[C]`** — abstract generic class parameterised over any config type `C`. Defines `__init__` (wires the stack) and `forward`, declares `build_layers` and `build_norm` as abstract. No assumptions about `C`.
12
+ - **`*`** (concrete) — extends `*Base[ConcreteConfig]`, fills in all `build_*` defaults, and exposes additional overridable hooks (`build_pos_encoding`, `build_attn_bias`, `build_ff`, …).
13
+
14
+ ## Presets
15
+
16
+ **`TransformerEncoder`** — self-attn → ff per layer. Set `causal=True` in `attn` for GPT-style autoregressive decoding. Set `window_size` for sliding-window (O(n·w)) attention.
17
+
18
+ **`TransformerDecoder`** — causal self-attn → cross-attn → ff per layer. Cross-attention always uses no positional encoding.
19
+
20
+ **`CrossAttender`** — cross-attn → ff per layer. `x` is a fixed set of queries with no self-attention. Useful for Perceiver-style or slot-attention architectures.
21
+
22
+ All presets accept `PaddedInput` or `PackedInput` interchangeably.
23
+
24
+ ## Customising a collaborator (subclass the concrete preset)
25
+
26
+ Override one or more `build_*` methods; the rest use their defaults:
27
+
28
+ ```python
29
+ class ALiBiEncoder(TransformerEncoder):
30
+ def build_attn_bias(self, _: TransformerEncoderConfig) -> AttnBias:
31
+ return ALiBi(self.config.attn.heads)
32
+ ```
33
+
34
+ ## Using a fully custom config (subclass the abstract base)
35
+
36
+ Implement `build_layers` and `build_norm`; the base `__init__` calls them and nothing else:
37
+
38
+ ```python
39
+ class MyConfig(BaseModel):
40
+ dim: int
41
+ heads: int
42
+ num_layers: int
43
+
44
+ class MyEncoder(TransformerEncoderBase[MyConfig]):
45
+ def build_layers(self, config: MyConfig) -> list[TransformerLayer]:
46
+ pos = MyRoPE(config.dim // config.heads)
47
+ return [
48
+ TransformerLayer(
49
+ self_attn=SelfAttention(
50
+ SelfAttentionConfig(dim=config.dim, heads=config.heads,
51
+ dim_head=config.dim // config.heads),
52
+ pos_encoding=pos,
53
+ ),
54
+ ff=SwiGLU(SwiGLUConfig(dim=config.dim, mult=4)),
55
+ norm_attn=nn.RMSNorm(config.dim),
56
+ norm_ff=nn.RMSNorm(config.dim),
57
+ )
58
+ for _ in range(config.num_layers)
59
+ ]
60
+
61
+ def build_norm(self, config: MyConfig) -> Norm:
62
+ return nn.RMSNorm(config.dim)
63
+ ```
@@ -0,0 +1,39 @@
1
+ from stackformers.feedforward.factory import build_ff
2
+ from stackformers.norm.factory import NormConfig, build_norm
3
+ from stackformers.positional.factory import build_pos_encoding
4
+ from stackformers.presets.cross_attender import (
5
+ CrossAttender,
6
+ CrossAttenderConfig,
7
+ plain_cross_attender_config,
8
+ )
9
+ from stackformers.presets.decoder import (
10
+ TransformerDecoder,
11
+ TransformerDecoderConfig,
12
+ plain_decoder_config,
13
+ )
14
+ from stackformers.presets.encoder import (
15
+ TransformerEncoder,
16
+ TransformerEncoderConfig,
17
+ plain_encoder_config,
18
+ windowed_encoder_config,
19
+ )
20
+
21
+ __all__ = [
22
+ # builders
23
+ "NormConfig",
24
+ "build_norm",
25
+ "build_ff",
26
+ "build_pos_encoding",
27
+ # preset config factories
28
+ "plain_encoder_config",
29
+ "windowed_encoder_config",
30
+ "plain_decoder_config",
31
+ "plain_cross_attender_config",
32
+ # presets
33
+ "TransformerEncoderConfig",
34
+ "TransformerEncoder",
35
+ "TransformerDecoderConfig",
36
+ "TransformerDecoder",
37
+ "CrossAttenderConfig",
38
+ "CrossAttender",
39
+ ]
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Generic, TypeVar
5
+
6
+ import torch.nn as nn
7
+ from pydantic import BaseModel, Field
8
+ from torch import Tensor
9
+
10
+ from stackformers.attention.config import CrossAttentionConfig
11
+ from stackformers.attention.cross_attn import CrossAttention
12
+ from stackformers.cross_attender import CrossAttenderLayer, CrossAttenderStack
13
+ from stackformers.feedforward.config import FeedForwardConfig, SwiGLUConfig
14
+ from stackformers.feedforward.factory import build_ff
15
+ from stackformers.feedforward.protocols import FeedForward
16
+ from stackformers.norm.config import RMSNormConfig
17
+ from stackformers.norm.factory import NormConfig, build_norm
18
+ from stackformers.norm.protocols import Norm
19
+ from stackformers.positional.config import NoPosEncodingConfig, PosEncodingConfig
20
+ from stackformers.positional.factory import build_pos_encoding
21
+ from stackformers.positional.protocols import PosEncoding
22
+ from stackformers.sequence import SequenceInput
23
+
24
+ C = TypeVar("C")
25
+
26
+
27
+ class CrossAttenderBase(nn.Module, Generic[C], ABC):
28
+ """Abstract cross-attender: queries from x attend to context, no self-attention.
29
+
30
+ Subclass with any config type C. Implement build_layers and build_norm;
31
+ __init__ wires them into a CrossAttenderStack and nothing else.
32
+ """
33
+
34
+ def __init__(self, config: C) -> None:
35
+ super().__init__()
36
+ self.config = config
37
+ self._stack = CrossAttenderStack(
38
+ layers=self.build_layers(config),
39
+ final_norm=self.build_norm(config),
40
+ )
41
+
42
+ @abstractmethod
43
+ def build_layers(self, config: C) -> list[CrossAttenderLayer]: ...
44
+
45
+ @abstractmethod
46
+ def build_norm(self, config: C) -> Norm: ...
47
+
48
+ def forward(self, x_input: SequenceInput, ctx_input: SequenceInput) -> Tensor:
49
+ return self._stack(x_input, ctx_input)
50
+
51
+
52
+ class CrossAttenderConfig(BaseModel):
53
+ attn: CrossAttentionConfig
54
+ ff: FeedForwardConfig
55
+ norm: NormConfig
56
+ pos_encoding: PosEncodingConfig = NoPosEncodingConfig()
57
+ num_layers: int = Field(gt=0)
58
+
59
+
60
+ def plain_cross_attender_config(
61
+ dim: int,
62
+ heads: int,
63
+ num_layers: int,
64
+ *,
65
+ ff_mult: float = 4.0,
66
+ dropout: float = 0.0,
67
+ ) -> CrossAttenderConfig:
68
+ """Global SDPA cross-attender with RMSNorm and SwiGLU FF, no positional encoding.
69
+
70
+ Pass PaddedInput for inference, PackedInput for training — same model.
71
+ """
72
+ dim_head = dim // heads
73
+ return CrossAttenderConfig(
74
+ attn=CrossAttentionConfig(dim=dim, heads=heads, dim_head=dim_head, dropout=dropout),
75
+ ff=SwiGLUConfig(dim=dim, mult=ff_mult, dropout=dropout),
76
+ norm=RMSNormConfig(dim=dim),
77
+ num_layers=num_layers,
78
+ )
79
+
80
+
81
+ class CrossAttender(CrossAttenderBase[CrossAttenderConfig]):
82
+ """Concrete cross-attender for CrossAttenderConfig.
83
+
84
+ Pass PaddedInput for inference, PackedInput for training.
85
+ Override build_pos_encoding, build_ff, or build_norm to customise individual
86
+ collaborators while keeping the rest of the defaults.
87
+ """
88
+
89
+ def build_layers(self, config: CrossAttenderConfig) -> list[CrossAttenderLayer]:
90
+ pos = self.build_pos_encoding(config)
91
+ return [
92
+ CrossAttenderLayer(
93
+ cross_attn=CrossAttention(config=config.attn, pos_encoding=pos),
94
+ ff=self.build_ff(config),
95
+ norm_cross=build_norm(config.norm),
96
+ norm_ff=build_norm(config.norm),
97
+ )
98
+ for _ in range(config.num_layers)
99
+ ]
100
+
101
+ def build_pos_encoding(self, config: CrossAttenderConfig) -> PosEncoding:
102
+ return build_pos_encoding(config.pos_encoding)
103
+
104
+ def build_ff(self, config: CrossAttenderConfig) -> FeedForward:
105
+ return build_ff(config.ff)
106
+
107
+ def build_norm(self, config: CrossAttenderConfig) -> Norm:
108
+ return build_norm(config.norm)