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
stackformers/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""stackformers public API."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
__version__ = version("stackformers")
|
|
6
|
+
|
|
7
|
+
from stackformers.attention.config import CrossAttentionConfig, SelfAttentionConfig
|
|
8
|
+
from stackformers.attention.cross_attn import CrossAttention
|
|
9
|
+
from stackformers.attention.protocols import CrossAttn, SelfAttn
|
|
10
|
+
from stackformers.attention.self_attn import SelfAttention
|
|
11
|
+
from stackformers.config import DecoderConfig, EncoderConfig, LayerConfig
|
|
12
|
+
from stackformers.cross_attender import CrossAttenderLayer, CrossAttenderStack
|
|
13
|
+
from stackformers.decoder import Decoder, DecoderLayer
|
|
14
|
+
from stackformers.encoder import Encoder
|
|
15
|
+
from stackformers.feedforward.config import FeedForwardConfig
|
|
16
|
+
from stackformers.feedforward.factory import build_ff
|
|
17
|
+
from stackformers.feedforward.protocols import FeedForward
|
|
18
|
+
from stackformers.feedforward.swiglu import SwiGLU
|
|
19
|
+
from stackformers.layers import TransformerLayer
|
|
20
|
+
from stackformers.norm.config import LayerNormConfig, RMSNormConfig
|
|
21
|
+
from stackformers.norm.factory import NormConfig, build_norm
|
|
22
|
+
from stackformers.norm.protocols import Norm
|
|
23
|
+
from stackformers.positional.config import (
|
|
24
|
+
NoPosEncodingConfig,
|
|
25
|
+
PosEncodingConfig,
|
|
26
|
+
RoPE1DConfig,
|
|
27
|
+
RoPE2DConfig,
|
|
28
|
+
)
|
|
29
|
+
from stackformers.positional.factory import build_pos_encoding
|
|
30
|
+
from stackformers.positional.none import NoPosEncoding
|
|
31
|
+
from stackformers.positional.protocols import PosEncoding
|
|
32
|
+
from stackformers.positional.rope1d import RotaryEmbedding1D
|
|
33
|
+
from stackformers.positional.rope2d import RotaryEmbedding2D
|
|
34
|
+
from stackformers.presets.cross_attender import (
|
|
35
|
+
CrossAttender,
|
|
36
|
+
CrossAttenderConfig,
|
|
37
|
+
plain_cross_attender_config,
|
|
38
|
+
)
|
|
39
|
+
from stackformers.presets.decoder import (
|
|
40
|
+
TransformerDecoder,
|
|
41
|
+
TransformerDecoderConfig,
|
|
42
|
+
plain_decoder_config,
|
|
43
|
+
)
|
|
44
|
+
from stackformers.presets.encoder import (
|
|
45
|
+
TransformerEncoder,
|
|
46
|
+
TransformerEncoderConfig,
|
|
47
|
+
plain_encoder_config,
|
|
48
|
+
windowed_encoder_config,
|
|
49
|
+
)
|
|
50
|
+
from stackformers.sequence import (
|
|
51
|
+
PackedInput,
|
|
52
|
+
PackedSequence,
|
|
53
|
+
PaddedInput,
|
|
54
|
+
PaddedSequence,
|
|
55
|
+
SequenceInfo,
|
|
56
|
+
SequenceInput,
|
|
57
|
+
lengths_to_cu_seqlens,
|
|
58
|
+
make_packed,
|
|
59
|
+
make_packed_input,
|
|
60
|
+
make_padded,
|
|
61
|
+
make_padded_input,
|
|
62
|
+
position_ids_from_packed,
|
|
63
|
+
to_seq_info,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"__version__",
|
|
68
|
+
# sequences
|
|
69
|
+
"PaddedSequence",
|
|
70
|
+
"PackedSequence",
|
|
71
|
+
"SequenceInfo",
|
|
72
|
+
"PaddedInput",
|
|
73
|
+
"PackedInput",
|
|
74
|
+
"SequenceInput",
|
|
75
|
+
"make_padded",
|
|
76
|
+
"make_packed",
|
|
77
|
+
"make_padded_input",
|
|
78
|
+
"make_packed_input",
|
|
79
|
+
"to_seq_info",
|
|
80
|
+
"lengths_to_cu_seqlens",
|
|
81
|
+
"position_ids_from_packed",
|
|
82
|
+
# protocols
|
|
83
|
+
"PosEncoding",
|
|
84
|
+
"SelfAttn",
|
|
85
|
+
"CrossAttn",
|
|
86
|
+
"FeedForward",
|
|
87
|
+
"Norm",
|
|
88
|
+
# configs — attention
|
|
89
|
+
"SelfAttentionConfig",
|
|
90
|
+
"CrossAttentionConfig",
|
|
91
|
+
# configs — ff / layer / encoder / decoder
|
|
92
|
+
"FeedForwardConfig",
|
|
93
|
+
"LayerConfig",
|
|
94
|
+
"EncoderConfig",
|
|
95
|
+
"DecoderConfig",
|
|
96
|
+
# configs — norm
|
|
97
|
+
"RMSNormConfig",
|
|
98
|
+
"LayerNormConfig",
|
|
99
|
+
"NormConfig",
|
|
100
|
+
# configs — positional
|
|
101
|
+
"RoPE1DConfig",
|
|
102
|
+
"RoPE2DConfig",
|
|
103
|
+
"NoPosEncodingConfig",
|
|
104
|
+
"PosEncodingConfig",
|
|
105
|
+
# positional
|
|
106
|
+
"NoPosEncoding",
|
|
107
|
+
"RotaryEmbedding1D",
|
|
108
|
+
"RotaryEmbedding2D",
|
|
109
|
+
# attention
|
|
110
|
+
"SelfAttention",
|
|
111
|
+
"CrossAttention",
|
|
112
|
+
# feedforward
|
|
113
|
+
"SwiGLU",
|
|
114
|
+
# builders
|
|
115
|
+
"build_norm",
|
|
116
|
+
"build_ff",
|
|
117
|
+
"build_pos_encoding",
|
|
118
|
+
# transformer blocks
|
|
119
|
+
"TransformerLayer",
|
|
120
|
+
"Encoder",
|
|
121
|
+
"DecoderLayer",
|
|
122
|
+
"Decoder",
|
|
123
|
+
"CrossAttenderLayer",
|
|
124
|
+
"CrossAttenderStack",
|
|
125
|
+
# presets
|
|
126
|
+
"TransformerEncoderConfig",
|
|
127
|
+
"TransformerEncoder",
|
|
128
|
+
"plain_encoder_config",
|
|
129
|
+
"windowed_encoder_config",
|
|
130
|
+
"TransformerDecoderConfig",
|
|
131
|
+
"TransformerDecoder",
|
|
132
|
+
"plain_decoder_config",
|
|
133
|
+
"CrossAttenderConfig",
|
|
134
|
+
"CrossAttender",
|
|
135
|
+
"plain_cross_attender_config",
|
|
136
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# attention
|
|
2
|
+
|
|
3
|
+
Multi-head self-attention and cross-attention.
|
|
4
|
+
|
|
5
|
+
## Design
|
|
6
|
+
|
|
7
|
+
**Two configs, two modules.** `SelfAttentionConfig` and `CrossAttentionConfig` are separate types because their parameter sets differ: self-attention has `causal` and `window_size`; cross-attention has neither.
|
|
8
|
+
|
|
9
|
+
**Static vs runtime dispatch.** The attention *type* (global vs sliding-window) is determined at construction time via `window_size: int | None` in `SelfAttentionConfig`. The *backend* (padded vs packed) is determined at runtime: `SelfAttention.forward` and `CrossAttention.forward` dispatch on `SequenceInput` via `match`. This means one model handles both training (packed) and inference (padded) without swapping classes.
|
|
10
|
+
|
|
11
|
+
**Kernel ops in `ops.py`.** All low-level attention math lives in `attention/ops.py`: mask builders, `packed_attn` (thin `varlen_attn` wrapper), `padded_sdpa`, and `packed_attn_or_fallback`. The modules stay thin dispatchers.
|
|
12
|
+
|
|
13
|
+
**Packed attention with automatic fallback.** `PackedInput` normally routes to `varlen_attn`, which requires CUDA and `float16`/`bfloat16`. When those conditions are not met (CPU, float32, `torch.export`), `packed_attn_or_fallback` transparently scatters q/k/v to padded layout, runs `F.scaled_dot_product_attention`, and gathers the result back to packed — same weights, same output shape `(nt, h, d)`. A `UserWarning` is emitted so callers know they are on the slow path. Dropout works on the fallback path; it is silently skipped only when `varlen_attn` is used (which does not accept a dropout argument).
|
|
14
|
+
|
|
15
|
+
**`torch.export` and `torch.compile` compatibility.** All scatter/gather helpers in `ops.py` (`_cu_to_indices`, `_packed_heads_to_padded`, `_padded_heads_to_packed`) are fully export-compatible. The key design constraint is that per-document length iteration must never use Python-side loops over data-dependent values (i.e., no `.tolist()` followed by a list comprehension). Instead, `_cu_to_indices` uses `torch.repeat_interleave` to produce `batch_idx` and derives `pos_idx` via the identity `arange(nt) - cu[batch_idx]`. This keeps the computation graph purely symbolic even when `nt` (total tokens) is a data-dependent unbacked symbol.
|
|
16
|
+
|
|
17
|
+
**GQA / MQA.** Set `kv_heads < heads` in either config. Key and value heads are repeated via `einops.repeat` before the dot-product — no attention-specific changes needed.
|
|
18
|
+
|
|
19
|
+
## Extending
|
|
20
|
+
|
|
21
|
+
To add a new attention variant, create a new `nn.Module` that satisfies the `SelfAttn` or `CrossAttn` protocol. No kernel registration or factory changes required.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from stackformers.attention.bias import NoAttnBias
|
|
2
|
+
from stackformers.attention.config import CrossAttentionConfig, SelfAttentionConfig
|
|
3
|
+
from stackformers.attention.cross_attn import CrossAttention
|
|
4
|
+
from stackformers.attention.protocols import AttnBias, CrossAttn, SelfAttn
|
|
5
|
+
from stackformers.attention.self_attn import SelfAttention
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SelfAttentionConfig",
|
|
9
|
+
"CrossAttentionConfig",
|
|
10
|
+
"SelfAttn",
|
|
11
|
+
"CrossAttn",
|
|
12
|
+
"AttnBias",
|
|
13
|
+
"SelfAttention",
|
|
14
|
+
"CrossAttention",
|
|
15
|
+
"NoAttnBias",
|
|
16
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
|
|
5
|
+
from stackformers.sequence import PaddedInput
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoAttnBias(nn.Module):
|
|
9
|
+
"""Null object for AttnBias — signals no bias so varlen_attn remains available."""
|
|
10
|
+
|
|
11
|
+
def __call__(self, input: PaddedInput) -> None:
|
|
12
|
+
return None
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
_ALIGN = 64 # tensor-core alignment for FP16/BF16
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _validate_attn_dims(dim: int, heads: int, dim_head: int, kv_heads: int | None) -> None:
|
|
11
|
+
if dim % heads != 0:
|
|
12
|
+
raise ValueError(f"dim ({dim}) must be divisible by heads ({heads})")
|
|
13
|
+
kv_h = kv_heads if kv_heads is not None else heads
|
|
14
|
+
if heads % kv_h != 0:
|
|
15
|
+
raise ValueError(f"heads ({heads}) must be divisible by kv_heads ({kv_h})")
|
|
16
|
+
if dim_head % _ALIGN != 0:
|
|
17
|
+
warnings.warn(
|
|
18
|
+
f"dim_head={dim_head} is not a multiple of {_ALIGN}. "
|
|
19
|
+
"Unaligned head dimension reduces GPU throughput on tensor-core hardware. "
|
|
20
|
+
f"Nearest aligned values: {(dim_head // _ALIGN) * _ALIGN} or "
|
|
21
|
+
f"{(dim_head // _ALIGN + 1) * _ALIGN}.",
|
|
22
|
+
UserWarning,
|
|
23
|
+
stacklevel=3,
|
|
24
|
+
)
|
|
25
|
+
projected = heads * dim_head
|
|
26
|
+
if projected != dim:
|
|
27
|
+
warnings.warn(
|
|
28
|
+
f"heads * dim_head = {heads} * {dim_head} = {projected}, "
|
|
29
|
+
f"which does not equal dim={dim}. "
|
|
30
|
+
"The Q/K/V projections are non-square — this is valid but may not be intentional. "
|
|
31
|
+
f"Standard transformers use dim_head = dim // heads = {dim // heads}.",
|
|
32
|
+
UserWarning,
|
|
33
|
+
stacklevel=3,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SelfAttentionConfig(BaseModel):
|
|
38
|
+
dim: int = Field(
|
|
39
|
+
gt=0,
|
|
40
|
+
description="Model (embedding) dimension — input and output width of the attention sublayer.", # noqa: E501
|
|
41
|
+
)
|
|
42
|
+
heads: int = Field(default=8, gt=0, description="Number of query heads.")
|
|
43
|
+
dim_head: int = Field(
|
|
44
|
+
default=64,
|
|
45
|
+
gt=0,
|
|
46
|
+
description=(
|
|
47
|
+
"Dimension per head. Output projection width is heads * dim_head,"
|
|
48
|
+
" which need not equal dim (non-square projections are valid)."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
kv_heads: int | None = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description=(
|
|
54
|
+
"Number of key/value heads. None = equal to heads (standard MHA)."
|
|
55
|
+
" Set to 1 for MQA or any divisor of heads for GQA."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
causal: bool = Field(
|
|
59
|
+
default=False,
|
|
60
|
+
description=(
|
|
61
|
+
"Apply a causal mask so each position attends only to itself and earlier positions."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
dropout: float = Field(
|
|
65
|
+
default=0.0,
|
|
66
|
+
ge=0.0,
|
|
67
|
+
le=1.0,
|
|
68
|
+
description="Dropout probability applied to the output projection, during training only.",
|
|
69
|
+
)
|
|
70
|
+
window_size: int | None = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
description=(
|
|
73
|
+
"Sliding-window width. None = global attention."
|
|
74
|
+
" A positive integer restricts each token to a local window of that width."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
qk_norm: bool = Field(
|
|
78
|
+
default=False,
|
|
79
|
+
description=(
|
|
80
|
+
"Apply RMSNorm to queries and keys (in head space) before the dot product."
|
|
81
|
+
" Stabilises logit scale at large depth or large dim_head."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@model_validator(mode="after")
|
|
86
|
+
def _validate(self) -> "SelfAttentionConfig":
|
|
87
|
+
_validate_attn_dims(self.dim, self.heads, self.dim_head, self.kv_heads)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def effective_kv_heads(self) -> int:
|
|
92
|
+
return self.kv_heads if self.kv_heads is not None else self.heads
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def groups(self) -> int:
|
|
96
|
+
return self.heads // self.effective_kv_heads
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CrossAttentionConfig(BaseModel):
|
|
100
|
+
dim: int = Field(
|
|
101
|
+
gt=0,
|
|
102
|
+
description="Model (embedding) dimension — input and output width of the attention sublayer.", # noqa: E501
|
|
103
|
+
)
|
|
104
|
+
heads: int = Field(default=8, gt=0, description="Number of query heads.")
|
|
105
|
+
dim_head: int = Field(
|
|
106
|
+
default=64,
|
|
107
|
+
gt=0,
|
|
108
|
+
description=(
|
|
109
|
+
"Dimension per head. Output projection width is heads * dim_head,"
|
|
110
|
+
" which need not equal dim (non-square projections are valid)."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
kv_heads: int | None = Field(
|
|
114
|
+
default=None,
|
|
115
|
+
description=(
|
|
116
|
+
"Number of key/value heads. None = equal to heads (standard MHA)."
|
|
117
|
+
" Set to 1 for MQA or any divisor of heads for GQA."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
dropout: float = Field(
|
|
121
|
+
default=0.0,
|
|
122
|
+
ge=0.0,
|
|
123
|
+
le=1.0,
|
|
124
|
+
description="Dropout probability applied to the output projection, during training only.",
|
|
125
|
+
)
|
|
126
|
+
qk_norm: bool = Field(
|
|
127
|
+
default=False,
|
|
128
|
+
description=(
|
|
129
|
+
"Apply RMSNorm to queries and keys (in head space) before the dot product."
|
|
130
|
+
" Stabilises logit scale at large depth or large dim_head."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@model_validator(mode="after")
|
|
135
|
+
def _validate(self) -> "CrossAttentionConfig":
|
|
136
|
+
_validate_attn_dims(self.dim, self.heads, self.dim_head, self.kv_heads)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def effective_kv_heads(self) -> int:
|
|
141
|
+
return self.kv_heads if self.kv_heads is not None else self.heads
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def groups(self) -> int:
|
|
145
|
+
return self.heads // self.effective_kv_heads
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
from einops import rearrange, repeat
|
|
5
|
+
from torch import Tensor
|
|
6
|
+
|
|
7
|
+
from stackformers.attention.config import CrossAttentionConfig
|
|
8
|
+
from stackformers.attention.ops import packed_attn_or_fallback, padded_sdpa
|
|
9
|
+
from stackformers.positional.protocols import PosEncoding
|
|
10
|
+
from stackformers.sequence import PackedInput, PackedSequence, PaddedInput, SequenceInput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrossAttention(nn.Module):
|
|
14
|
+
"""Multi-head cross-attention: queries from x, keys/values from context.
|
|
15
|
+
|
|
16
|
+
Always global (no windowing). Pass PaddedInput for inference or export, PackedInput for
|
|
17
|
+
training. Falls back to padded SDPA when varlen_attn is unavailable (CPU, float32,
|
|
18
|
+
torch.export).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: CrossAttentionConfig, pos_encoding: PosEncoding) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.config = config
|
|
24
|
+
h, kv_h, dh = config.heads, config.effective_kv_heads, config.dim_head
|
|
25
|
+
self.to_q = nn.Linear(config.dim, h * dh, bias=False)
|
|
26
|
+
self.to_k = nn.Linear(config.dim, kv_h * dh, bias=False)
|
|
27
|
+
self.to_v = nn.Linear(config.dim, kv_h * dh, bias=False)
|
|
28
|
+
self.to_out = nn.Linear(h * dh, config.dim, bias=False)
|
|
29
|
+
self.dropout = nn.Dropout(config.dropout)
|
|
30
|
+
self.pos_encoding = pos_encoding
|
|
31
|
+
self.q_norm: nn.Module = nn.RMSNorm(dh) if config.qk_norm else nn.Identity()
|
|
32
|
+
self.k_norm: nn.Module = nn.RMSNorm(dh) if config.qk_norm else nn.Identity()
|
|
33
|
+
nn.init.normal_(self.to_out.weight, std=0.02)
|
|
34
|
+
|
|
35
|
+
def _forward_padded(self, x_input: PaddedInput, ctx_input: PaddedInput) -> Tensor:
|
|
36
|
+
cfg = self.config
|
|
37
|
+
h, kv_h, groups = cfg.heads, cfg.effective_kv_heads, cfg.groups
|
|
38
|
+
x, context = x_input.x, ctx_input.x
|
|
39
|
+
q = self.q_norm(rearrange(self.to_q(x), "b n (h d) -> b h n d", h=h))
|
|
40
|
+
k = self.k_norm(rearrange(self.to_k(context), "b s (h d) -> b h s d", h=kv_h))
|
|
41
|
+
v = rearrange(self.to_v(context), "b s (h d) -> b h s d", h=kv_h)
|
|
42
|
+
if groups > 1:
|
|
43
|
+
k = repeat(k, "b h s d -> b (h g) s d", g=groups)
|
|
44
|
+
v = repeat(v, "b h s d -> b (h g) s d", g=groups)
|
|
45
|
+
q, k = self.pos_encoding.forward_padded(
|
|
46
|
+
q, k, x_input.abs_positions, ctx_input.abs_positions
|
|
47
|
+
)
|
|
48
|
+
out = padded_sdpa(q, k, v, ctx_input.mask, causal=False, window_size=None, bias=None)
|
|
49
|
+
out = self.dropout(self.to_out(rearrange(out, "b h n d -> b n (h d)")))
|
|
50
|
+
return out * x_input.mask.unsqueeze(-1)
|
|
51
|
+
|
|
52
|
+
def _forward_packed(self, x_input: PackedInput, ctx_input: PackedInput) -> Tensor:
|
|
53
|
+
cfg = self.config
|
|
54
|
+
h, kv_h, groups = cfg.heads, cfg.effective_kv_heads, cfg.groups
|
|
55
|
+
x, context = x_input.x, ctx_input.x
|
|
56
|
+
q = self.q_norm(rearrange(self.to_q(x), "nt (h d) -> nt h d", h=h))
|
|
57
|
+
k = self.k_norm(rearrange(self.to_k(context), "nt (h d) -> nt h d", h=kv_h))
|
|
58
|
+
v = rearrange(self.to_v(context), "nt (h d) -> nt h d", h=kv_h)
|
|
59
|
+
if groups > 1:
|
|
60
|
+
k = repeat(k, "nt h d -> nt (h g) d", g=groups)
|
|
61
|
+
v = repeat(v, "nt h d -> nt (h g) d", g=groups)
|
|
62
|
+
q, k = self.pos_encoding.forward_packed(
|
|
63
|
+
q, k, x_input.abs_positions, ctx_input.abs_positions
|
|
64
|
+
)
|
|
65
|
+
x_seq = PackedSequence(cu_seqlens=x_input.cu_seqlens, max_seqlen=x_input.max_seqlen)
|
|
66
|
+
ctx_seq = PackedSequence(cu_seqlens=ctx_input.cu_seqlens, max_seqlen=ctx_input.max_seqlen)
|
|
67
|
+
out = packed_attn_or_fallback(
|
|
68
|
+
q, k, v, x_seq, ctx_seq, causal=False, window_size=None, bias=None
|
|
69
|
+
)
|
|
70
|
+
return self.dropout(self.to_out(rearrange(out, "nt h d -> nt (h d)")))
|
|
71
|
+
|
|
72
|
+
def forward(self, x_input: SequenceInput, ctx_input: SequenceInput) -> Tensor:
|
|
73
|
+
match x_input:
|
|
74
|
+
case PaddedInput():
|
|
75
|
+
return self._forward_padded(x_input, ctx_input) # type: ignore[arg-type]
|
|
76
|
+
case PackedInput():
|
|
77
|
+
return self._forward_packed(x_input, ctx_input) # type: ignore[arg-type]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
from torch.nn.attention.varlen import varlen_attn as _varlen_attn
|
|
9
|
+
|
|
10
|
+
from stackformers.sequence import PackedSequence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def varlen_supported(q: Tensor) -> bool:
|
|
14
|
+
return q.is_cuda and q.dtype in (torch.float16, torch.bfloat16)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def padding_mask(mask: Tensor, dtype: torch.dtype) -> Tensor:
|
|
18
|
+
bias = torch.zeros(mask.shape, dtype=dtype, device=mask.device)
|
|
19
|
+
bias.masked_fill_(~mask, torch.finfo(dtype).min)
|
|
20
|
+
return bias.view(mask.shape[0], 1, 1, mask.shape[1])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def window_mask(n: int, s: int, window_size: int, causal: bool, device: torch.device) -> Tensor:
|
|
24
|
+
"""Additive sliding-window mask (0 = attend, -inf = ignore): (1, 1, n, s)."""
|
|
25
|
+
q_pos = torch.arange(n, device=device).unsqueeze(1)
|
|
26
|
+
k_pos = torch.arange(s, device=device).unsqueeze(0)
|
|
27
|
+
if causal:
|
|
28
|
+
allowed = (k_pos <= q_pos) & (k_pos >= q_pos - window_size)
|
|
29
|
+
else:
|
|
30
|
+
half = window_size // 2
|
|
31
|
+
allowed = (k_pos >= q_pos - half) & (k_pos <= q_pos + half)
|
|
32
|
+
mask = torch.zeros(1, 1, n, s, dtype=torch.float, device=device)
|
|
33
|
+
mask.masked_fill_(~allowed.unsqueeze(0).unsqueeze(0), float("-inf"))
|
|
34
|
+
return mask
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def varlen_window(causal: bool, window_size: int | None) -> tuple[int, int]:
|
|
38
|
+
if window_size is None:
|
|
39
|
+
return (-1, 0) if causal else (-1, -1)
|
|
40
|
+
return (window_size, 0) if causal else (window_size // 2, window_size // 2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def padded_sdpa(
|
|
44
|
+
q: Tensor,
|
|
45
|
+
k: Tensor,
|
|
46
|
+
v: Tensor,
|
|
47
|
+
mask: Tensor,
|
|
48
|
+
causal: bool,
|
|
49
|
+
window_size: int | None,
|
|
50
|
+
bias: Tensor | None,
|
|
51
|
+
) -> Tensor:
|
|
52
|
+
"""SDPA for padded inputs, applying padding mask + optional window mask + attention bias."""
|
|
53
|
+
n, s = q.shape[-2], k.shape[-2]
|
|
54
|
+
attn_mask = padding_mask(mask, q.dtype)
|
|
55
|
+
if bias is not None:
|
|
56
|
+
attn_mask = attn_mask + bias
|
|
57
|
+
if window_size is None:
|
|
58
|
+
if causal:
|
|
59
|
+
attn_mask = attn_mask + window_mask(n, s, s, causal=True, device=q.device)
|
|
60
|
+
# is_causal=False: causal constraint already encoded in attn_mask above
|
|
61
|
+
return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, is_causal=False)
|
|
62
|
+
else:
|
|
63
|
+
win_mask = window_mask(n, s, window_size, causal, q.device)
|
|
64
|
+
return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask + win_mask)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cu_to_indices(cu: Tensor, b: int) -> tuple[Tensor, Tensor]:
|
|
68
|
+
"""Return ``(batch_idx, pos_idx)``, each shape ``(nt,)``, from ``cu_seqlens``.
|
|
69
|
+
|
|
70
|
+
Both tensors are built without Python-side iteration over data-dependent lengths, so
|
|
71
|
+
this function is compatible with ``torch.export`` and ``torch.compile``.
|
|
72
|
+
|
|
73
|
+
The identity used for ``pos_idx`` is::
|
|
74
|
+
|
|
75
|
+
pos_idx[i] = i_global - cu[batch_idx[i]]
|
|
76
|
+
|
|
77
|
+
i.e. the within-document offset of the i-th flat token.
|
|
78
|
+
"""
|
|
79
|
+
lengths = cu[1:] - cu[:-1] # (b,) — never converted to a Python list
|
|
80
|
+
batch_idx = torch.repeat_interleave(
|
|
81
|
+
torch.arange(b, device=cu.device, dtype=torch.long), lengths
|
|
82
|
+
) # (nt,)
|
|
83
|
+
nt = batch_idx.shape[0]
|
|
84
|
+
pos_idx = torch.arange(nt, device=cu.device, dtype=torch.long) - cu[batch_idx]
|
|
85
|
+
return batch_idx, pos_idx
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _packed_heads_to_padded(x: Tensor, cu: Tensor, b: int, n: int) -> tuple[Tensor, Tensor]:
|
|
89
|
+
"""Scatter (nt, h, d) → (b, h, n, d). Returns padded tensor and (b, n) bool mask."""
|
|
90
|
+
batch_idx, pos_idx = _cu_to_indices(cu, b)
|
|
91
|
+
h, d = x.shape[1], x.shape[2]
|
|
92
|
+
out = x.new_zeros(b, h, n, d)
|
|
93
|
+
out[batch_idx, :, pos_idx] = x
|
|
94
|
+
mask = torch.zeros(b, n, dtype=torch.bool, device=cu.device)
|
|
95
|
+
mask[batch_idx, pos_idx] = True
|
|
96
|
+
return out, mask
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _padded_heads_to_packed(x: Tensor, mask: Tensor) -> Tensor:
|
|
100
|
+
"""Gather (b, h, n, d) → (nt, h, d) using bool mask (b, n)."""
|
|
101
|
+
return x.permute(0, 2, 1, 3)[mask]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def packed_attn(
|
|
105
|
+
q: Tensor,
|
|
106
|
+
k: Tensor,
|
|
107
|
+
v: Tensor,
|
|
108
|
+
q_seq: PackedSequence,
|
|
109
|
+
k_seq: PackedSequence,
|
|
110
|
+
causal: bool,
|
|
111
|
+
window_size: int | None,
|
|
112
|
+
) -> Tensor:
|
|
113
|
+
win = varlen_window(causal, window_size)
|
|
114
|
+
result = _varlen_attn(
|
|
115
|
+
query=q,
|
|
116
|
+
key=k,
|
|
117
|
+
value=v,
|
|
118
|
+
cu_seq_q=q_seq.cu_seqlens.to(torch.int32),
|
|
119
|
+
cu_seq_k=k_seq.cu_seqlens.to(torch.int32),
|
|
120
|
+
max_q=q_seq.max_seqlen,
|
|
121
|
+
max_k=k_seq.max_seqlen,
|
|
122
|
+
window_size=win,
|
|
123
|
+
)
|
|
124
|
+
assert isinstance(result, Tensor)
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def packed_attn_or_fallback(
|
|
129
|
+
q: Tensor,
|
|
130
|
+
k: Tensor,
|
|
131
|
+
v: Tensor,
|
|
132
|
+
q_seq: PackedSequence,
|
|
133
|
+
k_seq: PackedSequence,
|
|
134
|
+
causal: bool,
|
|
135
|
+
window_size: int | None,
|
|
136
|
+
bias: Tensor | None,
|
|
137
|
+
) -> Tensor:
|
|
138
|
+
"""Run varlen attention; fall back to padded SDPA when unavailable.
|
|
139
|
+
|
|
140
|
+
Inputs and output are all packed: q/k/v are (nt, h, d), result is (nt_q, h, d).
|
|
141
|
+
bias=None → no attention bias; varlen_attn is tried first.
|
|
142
|
+
bias=Tensor → attention bias present; varlen_attn is skipped (it has no bias slot) and a
|
|
143
|
+
warning is emitted. Fallback also activates on CPU, non-float16/bfloat16 dtypes, or GPUs
|
|
144
|
+
that do not support varlen_attn (e.g. compute capability < 8.0).
|
|
145
|
+
"""
|
|
146
|
+
if bias is not None and varlen_supported(q):
|
|
147
|
+
warnings.warn(
|
|
148
|
+
"varlen_attn does not support attention bias; falling back to padded SDPA. "
|
|
149
|
+
"Performance may be lower.",
|
|
150
|
+
stacklevel=2,
|
|
151
|
+
)
|
|
152
|
+
elif varlen_supported(q):
|
|
153
|
+
try:
|
|
154
|
+
return packed_attn(q, k, v, q_seq, k_seq, causal, window_size)
|
|
155
|
+
except RuntimeError as exc:
|
|
156
|
+
warnings.warn(
|
|
157
|
+
f"varlen_attn is not supported on this device ({exc}); "
|
|
158
|
+
"falling back to padded SDPA. Performance may be lower.",
|
|
159
|
+
stacklevel=2,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
b = int(q_seq.cu_seqlens.shape[0]) - 1
|
|
163
|
+
q_pad, q_mask = _packed_heads_to_padded(q, q_seq.cu_seqlens, b, q_seq.max_seqlen)
|
|
164
|
+
k_pad, k_mask = _packed_heads_to_padded(k, k_seq.cu_seqlens, b, k_seq.max_seqlen)
|
|
165
|
+
v_pad, _ = _packed_heads_to_padded(v, k_seq.cu_seqlens, b, k_seq.max_seqlen)
|
|
166
|
+
out_pad = padded_sdpa(q_pad, k_pad, v_pad, k_mask, causal, window_size, bias)
|
|
167
|
+
return _padded_heads_to_packed(out_pad, q_mask)
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
from stackformers.sequence import PaddedInput, SequenceInput
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class AttnBias(Protocol):
|
|
13
|
+
"""Additive bias injected into attention logits before softmax.
|
|
14
|
+
|
|
15
|
+
Receives the padded sequence input so implementations can use abs_positions.
|
|
16
|
+
Returns a broadcastable bias (1|b, 1|h, n, s), or None if the bias is a no-op
|
|
17
|
+
(which allows the packed path to use varlen_attn instead of SDPA).
|
|
18
|
+
Null implementation: NoAttnBias.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __call__(
|
|
22
|
+
self,
|
|
23
|
+
input: PaddedInput,
|
|
24
|
+
) -> Float[Tensor, "b h n s"] | None: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class SelfAttn(Protocol):
|
|
29
|
+
"""Self-attention over a sequence: maps input → x.
|
|
30
|
+
|
|
31
|
+
Implementation: SelfAttention.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __call__(self, input: SequenceInput) -> Tensor: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class CrossAttn(Protocol):
|
|
39
|
+
"""Cross-attention from x to context: maps (x_input, ctx_input) → x.
|
|
40
|
+
|
|
41
|
+
Implementation: CrossAttention.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __call__(self, x_input: SequenceInput, ctx_input: SequenceInput) -> Tensor: ...
|