Skip to content

Commit 9eda5d5

Browse files
codexjomitchellnv
authored andcommitted
Document triangle attention kv_lengths fast path
1 parent ff0ce84 commit 9eda5d5

6 files changed

Lines changed: 128 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## Latest Changes
22
## (unreleased)
33

4+
### Added
5+
- [Torch] `cuet.triangle_attention` accepts `kv_lengths`, an int32 per-row key/value length tensor for right-padded sequence masks. Passing `kv_lengths` selects the Blackwell sm100f length fast path when available; dense `mask` remains the correct fallback path for arbitrary masks. Added `cuet.mask_to_kv_lengths` to convert and validate prefix masks.
6+
47
## 0.10.0 (2026-04-21)
58

69
### Added
@@ -282,4 +285,4 @@ output = cuex.equivariant_tensor_product(e, w, input)
282285

283286
## 0.1.0 (2024-11-18)
284287

285-
- Beta version of cuEquivariance released.
288+
- Beta version of cuEquivariance released.

cuequivariance_torch/cuequivariance_torch/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ Require `cuequivariance_ops_torch`.
306306

307307
```python
308308
# Triangle attention with pair bias
309-
out = cuet.triangle_attention(q, k, v, bias, mask=mask, scale=scale)
309+
kv_lengths = cuet.mask_to_kv_lengths(prefix_mask)
310+
out = cuet.triangle_attention(q, k, v, bias, scale=scale, kv_lengths=kv_lengths)
311+
# Arbitrary dense masks are supported through the fallback path:
312+
out = cuet.triangle_attention(q, k, v, bias, mask=holey_mask, scale=scale)
310313
# q, k, v: (B, N, H, Q/K, D), bias: (B, 1, H, Q, K)
311314

312315
# Triangle multiplicative update

cuequivariance_torch/cuequivariance_torch/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
)
4343
from .operations.spherical_harmonics import SphericalHarmonics
4444
from .primitives.triangle import (
45+
mask_to_kv_lengths,
4546
triangle_attention,
4647
triangle_multiplicative_update,
4748
attention_pair_bias,
@@ -96,6 +97,7 @@ def register_tensorrt_plugins():
9697
"vector_to_euler_angles",
9798
"Inversion",
9899
"SphericalHarmonics",
100+
"mask_to_kv_lengths",
99101
"triangle_attention",
100102
"triangle_multiplicative_update",
101103
"attention_pair_bias",

cuequivariance_torch/cuequivariance_torch/primitives/triangle.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,44 @@ class TriMulPrecision(enum.IntEnum): # type: ignore
3939
BFLOAT16 = 5
4040

4141

42+
def mask_to_kv_lengths(mask: torch.Tensor) -> torch.Tensor:
43+
r"""
44+
Convert a right-padded triangle-attention mask to per-row key/value lengths.
45+
46+
Args:
47+
mask (torch.Tensor): Boolean-like mask of shape (B, N, 1, 1, K). For B=1,
48+
can also be (N, 1, 1, K). The last dimension must be prefix-shaped:
49+
all True entries first, followed by all False entries.
50+
51+
Returns:
52+
torch.Tensor: int32 tensor of shape (B, N, 1, 1, 1) containing each row's
53+
effective key/value length. Zero-length rows are allowed.
54+
55+
Raises:
56+
ValueError: If the mask shape is not supported or the mask is not prefix-shaped.
57+
58+
Note:
59+
This helper validates the prefix contract. For torch.compile-heavy code, compute
60+
lengths before the compiled region and pass them to :func:`triangle_attention`
61+
with ``kv_lengths=...``.
62+
"""
63+
mask_bool = mask.to(dtype=torch.bool)
64+
while len(mask_bool.shape) < 5:
65+
mask_bool = mask_bool.unsqueeze(0)
66+
if mask_bool.ndim != 5 or mask_bool.shape[2:4] != (1, 1):
67+
raise ValueError(
68+
"mask_to_kv_lengths: mask must have shape (B, N, 1, 1, K) "
69+
f"after adding leading singleton dimensions, got {tuple(mask_bool.shape)}"
70+
)
71+
lengths = mask_bool.to(dtype=torch.int32).sum(dim=-1, keepdim=True).to(torch.int32)
72+
prefix = torch.arange(mask_bool.shape[-1], device=mask_bool.device).view(
73+
1, 1, 1, 1, mask_bool.shape[-1]
74+
) < lengths
75+
if not bool(torch.all(mask_bool == prefix).item()):
76+
raise ValueError("mask_to_kv_lengths: mask must be right-padded/prefix-shaped")
77+
return lengths.detach().contiguous()
78+
79+
4280
def triangle_attention(
4381
q: torch.Tensor,
4482
k: torch.Tensor,
@@ -47,6 +85,8 @@ def triangle_attention(
4785
mask: Optional[torch.Tensor] = None,
4886
scale: Optional[float] = None,
4987
return_aux: bool = False,
88+
*,
89+
kv_lengths: Optional[torch.Tensor] = None,
5090
) -> torch.Tensor | Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
5191
r"""
5292
Triangle Attention
@@ -68,10 +108,17 @@ def triangle_attention(
68108
capability 10.0 or 10.3), will be cast to match q/k/v dtype (bf16/fp16) for best
69109
performance.
70110
mask (torch.Tensor, optional): Mask tensor of shape (B, N, 1, 1, K). For B=1, can also be (N, 1, 1, K).
71-
Will be cast to bool internally.
111+
Will be cast to bool internally. Dense masks accept any pattern and route to the
112+
correct fallback path. For right-padded masks, pass ``kv_lengths`` instead to use
113+
the Blackwell sm100f length fast path.
72114
scale (float, optional): Float scale for q (s in the equation). If None, value 1/sqrt(d) is used.
73115
return_aux (bool): If True, two auxiliary tensors are returned along with the result.
74116
Defaults to False.
117+
kv_lengths (torch.Tensor, optional): int32 tensor of shape (B, N, 1, 1, 1)
118+
containing each row's effective K length. This represents a right-padded /
119+
prefix mask: positions ``j < kv_lengths[b, n]`` are valid and later positions
120+
are masked. Pass either ``mask`` or ``kv_lengths``, not both. On supported
121+
Blackwell cu13 builds, ``kv_lengths`` selects the sm100f length fast path.
75122
76123
Note:
77124
- B: batch size
@@ -90,12 +137,12 @@ def triangle_attention(
90137
(1) Context is saved for backward pass. You don't need to save it manually.
91138
(2) Kernel precision (fp32, bf16, fp16) is based on input dtypes. For tf32, set it from torch global scope
92139
(3) Triangle attention kernel supports: all hidden_dim<=32 and divisible by 4 for tf32/fp32, and for all hidden_dim<=128 and divisible by 8 for bf16/fp16 (standard kernels). On Blackwell GPUs (compute capability 10.0 or 10.3), the sm100f kernel supports hidden_dim<=256 for forward passes and hidden_dim<=128 for backward passes. In the rare instance that the kernel does not support an input config, fallback to torch is enabled instead of erroring out.
93-
(4) Blackwell-optimized kernels (for compute capabilities 10.0 and 10.3) provide superior performance especially for long sequences and higher head dimensions. These kernels require the sequence length N to be a multiple of 8 for the forward pass; pad the sequence if necessary. The kernel provides optimal performance for a "padding mask" consisting in (all True, followed by all False) in the last dimension. Currently, this feature is supported only for cu13 builds.
140+
(4) Blackwell-optimized kernels (for compute capabilities 10.0 and 10.3) provide superior performance especially for long sequences and higher head dimensions. These kernels require the key/value sequence length K to be a multiple of 8 for the forward pass; pad the sequence if necessary. Use ``kv_lengths`` for right-padded sequence masks to select the sm100f length fast path. A dense ``mask`` without ``kv_lengths`` remains correct for arbitrary or holey patterns, but it routes to the fallback path.
94141
95142
Example:
96143
>>> import torch
97144
>>> import math
98-
>>> from cuequivariance_torch import triangle_attention
145+
>>> from cuequivariance_torch import mask_to_kv_lengths, triangle_attention
99146
>>> if torch.cuda.is_available(): # doctest: +SKIP
100147
... device = torch.device("cuda")
101148
... # Set up dimensions
@@ -109,14 +156,25 @@ def triangle_attention(
109156
... device=device, dtype=torch.float16, requires_grad=True)
110157
... bias = torch.randn(batch_size, 1, num_heads, seq_len, seq_len,
111158
... device=device, dtype=torch.float32, requires_grad=True)
112-
... # Create optional mask
113-
... mask = torch.rand(batch_size, seq_len, 1, 1, seq_len,
114-
... device=device) < 0.5
159+
... # Right-padded sequence mask: valid tokens first, padding last.
160+
... seq_lengths = torch.tensor([96], device=device, dtype=torch.int32)
161+
... positions = torch.arange(seq_len, device=device)
162+
... row_valid = positions.view(1, seq_len) < seq_lengths.view(batch_size, 1)
163+
... col_valid = positions.view(1, seq_len) < seq_lengths.view(batch_size, 1)
164+
... mask = row_valid.view(batch_size, seq_len, 1, 1, 1) & col_valid.view(
165+
... batch_size, 1, 1, 1, seq_len)
166+
... kv_lengths = mask_to_kv_lengths(mask)
115167
... # Calculate scale
116168
... scale = 1 / math.sqrt(hidden_dim)
117-
... # Forward pass
169+
... # Forward pass using the Blackwell sm100f length fast path when available.
118170
... output, lse, max_val = triangle_attention(
119-
... q=q, k=k, v=v, bias=bias, mask=mask, scale=scale, return_aux=True)
171+
... q=q, k=k, v=v, bias=bias, scale=scale, return_aux=True,
172+
... kv_lengths=kv_lengths)
173+
... # Arbitrary dense masks are still correct; they use the fallback path.
174+
... arbitrary_mask = torch.rand(batch_size, seq_len, 1, 1, seq_len,
175+
... device=device) < 0.5
176+
... fallback_output = triangle_attention(
177+
... q=q, k=k, v=v, bias=bias, mask=arbitrary_mask, scale=scale)
120178
... print(output.shape) # torch.Size([1, 128, 2, 128, 32])
121179
... # Create gradient tensor and perform backward pass
122180
... grad_out = torch.randn_like(output)
@@ -140,7 +198,7 @@ def triangle_attention(
140198
"Error importing triangle_attention from cuequivariance_ops_torch."
141199
)
142200
else:
143-
return f(q, k, v, bias, mask, scale, return_aux)
201+
return f(q, k, v, bias, mask, scale, return_aux, kv_lengths=kv_lengths)
144202

145203

146204
def triangle_multiplicative_update(

cuequivariance_torch/tests/primitives/triangle_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,56 @@ def test_triangle_attention():
9494
)
9595

9696

97+
def test_triangle_attention_kv_lengths():
98+
if torch.cuda.is_available():
99+
device = torch.device("cuda")
100+
batch_size, seq_len, num_heads, hidden_dim = 1, 16, 2, 32
101+
q = torch.randn(
102+
batch_size,
103+
seq_len,
104+
num_heads,
105+
seq_len,
106+
hidden_dim,
107+
device=device,
108+
dtype=torch.float16,
109+
)
110+
k = torch.randn_like(q)
111+
v = torch.randn_like(q)
112+
bias = torch.randn(
113+
batch_size,
114+
1,
115+
num_heads,
116+
seq_len,
117+
seq_len,
118+
device=device,
119+
dtype=torch.float32,
120+
)
121+
seq_lengths = torch.tensor([12], device=device, dtype=torch.int32)
122+
positions = torch.arange(seq_len, device=device)
123+
row_valid = positions.view(1, seq_len) < seq_lengths.view(batch_size, 1)
124+
col_valid = positions.view(1, seq_len) < seq_lengths.view(batch_size, 1)
125+
mask = row_valid.view(batch_size, seq_len, 1, 1, 1) & col_valid.view(
126+
batch_size, 1, 1, 1, seq_len
127+
)
128+
kv_lengths = cuet.mask_to_kv_lengths(mask)
129+
130+
assert kv_lengths.shape == torch.Size([batch_size, seq_len, 1, 1, 1])
131+
assert kv_lengths.dtype == torch.int32
132+
output = cuet.triangle_attention(
133+
q=q,
134+
k=k,
135+
v=v,
136+
bias=bias,
137+
scale=1 / math.sqrt(hidden_dim),
138+
kv_lengths=kv_lengths,
139+
)
140+
assert output.shape == torch.Size(
141+
[batch_size, seq_len, num_heads, seq_len, hidden_dim]
142+
)
143+
zero_rows = kv_lengths.view(batch_size, seq_len) == 0
144+
torch.testing.assert_close(output[zero_rows], torch.zeros_like(output[zero_rows]))
145+
146+
97147
def test_triangle_multiplicative_update():
98148
if torch.cuda.is_available():
99149
device = torch.device("cuda")

docs/api/cuequivariance_torch.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Triangle
6868
:toctree: generated/
6969
:template: function_template.rst
7070

71+
mask_to_kv_lengths
7172
triangle_attention
7273
triangle_multiplicative_update
7374
attention_pair_bias

0 commit comments

Comments
 (0)