gigl.nn.packed_attention#
Attention over packed variable-length sequences: the compute half of the packed backend.
This is the attention side of the packed backend of GraphTransformerEncoder.
The encoder builds one token sequence per anchor (for example, from its k-hop
neighborhood with gigl.transforms.static_khop), copies the real tokens of all
sequences into one [num_tokens, dim] buffer, and runs the transformer on that
buffer. packed_attention is the attention step and does not depend on how the
sequences were built: each sequence attends only to itself, with boundaries given
by cu_seqlens.
- Why this exists:
With uneven sequence lengths (about 90 real tokens out of 256 in our models), most of the linear-layer, FFN and attention work on a padded
[num_sequences, max_seq_len, dim]batch is spent on padding. Packed tokens spend it only on real ones.On CUDA this calls aten’s memory-efficient varlen attention kernel, wrapped in a custom op. The kernel is private because torch 2.8 has no public attention API that takes
cu_seqlens;_varlen_attentionexplains why the public options do not fit. The kernel runs in fp32, which inference needs for accuracy, and aten’s own backward kernel supplies the gradients.On CPU the same inputs go through SDPA on a padded grid. That is the reference the CUDA path is tested against, and it lets the packed backend run anywhere.
Packing needs one host read (the total token count, in
pack_sequences) so the buffer holds exactly the real tokens. Everything else has fixed shapes and no syncs, so the layers can be compiled withtorch.compile.
Data flow of a packed forward:
padded grid [num_sequences, max_seq_len, ...] lengths [num_sequences]
| |
| pack_sequences(lengths, max_seq_len) <------+
| -> flat_index, PackedLayout(cu_seqlens, max_seq_len)
| grid.flatten()[flat_index]
v
packed buffer [num_tokens, dim]
|
| per layer, on real tokens only:
| norm -> q / k / v linears -> [num_tokens, num_heads, head_dim]
| packed_attention(q, k, v, layout, ...)
| CUDA: aten memory-efficient attention with cu_seqlens
| CPU: SDPA on a padded grid (reference)
| output linear + residual -> FFN + residual
v
anchor readout: rows cu_seqlens[:-1], the first token of every sequence
- Constraints:
The varlen kernel is CUDA only; other devices use the padded SDPA path.
head_dimmust be a multiple of 8 for the aligned kernels.Attention dropout is not supported; the kernel’s varlen dropout has not been tested against the padded path.
cu_seqlensis int32, so the token count must fit in int32.
Example
>>> from gigl.transforms.sequence_layout import pack_sequences
>>> lengths = torch.tensor([3, 1, 2])
>>> flat_index, layout = pack_sequences(lengths, max_seq_len=3)
>>> flat_index.tolist()
[0, 1, 2, 3, 6, 7]
>>> num_tokens = int(lengths.sum())
>>> query = key = value = torch.randn(num_tokens, 2, 8)
>>> output = packed_attention(
... query, key, value, layout, query_is_first_token=False
... )
>>> output.shape
torch.Size([6, 2, 8])
>>> first_token_only = packed_attention(
... query[layout.cu_seqlens[:-1].long()],
... key,
... value,
... layout,
... query_is_first_token=True,
... )
>>> first_token_only.shape
torch.Size([3, 2, 8])
- Results:
We have seen the following with this path. These are observations on one production model, not benchmarks of this module alone.
One L4 GPU, 3-layer graph transformer (hidden 512, 8 heads), sequences of up to 256 tokens holding about 90 real tokens on average, realistic batches, steady state:
Forward, no compile: 225 -> 76 ms per batch (~3.0x).
Forward, per-layer
torch.compile: 53 ms per batch (~4.3x vs the padded path without compile).Training step: 944 -> 364 ms (~2.6x).
Peak GPU memory: 15.4 -> 6.8 GiB training, 6.9 -> 3.8 GiB forward.
120 L4 GPUs. These runs also include
torch.compileand loader changes, so the gain is not from packing alone:Training step: 6.62 -> 2.66 s (2.5x), peak memory 9.8 -> 4.3 GiB.
Full-graph inference over ~230M nodes: 1884 -> 1014 s end to end (1.9x). The model stopped being the bottleneck; sampling was.
Functions#
|
Attention within each packed sequence; the entry point of this module. |
Module Contents#
- gigl.nn.packed_attention.packed_attention(query, key, value, layout, query_is_first_token)[source]#
Attention within each packed sequence; the entry point of this module.
Uses the CUDA varlen kernel on GPU and padded SDPA elsewhere. With
query_is_first_token, only each sequence’s first token is queried, which is all the anchor readout of the last layer needs.Every sequence must contain at least one token (
lengths >= 1). An empty sequence gives wrong output on the padded path and is not supported.Args and returns are as in
_padded_packed_attention.- Parameters:
query (jaxtyping.Float[torch.Tensor, num_queries num_heads head_dim])
key (jaxtyping.Float[torch.Tensor, num_tokens num_heads head_dim])
value (jaxtyping.Float[torch.Tensor, num_tokens num_heads head_dim])
query_is_first_token (bool)
- Return type:
jaxtyping.Float[torch.Tensor, num_queries num_heads head_dim]