Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

stackformers

Typed, composable transformer library for PyTorch. Every architectural choice — positional encoding, normalization, feedforward variant — is an injected dependency, not a constructor flag.

uv add stackformers

Runnable examples cover variable-length layouts, causal language modeling, image patches, continuous point coordinates, user subclassing, and cached ONNX Runtime deployment. See the examples guide.


Why

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.

stackformers takes a different approach:

  • Swap any component without touching anything else — SelfAttention(config, pos_encoding=RoPE) vs SelfAttention(config, pos_encoding=ALiBi) — same call site, different object
  • No None checks in forward() — NoPosEncoding is a real object that passes q/k unchanged; the branch never exists
  • Sealed sequence unions — PaddedInput | PackedInput instead of optional cu_seqlens and mask arguments that conflict with each other
  • torch.compile / torch.export safe — no Python control flow on tensors inside any forward()
  • Structural protocols — bring your own implementation; no ABC inheritance required

Quick start

Import the public API with import stackformers as sf. Configs, models, protocols, base classes, factories, sequence helpers, and projection conversions are available directly through sf.

Zero boilerplate

import torch
import stackformers as sf

model = sf.TransformerEncoder(sf.plain_encoder_config(dim=512, heads=8, num_layers=6))

x    = torch.randn(2, 128, 512)
mask = torch.ones(2, 128, dtype=torch.bool)
out  = model(sf.make_padded_input(x, mask))   # (2, 128, 512)

Switch to packed (variable-length, no padding waste) — same weights:

cu  = torch.tensor([0, 64, 128], dtype=torch.int32)
out = model(sf.make_packed_input(x_flat, cu, max_seqlen=64))  # (128, 512)

Causal LM backbone:

sf.plain_encoder_config(dim=768, heads=12, num_layers=12, causal=True)

Sliding-window local attention (O(n · w)):

sf.windowed_encoder_config(dim=512, heads=8, num_layers=6, window_size=128)

Variable-width encoder — one model and head dimension per Transformer block:

config = sf.variable_width_encoder_config(
    d_models=[512, 512, 768, 768, 1024],
    dim_heads=[64, 64, 64, 64, 128],
)
model = sf.VariableWidthTransformerEncoder(config)

Omitting heads derives each block's head count as d_model // dim_head. Set heads=12 or heads=[12, 12, 16, 16, 16] to choose the internal attention width independently of d_models. dim_heads, heads, causal, ff_mult, dropout, and norm_placement accept either a shared value or a list with one entry per block. The preset inserts a bias-free learned projection before a block when its residual width differs from the preceding block; equal-width neighbors use an identity. The convenience factory expands each width pair into a VariableWidthEncoderLayerConfig whose attention, feed-forward, norm, positional encoding, and attention bias are all explicit. Construct these layer configs directly to replace any component; the model does not rebuild or override them. The input width must match the first layer's attn.dim, and the output uses the last layer's width.

Reference: Wu et al., “Variable-Width Transformers” (2026). The paper uses parameter-free residual resizing; this preset instead makes each width transition a learned linear projection, so it is a related experimental variant rather than an exact replica.

Encoder–decoder:

model = sf.TransformerDecoder(sf.plain_decoder_config(dim=512, heads=8, num_layers=6))
out   = model(sf.make_padded_input(x, mask), sf.make_padded_input(context, ctx_mask))

For autoregressive export, build cross K/V once and pass both required cache tensors to every one-token decoder invocation:

import torch

import stackformers as sf

model.eval()
cache_builder = sf.DecoderCrossAttentionCacheBuilder(model).eval()
cached_decoder = sf.CachedDecoderWrapper(model).eval()

cross_cache = cache_builder(context_input)
self_kv_cache = torch.empty(num_layers, 2, batch, kv_heads, 0, dim_head)
step_i = torch.tensor([0], dtype=torch.int64)
cache_onnx = torch.onnx.export(cache_builder, (context_input,), dynamo=True)
decoder_onnx = torch.onnx.export(
    cached_decoder,
    (target_token, cross_cache, self_kv_cache, step_i),
    dynamo=True,
)

