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.
- stackformers/__init__.py +136 -0
- stackformers/attention/README.md +21 -0
- stackformers/attention/__init__.py +16 -0
- stackformers/attention/bias.py +12 -0
- stackformers/attention/config.py +145 -0
- stackformers/attention/cross_attn.py +77 -0
- stackformers/attention/ops.py +167 -0
- stackformers/attention/protocols.py +44 -0
- stackformers/attention/self_attn.py +88 -0
- stackformers/config.py +26 -0
- stackformers/cross_attender.py +46 -0
- stackformers/decoder.py +57 -0
- stackformers/encoder.py +26 -0
- stackformers/feedforward/README.md +11 -0
- stackformers/feedforward/__init__.py +5 -0
- stackformers/feedforward/config.py +68 -0
- stackformers/feedforward/factory.py +24 -0
- stackformers/feedforward/geglu.py +35 -0
- stackformers/feedforward/protocols.py +16 -0
- stackformers/feedforward/relu_squared.py +28 -0
- stackformers/feedforward/swiglu.py +34 -0
- stackformers/layers.py +31 -0
- stackformers/norm/README.md +11 -0
- stackformers/norm/__init__.py +3 -0
- stackformers/norm/config.py +20 -0
- stackformers/norm/factory.py +18 -0
- stackformers/norm/protocols.py +16 -0
- stackformers/positional/README.md +13 -0
- stackformers/positional/__init__.py +23 -0
- stackformers/positional/config.py +65 -0
- stackformers/positional/factory.py +28 -0
- stackformers/positional/learned.py +49 -0
- stackformers/positional/none.py +32 -0
- stackformers/positional/protocols.py +32 -0
- stackformers/positional/rope1d.py +111 -0
- stackformers/positional/rope2d.py +59 -0
- stackformers/presets/README.md +63 -0
- stackformers/presets/__init__.py +39 -0
- stackformers/presets/cross_attender.py +108 -0
- stackformers/presets/decoder.py +126 -0
- stackformers/presets/encoder.py +150 -0
- stackformers/sequence.py +158 -0
- stackformers-3.8.0.dist-info/METADATA +204 -0
- stackformers-3.8.0.dist-info/RECORD +46 -0
- stackformers-3.8.0.dist-info/WHEEL +4 -0
- 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)
|
stackformers/sequence.py
ADDED
|
@@ -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,,
|