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,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: ...