The cache builder runs once per encoder context. Each decoder call consumes exactly one target token, the immutable cross cache, the growing self cache, and step_i; it returns the output token and a self cache extended by one position. K/V tensors retain kv_heads rather than expanded query heads. Cache construction positions cross K once, while cached decoding positions each new self K and both self/cross Q, so all built-in positional encodings remain available. Batch, source, and past-target axes are dynamic during export; model width, layer count, head geometry, and the one-token axis remain static because weights constrain them.

See the complete eager, ONNX export, and raw ONNX Runtime demonstration in examples/encoder_decoder_onnxruntime.py.

Explicit config

Full control with JSON round-trip via kind discriminators:

import stackformers as sf

cfg = sf.TransformerEncoderConfig(
    attn=sf.SelfAttentionConfig(dim=512, heads=8, dim_head=64, causal=False),
    ff=sf.SwiGLUConfig(dim=512, mult=4.0),
    norm=sf.RMSNormConfig(dim=512),
    pos_encoding=sf.RoPE1DConfig(dim_head=64),
    num_layers=6,
)
model = sf.TransformerEncoder(cfg)

# Serialise / restore
cfg2 = sf.TransformerEncoderConfig.model_validate(cfg.model_dump())

norm_placement is available on TransformerEncoderConfig, TransformerDecoderConfig, and CrossAttenderConfig, as well as their plain-config helpers. It accepts four layouts and defaults to "pre", so existing constructors, serialized configs, checkpoints, outputs, and gradients retain the previous behavior when the field is omitted. For a residual branch F with norm N:

Value Branch equation Reference
"pre" x + F(N(x)) Xiong et al., 2020
"post" N(x + F(x)) Vaswani et al., 2017
"sandwich" x + N_post(F(N_pre(x))) Ding et al., 2021
"reordered" x + N(F(x)) Liu et al., 2022; OLMo Team et al., 2025

Sandwich placement creates independent pre- and post-branch norms. Reordered placement follows the OLMo 2 residual layout; enable QK-Norm separately in the attention config when reproducing the broader OLMo 2 stabilization recipe. Preset construction maps each value to a focused encoder, decoder, or cross-attender layer class. Decoder and cross-attender placement applies to the target/query residual stream; the context sequence is not normalized or mutated by these layers.

Custom wiring

Wire layers yourself when presets aren't enough:

import stackformers as sf

pos  = sf.RotaryEmbedding1D(sf.RoPE1DConfig(dim_head=64))
attn = sf.SelfAttention(sf.SelfAttentionConfig(dim=512, heads=8, dim_head=64), pos_encoding=pos)

layers = [
    sf.TransformerLayer(
        self_attn=attn,
        ff=sf.SwiGLU(sf.SwiGLUConfig(dim=512)),
        norm_attn=sf.build_norm(sf.RMSNormConfig(dim=512)),
        norm_ff=sf.build_norm(sf.RMSNormConfig(dim=512)),
    )
    for _ in range(6)
]
encoder = sf.Encoder(layers=layers, final_norm=sf.build_norm(sf.RMSNormConfig(dim=512)))

Convert projection layouts

Load an ordinary checkpoint, then recursively pack supported self-attention, cross-attention, SwiGLU, GEGLU, and HardSwishGLU layers:

import stackformers as sf

model.load_state_dict(checkpoint)
model = sf.to_packed(model)
# Run or save the packed model.
model = sf.to_unpacked(model)

These helpers replace registered children in place and return the root, which may itself be replaced. Repeated calls keep layers already in the requested layout. Shared layer references and retained child modules stay shared. Built-in preset configs follow the new layout; save model.config.model_dump_json() alongside its state dict to reconstruct that layout later. Caller-owned config objects are left unchanged. Custom subclasses keep their own behavior and metadata; supported children are still visited.

