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,126 @@
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.bias import NoAttnBias
11
+ from stackformers.attention.config import CrossAttentionConfig, SelfAttentionConfig
12
+ from stackformers.attention.cross_attn import CrossAttention
13
+ from stackformers.attention.protocols import AttnBias
14
+ from stackformers.attention.self_attn import SelfAttention
15
+ from stackformers.decoder import Decoder, DecoderLayer
16
+ from stackformers.feedforward.config import FeedForwardConfig, SwiGLUConfig
17
+ from stackformers.feedforward.factory import build_ff
18
+ from stackformers.feedforward.protocols import FeedForward
19
+ from stackformers.norm.config import RMSNormConfig
20
+ from stackformers.norm.factory import NormConfig, build_norm
21
+ from stackformers.norm.protocols import Norm
22
+ from stackformers.positional.config import NoPosEncodingConfig, PosEncodingConfig, RoPE1DConfig
23
+ from stackformers.positional.factory import build_pos_encoding
24
+ from stackformers.positional.none import NoPosEncoding
25
+ from stackformers.positional.protocols import PosEncoding
26
+ from stackformers.sequence import SequenceInput
27
+
28
+ C = TypeVar("C")
29
+
30
+
31
+ class TransformerDecoderBase(nn.Module, Generic[C], ABC):
32
+ """Abstract decoder: causal self-attn → cross-attn → ff per layer.
33
+
34
+ Subclass with any config type C. Implement build_layers and build_norm;
35
+ __init__ wires them into a Decoder and nothing else.
36
+ """
37
+
38
+ def __init__(self, config: C) -> None:
39
+ super().__init__()
40
+ self.config = config
41
+ self._decoder = Decoder(
42
+ layers=self.build_layers(config),
43
+ final_norm=self.build_norm(config),
44
+ )
45
+
46
+ @abstractmethod
47
+ def build_layers(self, config: C) -> list[DecoderLayer]: ...
48
+
49
+ @abstractmethod
50
+ def build_norm(self, config: C) -> Norm: ...
51
+
52
+ def forward(self, x_input: SequenceInput, ctx_input: SequenceInput) -> Tensor:
53
+ return self._decoder(x_input, ctx_input)
54
+
55
+
56
+ class TransformerDecoderConfig(BaseModel):
57
+ self_attn: SelfAttentionConfig
58
+ cross_attn: CrossAttentionConfig
59
+ ff: FeedForwardConfig
60
+ norm: NormConfig
61
+ pos_encoding: PosEncodingConfig # applies to self-attention only
62
+ num_layers: int = Field(gt=0)
63
+
64
+
65
+ def plain_decoder_config(
66
+ dim: int,
67
+ heads: int,
68
+ num_layers: int,
69
+ *,
70
+ ff_mult: float = 4.0,
71
+ dropout: float = 0.0,
72
+ ) -> TransformerDecoderConfig:
73
+ """Causal self-attn + cross-attn decoder with RoPE-1D, RMSNorm, SwiGLU FF."""
74
+ dim_head = dim // heads
75
+ return TransformerDecoderConfig(
76
+ self_attn=SelfAttentionConfig(
77
+ dim=dim, heads=heads, dim_head=dim_head, causal=True, dropout=dropout
78
+ ),
79
+ cross_attn=CrossAttentionConfig(dim=dim, heads=heads, dim_head=dim_head, dropout=dropout),
80
+ ff=SwiGLUConfig(dim=dim, mult=ff_mult, dropout=dropout),
81
+ norm=RMSNormConfig(dim=dim),
82
+ pos_encoding=RoPE1DConfig(dim_head=dim_head),
83
+ num_layers=num_layers,
84
+ )
85
+
86
+
87
+ class TransformerDecoder(TransformerDecoderBase[TransformerDecoderConfig]):
88
+ """Concrete decoder for TransformerDecoderConfig.
89
+
90
+ Override build_self_pos_encoding, build_self_attn_bias, build_ff, or build_norm
91
+ to customise individual collaborators while keeping the rest of the defaults.
92
+ """
93
+
94
+ def build_layers(self, config: TransformerDecoderConfig) -> list[DecoderLayer]:
95
+ self_pos = self.build_self_pos_encoding(config)
96
+ self_bias = self.build_self_attn_bias(config)
97
+ return [
98
+ DecoderLayer(
99
+ self_attn=SelfAttention(
100
+ config=config.self_attn,
101
+ pos_encoding=self_pos,
102
+ attn_bias=self_bias,
103
+ ),
104
+ cross_attn=CrossAttention(
105
+ config=config.cross_attn,
106
+ pos_encoding=NoPosEncoding(NoPosEncodingConfig()),
107
+ ),
108
+ ff=self.build_ff(config),
109
+ norm_self=build_norm(config.norm),
110
+ norm_cross=build_norm(config.norm),
111
+ norm_ff=build_norm(config.norm),
112
+ )
113
+ for _ in range(config.num_layers)
114
+ ]
115
+
116
+ def build_self_pos_encoding(self, config: TransformerDecoderConfig) -> PosEncoding:
117
+ return build_pos_encoding(config.pos_encoding)
118
+
119
+ def build_self_attn_bias(self, _: TransformerDecoderConfig) -> AttnBias:
120
+ return NoAttnBias()
121
+
122
+ def build_ff(self, config: TransformerDecoderConfig) -> FeedForward:
123
+ return build_ff(config.ff)
124
+
125
+ def build_norm(self, config: TransformerDecoderConfig) -> Norm:
126
+ return build_norm(config.norm)
@@ -0,0 +1,150 @@
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.bias import NoAttnBias
11
+ from stackformers.attention.config import SelfAttentionConfig
12
+ from stackformers.attention.protocols import AttnBias
13
+ from stackformers.attention.self_attn import SelfAttention
14
+ from stackformers.encoder import Encoder
15
+ from stackformers.feedforward.config import FeedForwardConfig, SwiGLUConfig
16
+ from stackformers.feedforward.factory import build_ff
17
+ from stackformers.feedforward.protocols import FeedForward
18
+ from stackformers.layers import TransformerLayer
19
+ from stackformers.norm.config import RMSNormConfig
20
+ from stackformers.norm.factory import NormConfig, build_norm
21
+ from stackformers.norm.protocols import Norm
22
+ from stackformers.positional.config import PosEncodingConfig, RoPE1DConfig
23
+ from stackformers.positional.factory import build_pos_encoding
24
+ from stackformers.positional.protocols import PosEncoding
25
+ from stackformers.sequence import SequenceInput
26
+
27
+ C = TypeVar("C")
28
+
29
+
30
+ class TransformerEncoderBase(nn.Module, Generic[C], ABC):
31
+ """Abstract encoder: self-attn → ff per layer.
32
+
33
+ Subclass with any config type C. Implement build_layers and build_norm;
34
+ __init__ wires them into an Encoder and nothing else.
35
+ """
36
+
37
+ def __init__(self, config: C) -> None:
38
+ super().__init__()
39
+ self.config = config
40
+ self._encoder = Encoder(
41
+ layers=self.build_layers(config),
42
+ final_norm=self.build_norm(config),
43
+ )
44
+
45
+ @abstractmethod
46
+ def build_layers(self, config: C) -> list[TransformerLayer]: ...
47
+
48
+ @abstractmethod
49
+ def build_norm(self, config: C) -> Norm: ...
50
+
51
+ def forward(self, input: SequenceInput) -> Tensor:
52
+ return self._encoder(input)
53
+
54
+
55
+ class TransformerEncoderConfig(BaseModel):
56
+ attn: SelfAttentionConfig
57
+ ff: FeedForwardConfig
58
+ norm: NormConfig
59
+ pos_encoding: PosEncodingConfig
60
+ num_layers: int = Field(gt=0)
61
+
62
+
63
+ def plain_encoder_config(
64
+ dim: int,
65
+ heads: int,
66
+ num_layers: int,
67
+ *,
68
+ causal: bool = False,
69
+ ff_mult: float = 4.0,
70
+ dropout: float = 0.0,
71
+ ) -> TransformerEncoderConfig:
72
+ """Global SDPA encoder with RoPE-1D, RMSNorm, and SwiGLU FF.
73
+
74
+ Pass PaddedInput for inference, PackedInput for training — same model.
75
+ """
76
+ dim_head = dim // heads
77
+ return TransformerEncoderConfig(
78
+ attn=SelfAttentionConfig(
79
+ dim=dim, heads=heads, dim_head=dim_head, causal=causal, dropout=dropout
80
+ ),
81
+ ff=SwiGLUConfig(dim=dim, mult=ff_mult, dropout=dropout),
82
+ norm=RMSNormConfig(dim=dim),
83
+ pos_encoding=RoPE1DConfig(dim_head=dim_head),
84
+ num_layers=num_layers,
85
+ )
86
+
87
+
88
+ def windowed_encoder_config(
89
+ dim: int,
90
+ heads: int,
91
+ num_layers: int,
92
+ window_size: int,
93
+ *,
94
+ causal: bool = False,
95
+ ff_mult: float = 4.0,
96
+ dropout: float = 0.0,
97
+ ) -> TransformerEncoderConfig:
98
+ """Sliding-window encoder — O(n·w) attention for long sequences.
99
+
100
+ Pass PaddedInput for inference, PackedInput for training — same model.
101
+ """
102
+ dim_head = dim // heads
103
+ return TransformerEncoderConfig(
104
+ attn=SelfAttentionConfig(
105
+ dim=dim,
106
+ heads=heads,
107
+ dim_head=dim_head,
108
+ causal=causal,
109
+ dropout=dropout,
110
+ window_size=window_size,
111
+ ),
112
+ ff=SwiGLUConfig(dim=dim, mult=ff_mult, dropout=dropout),
113
+ norm=RMSNormConfig(dim=dim),
114
+ pos_encoding=RoPE1DConfig(dim_head=dim_head),
115
+ num_layers=num_layers,
116
+ )
117
+
118
+
119
+ class TransformerEncoder(TransformerEncoderBase[TransformerEncoderConfig]):
120
+ """Concrete encoder for TransformerEncoderConfig.
121
+
122
+ Pass PaddedInput for inference, PackedInput for training.
123
+ Override build_pos_encoding, build_attn_bias, build_ff, or build_norm to customise
124
+ individual collaborators while keeping the rest of the defaults.
125
+ """
126
+
127
+ def build_layers(self, config: TransformerEncoderConfig) -> list[TransformerLayer]:
128
+ pos = self.build_pos_encoding(config)
129
+ bias = self.build_attn_bias(config)
130
+ return [
131
+ TransformerLayer(
132
+ self_attn=SelfAttention(config=config.attn, pos_encoding=pos, attn_bias=bias),
133
+ ff=self.build_ff(config),
134
+ norm_attn=build_norm(config.norm),
135
+ norm_ff=build_norm(config.norm),
136
+ )
137
+ for _ in range(config.num_layers)
138
+ ]
139
+
140
+ def build_pos_encoding(self, config: TransformerEncoderConfig) -> PosEncoding:
141
+ return build_pos_encoding(config.pos_encoding)
142
+
143
+ def build_attn_bias(self, _: TransformerEncoderConfig) -> AttnBias:
144
+ return NoAttnBias()
145
+
146
+ def build_ff(self, config: TransformerEncoderConfig) -> FeedForward:
147
+ return build_ff(config.ff)
148
+
149
+ def build_norm(self, config: TransformerEncoderConfig) -> Norm:
150
+ return build_norm(config.norm)
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NamedTuple
4
+
5
+ import torch
6
+ from jaxtyping import Bool, Float, Int
7
+ from torch import Tensor
8
+
9
+ # NamedTuple instead of dataclass: PyTorch's pytree system handles tuples natively,
10
+ # so torch.export and torch.compile see the tensor fields without any registration.
11
+ # Dataclasses require explicit pytree.register_dataclass() to achieve the same.
12
+
13
+
14
+ class PaddedSequence(NamedTuple):
15
+ """Batch of padded sequences; mask marks valid (non-padding) positions."""
16
+
17
+ mask: Bool[Tensor, "b n"] # True = valid token
18
+
19
+
20
+ class PackedSequence(NamedTuple):
21
+ """Variable-length sequences packed into a single flat tensor.
22
+
23
+ cu_seqlens[i] is the cumulative token count up to (not including) sequence i.
24
+ Follows the FlashAttention convention: length[i] = cu_seqlens[i+1] - cu_seqlens[i].
25
+ """
26
+
27
+ cu_seqlens: Int[Tensor, "bp1"] # shape (batch + 1,)
28
+ max_seqlen: int
29
+
30
+
31
+ SequenceInfo = PaddedSequence | PackedSequence
32
+
33
+
34
+ class PaddedInput(NamedTuple):
35
+ x: Float[Tensor, "b n d"]
36
+ mask: Bool[Tensor, "b n"]
37
+ abs_positions: Float[Tensor, "b n c"] # c=1 for 1-D (text), c=2 for grids, c=3 for 3-D
38
+
39
+
40
+ class PackedInput(NamedTuple):
41
+ x: Float[Tensor, "nt d"]
42
+ cu_seqlens: Int[Tensor, "bp1"]
43
+ max_seqlen: int
44
+ abs_positions: Float[Tensor, "nt c"] # c matches PaddedInput convention
45
+
46
+
47
+ SequenceInput = PaddedInput | PackedInput
48
+
49
+
50
+ def to_seq_info(inp: SequenceInput) -> SequenceInfo:
51
+ match inp:
52
+ case PaddedInput(mask=mask):
53
+ return PaddedSequence(mask=mask)
54
+ case PackedInput(cu_seqlens=cu, max_seqlen=ms):
55
+ return PackedSequence(cu_seqlens=cu, max_seqlen=ms)
56
+
57
+
58
+ def make_padded_input(x: Tensor, mask: Bool[Tensor, "b n"]) -> PaddedInput:
59
+ """Build PaddedInput with sequential 1-D positions (shape b n 1)."""
60
+ n = x.shape[1]
61
+ pos = torch.arange(n, device=x.device, dtype=x.dtype)
62
+ positions = pos.unsqueeze(0).unsqueeze(-1).expand(x.shape[0], -1, -1) # b n 1
63
+ return PaddedInput(x=x, mask=mask, abs_positions=positions)
64
+
65
+
66
+ def make_packed_input(x: Tensor, cu_seqlens: Int[Tensor, "bp1"], max_seqlen: int) -> PackedInput:
67
+ """Build PackedInput with per-token sequential 1-D positions (shape nt 1).
68
+
69
+ Delegates to :func:`position_ids_from_packed`, which is ``torch.export``-compatible.
70
+ """
71
+ seq = PackedSequence(cu_seqlens=cu_seqlens, max_seqlen=max_seqlen)
72
+ positions = position_ids_from_packed(seq).to(x.dtype).unsqueeze(-1) # nt 1
73
+ return PackedInput(x=x, cu_seqlens=cu_seqlens, max_seqlen=max_seqlen, abs_positions=positions)
74
+
75
+
76
+ def padded_to_key_padding_mask(seq: PaddedSequence) -> Bool[Tensor, "b n"]:
77
+ """Return True where tokens are valid (mirrors seq.mask)."""
78
+ return seq.mask
79
+
80
+
81
+ def packed_batch_size(seq: PackedSequence) -> int:
82
+ return int(seq.cu_seqlens.shape[0]) - 1
83
+
84
+
85
+ def make_padded(mask: Bool[Tensor, "b n"]) -> PaddedSequence:
86
+ return PaddedSequence(mask=mask)
87
+
88
+
89
+ def make_packed(cu_seqlens: Int[Tensor, "bp1"], max_seqlen: int) -> PackedSequence:
90
+ return PackedSequence(cu_seqlens=cu_seqlens, max_seqlen=max_seqlen)
91
+
92
+
93
+ def padded_to_packed(inp: PaddedInput) -> PackedInput:
94
+ """Remove padding tokens from a padded batch, producing a flat packed tensor.
95
+
96
+ Output shape is data-dependent (nt = mask.sum()); not compatible with torch.export.
97
+ """
98
+ lengths = inp.mask.sum(dim=1)
99
+ cu = lengths_to_cu_seqlens(lengths)
100
+ max_seqlen = int(lengths.max().item())
101
+ return PackedInput(
102
+ x=inp.x[inp.mask],
103
+ cu_seqlens=cu,
104
+ max_seqlen=max_seqlen,
105
+ abs_positions=inp.abs_positions[inp.mask],
106
+ )
107
+
108
+
109
+ def packed_to_padded(inp: PackedInput) -> PaddedInput:
110
+ """Re-pad a packed tensor to (b, max_seqlen, d), filling unused positions with zeros.
111
+
112
+ Uses scatter — compatible with torch.export and torch.compile.
113
+ """
114
+ cu = inp.cu_seqlens
115
+ b = cu.shape[0] - 1
116
+ n = inp.max_seqlen
117
+ # Build a (nt,) tensor of batch indices and a (nt,) tensor of within-sequence positions.
118
+ lengths = cu[1:] - cu[:-1] # (b,)
119
+ batch_idx = torch.repeat_interleave(
120
+ torch.arange(b, device=cu.device, dtype=torch.long), lengths
121
+ ) # (nt,)
122
+ pos_idx = position_ids_from_packed(PackedSequence(cu_seqlens=cu, max_seqlen=n)) # (nt,)
123
+ d, c = inp.x.shape[-1], inp.abs_positions.shape[-1]
124
+ x_out = inp.x.new_zeros(b, n, d)
125
+ x_out[batch_idx, pos_idx] = inp.x
126
+ pos_out = inp.abs_positions.new_zeros(b, n, c)
127
+ pos_out[batch_idx, pos_idx] = inp.abs_positions
128
+ mask = torch.zeros(b, n, dtype=torch.bool, device=inp.x.device)
129
+ mask[batch_idx, pos_idx] = True
130
+ return PaddedInput(x=x_out, mask=mask, abs_positions=pos_out)
131
+
132
+
133
+ def lengths_to_cu_seqlens(lengths: Int[Tensor, "b"]) -> Int[Tensor, "bp1"]:
134
+ zero = torch.zeros(1, dtype=lengths.dtype, device=lengths.device)
135
+ return torch.cat([zero, lengths.cumsum(0)])
136
+
137
+
138
+ def position_ids_from_packed(seq: PackedSequence) -> Int[Tensor, "nt"]:
139
+ """Build per-token position indices [0..len_i-1] for each document in the pack.
140
+
141
+ The result at flat index i is ``i - cu_seqlens[batch_of_i]``, i.e. the within-document
142
+ position of that token.
143
+
144
+ Compatible with ``torch.export`` and ``torch.compile``: no Python-side iteration over
145
+ data-dependent lengths. The key identity is::
146
+
147
+ pos_idx = arange(nt) - cu[batch_idx]
148
+
149
+ where ``batch_idx`` is built via ``repeat_interleave``, which is a supported dynamic op.
150
+ """
151
+ cu = seq.cu_seqlens
152
+ b = cu.shape[0] - 1
153
+ lengths = cu[1:] - cu[:-1] # (b,) — kept as a tensor, never converted to Python list
154
+ batch_idx = torch.repeat_interleave(
155
+ torch.arange(b, device=cu.device, dtype=torch.long), lengths
156
+ ) # (nt,)
157
+ nt = batch_idx.shape[0]
158
+ return torch.arange(nt, device=cu.device, dtype=torch.long) - cu[batch_idx]
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackformers
3
+ Version: 3.8.0
4
+ Summary: Typed, composable, SOLID transformer library for PyTorch
5
+ Project-URL: Homepage, https://github.com/Red-Eyed/stackformers
6
+ Project-URL: Repository, https://github.com/Red-Eyed/stackformers
7
+ Project-URL: Bug Tracker, https://github.com/Red-Eyed/stackformers/issues
8
+ Author-email: Vadym Stupakov <vadim.stupakov@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Vadym Stupakov
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: attention,deep-learning,pytorch,transformer
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
39
+ Classifier: Typing :: Typed
40
+ Requires-Python: >=3.11
41
+ Requires-Dist: beartype>=0.18
42
+ Requires-Dist: einops>=0.7
43
+ Requires-Dist: jaxtyping>=0.2
44
+ Requires-Dist: numpy>=2.4.4
45
+ Requires-Dist: pydantic>=2.0
46
+ Requires-Dist: torch>=2.2
47
+ Description-Content-Type: text/markdown
48
+
49
+ # stackformers
50
+
51
+ Typed, composable transformer library for PyTorch.
52
+ Every architectural choice — positional encoding, normalization, feedforward variant — is an injected dependency, not a constructor flag.
53
+
54
+ ```bash
55
+ uv add stackformers
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Why
61
+
62
+ Most transformer libraries grow into a tangle of `if self.use_rope`, `if self.window_size is not None`, and god-config objects with thirty nullable fields. Adding a new variant means touching existing code.
63
+
64
+ stackformers takes a different approach:
65
+
66
+ - **Swap any component without touching anything else** — `SelfAttention(config, pos_encoding=RoPE)` vs `SelfAttention(config, pos_encoding=ALiBi)` — same call site, different object
67
+ - **No `None` checks in `forward()`** — `NoPosEncoding` is a real object that passes q/k unchanged; the branch never exists
68
+ - **Sealed sequence unions** — `PaddedInput | PackedInput` instead of optional `cu_seqlens` and `mask` arguments that conflict with each other
69
+ - **`torch.compile` / `torch.export` safe** — no Python control flow on tensors inside any `forward()`
70
+ - **Structural protocols** — bring your own implementation; no ABC inheritance required
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ### Zero boilerplate
77
+
78
+ ```python
79
+ import torch
80
+ from stackformers import TransformerEncoder, plain_encoder_config, make_padded_input
81
+
82
+ model = TransformerEncoder(plain_encoder_config(dim=512, heads=8, num_layers=6))
83
+
84
+ x = torch.randn(2, 128, 512)
85
+ mask = torch.ones(2, 128, dtype=torch.bool)
86
+ out = model(make_padded_input(x, mask)) # (2, 128, 512)
87
+ ```
88
+
89
+ Switch to packed (variable-length, no padding waste) — same weights:
90
+
91
+ ```python
92
+ from stackformers import make_packed_input
93
+
94
+ cu = torch.tensor([0, 64, 128], dtype=torch.int32)
95
+ out = model(make_packed_input(x_flat, cu, max_seqlen=64)) # (128, 512)
96
+ ```
97
+
98
+ Causal LM backbone:
99
+
100
+ ```python
101
+ plain_encoder_config(dim=768, heads=12, num_layers=12, causal=True)
102
+ ```
103
+
104
+ Sliding-window local attention (O(n · w)):
105
+
106
+ ```python
107
+ from stackformers import windowed_encoder_config
108
+ windowed_encoder_config(dim=512, heads=8, num_layers=6, window_size=128)
109
+ ```
110
+
111
+ Encoder–decoder:
112
+
113
+ ```python
114
+ from stackformers import TransformerDecoder, plain_decoder_config
115
+
116
+ model = TransformerDecoder(plain_decoder_config(dim=512, heads=8, num_layers=6))
117
+ out = model(make_padded_input(x, mask), make_padded_input(context, ctx_mask))
118
+ ```
119
+
120
+ ### Explicit config
121
+
122
+ Full control with JSON round-trip via `kind` discriminators:
123
+
124
+ ```python
125
+ from stackformers import (
126
+ TransformerEncoderConfig, TransformerEncoder,
127
+ SelfAttentionConfig, SwiGLUConfig, RMSNormConfig, RoPE1DConfig,
128
+ make_padded_input,
129
+ )
130
+
131
+ cfg = TransformerEncoderConfig(
132
+ attn=SelfAttentionConfig(dim=512, heads=8, dim_head=64, causal=False),
133
+ ff=SwiGLUConfig(dim=512, mult=4.0),
134
+ norm=RMSNormConfig(dim=512),
135
+ pos_encoding=RoPE1DConfig(dim_head=64),
136
+ num_layers=6,
137
+ )
138
+ model = TransformerEncoder(cfg)
139
+
140
+ # Serialise / restore
141
+ cfg2 = TransformerEncoderConfig.model_validate(cfg.model_dump())
142
+ ```
143
+
144
+ ### Custom wiring
145
+
146
+ Wire layers yourself when presets aren't enough:
147
+
148
+ ```python
149
+ from stackformers import (
150
+ SelfAttention, SwiGLU, TransformerLayer, Encoder, RMSNorm,
151
+ RotaryEmbedding1D,
152
+ SelfAttentionConfig, SwiGLUConfig, RMSNormConfig, RoPE1DConfig,
153
+ )
154
+
155
+ pos = RotaryEmbedding1D(RoPE1DConfig(dim_head=64))
156
+ attn = SelfAttention(SelfAttentionConfig(dim=512, heads=8, dim_head=64), pos_encoding=pos)
157
+
158
+ layers = [
159
+ TransformerLayer(
160
+ self_attn=attn,
161
+ ff=SwiGLU(SwiGLUConfig(dim=512)),
162
+ norm_attn=RMSNorm(RMSNormConfig(dim=512)),
163
+ norm_ff=RMSNorm(RMSNormConfig(dim=512)),
164
+ )
165
+ for _ in range(6)
166
+ ]
167
+ encoder = Encoder(layers=layers, final_norm=RMSNorm(RMSNormConfig(dim=512)))
168
+ ```
169
+
170
+ ---
171
+
172
+ ## What's included
173
+
174
+ | Area | Variants |
175
+ |------|----------|
176
+ | Self-attention | Global, sliding-window (local); padded and packed backends; GQA / MQA |
177
+ | Cross-attention | Global; padded and packed backends |
178
+ | Positional encoding | RoPE-1D, RoPE-2D, none (null object) |
179
+ | Feedforward | SwiGLU, GEGLU |
180
+ | Normalization | RMSNorm, LayerNorm |
181
+ | Presets | Encoder, Decoder, CrossAttender |
182
+
183
+ On CUDA with fp16/bf16 the packed path uses `torch.nn.attention.varlen.varlen_attn`. CPU and fp32 fall back to a scatter-to-padded SDPA — correct everywhere, fast where it matters.
184
+
185
+ ---
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ git clone <repo> && cd stackformers
191
+ uv sync --group dev
192
+
193
+ just fmt # format
194
+ just lint # lint
195
+ just types # type-check
196
+ just test # test
197
+ just check # full CI gate
198
+ ```
199
+
200
+ ---
201
+
202
+ ## License
203
+
204
+ See [LICENSE](LICENSE).
@@ -0,0 +1,46 @@
1
+ stackformers/__init__.py,sha256=kXFjn1AbcqhP3Pf2TynXdHkaeB3qivOSsGDU5rrtgWA,3787
2
+ stackformers/config.py,sha256=tPRmrwvO7t1rGw2KA2pna-Tt-5He4QaBYh1uF464t0I,704
3
+ stackformers/cross_attender.py,sha256=VFrDDLWv_36INFUiScYl8zc4ujeS7w5tP0v7ksu7kNo,1506
4
+ stackformers/decoder.py,sha256=PsjssyIEM6tqUWasQ1AbqLj-pfTHjVG2nnQy8skYf4E,1824
5
+ stackformers/encoder.py,sha256=GBe3HxZUHCdpfa8WuQZMXM-yJR1FdIF48HZbdg5gpGI,701
6
+ stackformers/layers.py,sha256=uM_85QhaGvlwXpkmC1IDZRtnPIr-DyVKcLz38626EdE,941
7
+ stackformers/sequence.py,sha256=8THo2FrN6InTEo1f9fSGlxMsP2wLcF3oVn3PQHAIa5w,5788
8
+ stackformers/attention/README.md,sha256=7UGXLTcJ9V3Imj9wgpzbaDHUKnfOXS4qU3zZ5oD_QQo,2544
9
+ stackformers/attention/__init__.py,sha256=2t91tmmlRy8Ww3gx0B-QX4_9LsQh72yl6smXyrxIJX4,510
10
+ stackformers/attention/bias.py,sha256=zIb8iEdqdO1gvnVesWJwTP60qAv1I3elldv97chkHpM,298
11
+ stackformers/attention/config.py,sha256=9QRBQ3akBXAHIHNDa-8_uHfAEH-etcaKPcc0RTRo1fM,5039
12
+ stackformers/attention/cross_attn.py,sha256=6Ak_DHlsCZu493YqwiTd8sYjMM52h-qNw39u9Rydujg,3965
13
+ stackformers/attention/ops.py,sha256=_V82bYaDlKurlOX5hL4nfObqRXuU2BiLs5zqB3Ij8es,6097
14
+ stackformers/attention/protocols.py,sha256=xl3ZkXUM93poT_2QxPGlRdS_7z6tf_1yNT1gjnSY9-I,1167
15
+ stackformers/attention/self_attn.py,sha256=6km3lFhkAiFctqDkI_voDXqGHEV2wyKssworfq44p3k,3933
16
+ stackformers/feedforward/README.md,sha256=rgOSpWmBRmoApaai1ZpRjiYtY1auCbqciP-lTkPwJkQ,503
17
+ stackformers/feedforward/__init__.py,sha256=AviF8Ib76eU5J3zuuiz4A4u1bl8qoB-UZ0LCS_Xgegk,212
18
+ stackformers/feedforward/config.py,sha256=XEAev9SDKmZzHFjFPfE3WvRpWYcXMK_9Sy7fmWd0OU0,2103
19
+ stackformers/feedforward/factory.py,sha256=FiLaR1SZ-CoU4uGZQaJizii718kl3kw0GsMmVrDuL5E,761
20
+ stackformers/feedforward/geglu.py,sha256=lEAZHyh4xfIxWZzTsxffr49lyQphXDpkqCQIs77Y7Lg,1200
21
+ stackformers/feedforward/protocols.py,sha256=ZxRJDYjGAyu7ruxWcBEblds7CMZQJK6kNm_qxNN4Zbg,380
22
+ stackformers/feedforward/relu_squared.py,sha256=8rhVGUaoXMah_prkz9SkTVNsHJ6AxpNjda2w9OPugbo,953
23
+ stackformers/feedforward/swiglu.py,sha256=87b0kH2sRWB4U2wpkGGt5fmOLFrHoM_0jkhYKhQRZvY,1114
24
+ stackformers/norm/README.md,sha256=NMENfZoBWcaVxcth4SBVn2RZD7cyL90PfcTxDrz_f0I,495
25
+ stackformers/norm/__init__.py,sha256=2ZbOQJbXg8SNxVo5AsmI1rsEMWw33cu6sQI-9DM67YQ,116
26
+ stackformers/norm/config.py,sha256=HIbn8NLcdAn_WIrSMaCwe0kMUi0Fh8Ykapz6E-P_RsQ,469
27
+ stackformers/norm/factory.py,sha256=AoaKgpn1aHGYNb4dqqGQiCd5d716agVroSUoJ4XLMQc,559
28
+ stackformers/norm/protocols.py,sha256=WWhqBqBFG5TSZe4U42tzDhT0X8nWJaRYDU9OnQ8uem4,355
29
+ stackformers/positional/README.md,sha256=R-QYDL7PiSvd7NwonEW7Mwcv7WtEHYzTNE5fIKHqJ8s,1114
30
+ stackformers/positional/__init__.py,sha256=1bqfSD6ekk35lQtm6tiEiwnvxeu3I6Gno1Um1jTr8oM,633
31
+ stackformers/positional/config.py,sha256=gT5w0BJnX5lvZRIav3YwkSvkbfDt6it2oq782OGuLpA,2141
32
+ stackformers/positional/factory.py,sha256=ILT8yZUEvvEuM6tkHyXSwc6OAQ37HkHIsrpUOeZJ7Rs,992
33
+ stackformers/positional/learned.py,sha256=nmXGv7HnNgadcq7LZ5OrmPlKT1qcPvAVKuQR53Ow55U,1855
34
+ stackformers/positional/none.py,sha256=lq4WREpY572jux3_V-_-9xG0ZvCgJUddMQTMfBZBwVs,993
35
+ stackformers/positional/protocols.py,sha256=uLFHYJrHfYedBxzxe3-2Apuq4VaY0zMgbN1gT8Uc0Uw,1014
36
+ stackformers/positional/rope1d.py,sha256=Y2Dn5DjO14lcQ7PmjQ4zZLxA4tefuB3yBMZz9x5QgbY,3851
37
+ stackformers/positional/rope2d.py,sha256=4VXQ0tDUnlFR386LlyWfOx9U85XwvH7_qVPJ-8mIVas,2319
38
+ stackformers/presets/README.md,sha256=uWaW8dGlGth2YNaE8TTWbaQTbLnojnp4g59Ez9lONao,2628
39
+ stackformers/presets/__init__.py,sha256=vlQagWfVRHFnH6GgqA8EUe_rEx5jw9eoif2SSyPrIxM,1024
40
+ stackformers/presets/cross_attender.py,sha256=yZG4oEnZqTUJwXuPZKF-fRdtdWpJ7dAO6WaECIFMQpY,3838
41
+ stackformers/presets/decoder.py,sha256=SChy1PlIUQ82zGeXcvJyPae60nbvfCC1_pf7OBb9ngI,4699
42
+ stackformers/presets/encoder.py,sha256=Hj6nDkSUPxeXMdBsBh3hl41JlNkHtZzWW7W05JuHLkA,4990
43
+ stackformers-3.8.0.dist-info/METADATA,sha256=WmSQC9jHW1XJFTgOD1ovm0nnlby_7GV0n-R1-uoGx3I,6846
44
+ stackformers-3.8.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
45
+ stackformers-3.8.0.dist-info/licenses/LICENSE,sha256=6rViNgu6RIlBLiKHptpp9oPrAZBbLJlw_1IPUNGvMOg,1071
46
+ stackformers-3.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any