Weights round-trip exactly; outputs match within dtype tolerance because matrix multiplication rounding can change. Convert before constructing an optimizer: replaced projection parameters do not migrate optimizer state, existing gradients, hooks, or cross-layer parameter aliases. Groups with mixed requires_grad fail before mutation. Cache wrappers support both ordinary and packed attention with the same calls and cache tensor layouts. Pack the model before constructing cache wrappers, or convert a containing module that registers the model and its existing wrappers together so their attention references remain shared. Packing a wrapper alone converts only its registered children; references outside that tree still point to the original attention modules. Projection packing is independent of the PackedInput sequence layout.

What's included

Area Variants
Self-attention Global, sliding-window (local); padded and packed backends; GQA / MQA
Cross-attention Global; padded and packed backends
Positional encoding RoPE-1D, RoPE-2D, none (null object)
Feedforward SwiGLU, HardSwishGLU, GEGLU, GELU, ReLU²
Normalization RMSNorm, LayerNorm
Presets Encoder, Decoder, CrossAttender

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.


Development

git clone <repo> && cd stackformers
uv sync --group dev

just fmt      # format
just lint     # lint
just types    # type-check
just test     # test
just check    # full CI gate

The quality gate checks the library, tests, and examples against Python 3.11. Ruff enforces annotations, common bug patterns, typing imports, and qualified, current suppressions. Pyrefly uses the strict preset with explicit/implicit Any, untyped returns, missing annotations, and stale suppressions treated as errors. Tool minimums are recorded in both the development dependencies and checker configs.

Module classes expose __call__ = forward only under TYPE_CHECKING. This gives callers the declared input/output contract while leaving PyTorch's hooks and runtime dispatch intact. Local Tensor annotations at built-in PyTorch projection boundaries rely on those modules' tensor-return contracts; they do not validate tensor shapes. Experimental kernel adapters capture the vendor's declared return type and validate the actual response before exposing Result[Tensor, VarlenFallback]. Export shape trees use opaque object leaves because PyTorch validates their module-dependent structure.

Core admission checks return Result[T, E] from stackformers.result, the closed union Ok[T] | Err[E]. Both variants are generic NamedTuples with a value or error and a literal kind tag. Match Ok(value=value) and Err(error=error) before accessing their payloads; keyword class patterns work with strict PyTorch export and full-graph compilation. No external Result package or unchecked unwrap method is required.

Recoverable core failures remain error data. Vendor adapters catch expected third-party exceptions and preserve their original causes; cache admission validates before construction without raising and catching its own expected failures. Existing public adapters retain tensor/configuration returns and raise when rejected inputs prevent that operation from proceeding. Pydantic still presents configuration failures as ValidationError; the experimental backend retains tensor-or-None returns and fallback warnings. Unexpected programming and PyTorch failures can propagate. See Result records.


License

See LICENSE.

Metadata

Release files for stackformers 5.0.0rc17

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stackformers 5.0.0rc17
File Size Uploaded Provenance
stackformers-5.0.0rc17.tar.gz 257.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for stackformers 5.0.0rc17
File Interpreter ABI Platform
stackformers-5.0.0rc17-py3-none-any.whl Python 3 none any Details

Total release size: 383.4 kB

Release files / stackformers-5.0.0rc17.tar.gz

Download URL stackformers-5.0.0rc17.tar.gz
Size 257.8 kB
Tags Source
SHA-256 checksum
How to use checksums
09f995c99ce68127ec205c6e6d7c9c65c22ad2330d9de92c3b10df40759de70e
BLAKE2b-256 checksum
How to use checksums
e8bb49c0dac83e1247548b4b3408115b06caf39c91bdfa1075bb7e78bf083b9f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / stackformers-5.0.0rc17-py3-none-any.whl

Download URL stackformers-5.0.0rc17-py3-none-any.whl
Size 125.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9c782cd7b556e7158e46c0e22aeefa7b4518230fbe90976c840563c4d62b542b
BLAKE2b-256 checksum
How to use checksums
794cd4f3638313ec1f0573d7bf49b3677bece425a68fa218420a0afcb281dc34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for stackformers 5.0.0rc17

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page