Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ab94e60
Modernize conda environment (#34)
sdvillal Mar 23, 2026
ce70ebb
fix linter problems
jandom Mar 23, 2026
9b1749c
add pre-commit
jandom Mar 23, 2026
6fbef74
Merge branch 'main' into pixi-beta
jandom Mar 26, 2026
57736cd
Merge branch 'public-main' into pixi-beta
jandom Mar 31, 2026
784502b
Merge branch 'public-main' into pixi-beta
jandom Apr 4, 2026
f696d6c
add pixi.excalidraw to docs
jandom Apr 7, 2026
4ce0467
Merge branch 'public-main' into pixi-beta
jandom Apr 8, 2026
c101a86
remove blackwell build instructions (obsolete)
jandom Apr 8, 2026
cf4bfb6
update docs to recommend pixi
jandom Apr 8, 2026
4e36782
better docs on pixi
jandom Apr 8, 2026
48e06e3
update pixi.lock
jandom Apr 8, 2026
8a4a26b
docker build and tests for pixi
jandom Apr 8, 2026
3f9ed35
Merge branch 'main' into pixi-beta
jandom Apr 8, 2026
888f070
set a sensible 2mb default
jandom Apr 8, 2026
39ddce9
more context manager plus dirty dataclass
jandom Apr 8, 2026
07d3454
unit tests
jandom Apr 9, 2026
5e42337
Merge branch 'public-main' into pixi-beta
jandom Apr 9, 2026
8a745e9
more linting
jandom Apr 9, 2026
ba24b95
missed a dep: regenerate pixi.lock
jandom Apr 9, 2026
68e743e
Merge branch 'main' into pixi-beta
jandom Apr 9, 2026
feeaacd
remove duplicate projects
jandom Apr 9, 2026
a6df9f7
Merge branch 'main' into pixi-beta
jandom Apr 14, 2026
ba83d3e
review: comments from Jennifer
jandom Apr 15, 2026
bc7badc
Merge branch 'public-main' into pixi-beta
jandom Apr 15, 2026
299e596
update pixi.lock
jandom Apr 15, 2026
eb1f9e4
cuequivariance support in pixi
jandom Apr 15, 2026
2fa2cfc
Merge branch 'public-main' into pixi-beta-cuequivariance
jandom Apr 23, 2026
a501be1
update documentation for cuequivariance for kernels
jnwei Apr 27, 2026
cc57a21
Merge branch 'public-main' into pixi-beta-cuequivariance
jandom Apr 28, 2026
3aa0a10
regenerate pixi.lock
jandom Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/source/kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ Note: cuEquivariance acceleration can be used while DeepSpeed acceleration is en
cuEquivariance would take precedence, and then would fall back to either DeepSpeed (if enabled) or PyTorch for the shapes it does not handle efficiently.
Notably, it would fall back for shorter sequences (threshold controlled by `CUEQ_TRIATTN_FALLBACK_THRESHOLD` environment variable), and for shapes with hidden dimension > 128 (diffusion transformer shapes).

To enable, first install OpenFold3 with cuEquivariance:
To enable cuequivariance with pixi, use the `openfold3-cuda12-pypi` or `openfold3-cuda13-pypi` environment. Below is a example inference command

```bash
pixi run -e openfold3-cuda12-pypi run_openfold predict --query-json=query_ubiquitin.json --runner-yaml=cuequivariance.yml
```

For other workflows, cuequivariance must first be installed with the cuequivariance optional dependency, e.g.

```bash
pip install openfold3[cuequivariance]
Expand All @@ -14,6 +20,7 @@ pip install openfold3[cuequivariance]
Then, to enable these kernels via the runner.yaml, add the following:

```yaml
# cuequivariance.yml
model_update:
presets:
- "predict"
Expand All @@ -26,4 +33,4 @@ model_update:
use_deepspeed_evo_attention: true # set this to False to use cueq only
```

This is specifically for inference, but similar settings can be used for training.
This runner.yml is specifically for inference, but similar settings can be used for training.
10 changes: 6 additions & 4 deletions openfold3/core/kernels/cueq_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import torch


def is_cuequivariance_installed() -> bool:
"""Check if cuequivariance_torch package is installed (regardless of CUDA)."""
return importlib.util.find_spec("cuequivariance_torch") is not None


def is_cuequivariance_available() -> bool:
"""
Check if cuequivariance_torch is installed and CUDA is available.
Even if cuequivariance_torch is installed, it only works with CUDA.
"""
return (
importlib.util.find_spec("cuequivariance_torch") is not None
and torch.cuda.is_available()
)
return is_cuequivariance_installed() and torch.cuda.is_available()
63 changes: 46 additions & 17 deletions openfold3/core/model/primitives/attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,40 +746,69 @@ def _cueq_triangle_attn(q, k, v, biases, scale):
)
mask_bias, triangle_bias = biases

##VS: the cueq attn kernel only allows up to 5 input dimensions:
## (batch,*,n_head, *,c_hidden); batch here denotes multiple
## structures in a single fwd pass; while this is fine for the
## pairformer, in the template module we have
## inputs of shape (batch, n_tmpl, n_res,n_head, n_res, c_in)
## so therefore we need to reshape the input to remove the
## extra batch dimension, then reshape it back to the original
# cuequivariance triangle_attention expects 5D inputs:
# q/k/v: (B, N, H, S, D)
# bias: (B, 1, H, S_qo, S_kv) — N must be 1
# mask: (B, N, 1, 1, S_kv)
#
# Inputs arrive here in one of three shapes depending on call path:
# 6D — template module: (batch, n_tmpl, N, H, S, D) → collapse to 5D
# 5D — standard pairformer: already correct
# 4D — after chunk_layer flattens batch dims: (chunk, H, S, D) → promote to 5D

# 6D → 5D: merge the (batch, n_tmpl) dims into a single batch dim.
if len(q.shape) > 5:
assert len(q.shape) == 6, (
"max number of dimensions for CUEQ triangle attention kernel is 6"
)
is_batched_input = True
batch, n_tmpl, n_res, n_head, c_hidden = q.shape[:5]
batch, n_tmpl = q.shape[:2]
# q: (batch, n_tmpl, N, H, S, D) → (batch*n_tmpl, N, H, S, D)
q = q.view(batch * n_tmpl, *q.shape[2:])
k = k.view(batch * n_tmpl, *k.shape[2:])
v = v.view(batch * n_tmpl, *v.shape[2:])
# mask_bias: (batch, n_tmpl, N, 1, 1, S) → (batch*n_tmpl, N, 1, 1, S)
mask_bias = mask_bias.view(batch * n_tmpl, *mask_bias.shape[2:])
# triangle_bias: (batch, n_tmpl, 1, H, S, S) → (batch*n_tmpl, 1, H, S, S)
triangle_bias = triangle_bias.view(batch * n_tmpl, *triangle_bias.shape[2:])
##VS: The mask for the triangle attention kernel needs to be a
## boolean mask - the default mask is an additive mask, where
## 0 means no masking and -inf means masking. so we need to
## convert this to a boolean mask where positions to keep are
## True, and positions to mask are False.

# 4D → 5D: chunk_layer flattens batch dims and slices into chunks.
# Promote to 5D with N=1 so each chunk entry is an independent batch item.
# cuequivariance >=0.8 requires bias shape (B, 1, H, Q, K) with exact
# batch match — no implicit broadcasting.
is_chunked_input = len(q.shape) == 4
if is_chunked_input:
# q: (chunk, H, S, D) → (chunk, 1, H, S, D)
q = q.unsqueeze(1)
k = k.unsqueeze(1)
v = v.unsqueeze(1)
# mask_bias: (chunk, 1, 1, S) → (chunk, 1, 1, 1, S)
mask_bias = mask_bias.unsqueeze(1)
# triangle_bias: (chunk, H, S, S) → (chunk, 1, H, S, S)
# or: (1, H, S, S) → (1, 1, H, S, S) when chunk_layer kept B=1
triangle_bias = triangle_bias.unsqueeze(1)
# chunk_layer skips expanding bias when all its batch dims are 1,
# so bias may have B=1 while q has B=chunk. Expand to match.
if triangle_bias.shape[0] != q.shape[0]:
# (1, 1, H, S, S) → (chunk, 1, H, S, S)
triangle_bias = triangle_bias.expand(q.shape[0], *triangle_bias.shape[1:])
if mask_bias.shape[0] != q.shape[0]:
mask_bias = mask_bias.expand(q.shape[0], *mask_bias.shape[1:])
Comment on lines +775 to +796

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christinaflo this migrates how we call a recent cueq - does it look sane? (it obviously doesn't :D)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK. IMO we're not going to get much cleaner without having separate functions depending on the number of dimensions in the input.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well actually it's the right way to fix chunking when BS>1. But we do want to avoid expand() on bias for BS=1 (it becomes huge and we do allocate real memory for it on contiguous() call), so I suggest adding a few conditionals to process BS=1 case just like before :

diff --git a/openfold3/core/model/primitives/attention.py b/openfold3/core/model/primitives/attention.py
index 46c924aa..dbe74e87 100644
--- a/openfold3/core/model/primitives/attention.py
+++ b/openfold3/core/model/primitives/attention.py
@@ -773,10 +773,12 @@ def _cueq_triangle_attn(q, k, v, biases, scale):
         triangle_bias = triangle_bias.view(batch * n_tmpl, *triangle_bias.shape[2:])

     # 4D → 5D: chunk_layer flattens batch dims and slices into chunks.
+    # chunk_layer skips expanding bias when all its batch dims are 1,
+    # so bias may have B=1 while q has B=chunk. In this case, we're good - otherwise:
     # Promote to 5D with N=1 so each chunk entry is an independent batch item.
     # cuequivariance >=0.8 requires bias shape (B, 1, H, Q, K) with exact
     # batch match — no implicit broadcasting.
-    is_chunked_input = len(q.shape) == 4
+    is_chunked_input = len(q.shape) == 4 and triangle_bias.shape[0] > 1
     if is_chunked_input:
         # q: (chunk, H, S, D) → (chunk, 1, H, S, D)
         q = q.unsqueeze(1)
@@ -787,8 +789,7 @@ def _cueq_triangle_attn(q, k, v, biases, scale):
         # triangle_bias: (chunk, H, S, S) → (chunk, 1, H, S, S)
         #   or: (1, H, S, S) → (1, 1, H, S, S) when chunk_layer kept B=1
         triangle_bias = triangle_bias.unsqueeze(1)
-        # chunk_layer skips expanding bias when all its batch dims are 1,
-        # so bias may have B=1 while q has B=chunk. Expand to match.
+        # This should not happen. Just in case. Expand to match.
         if triangle_bias.shape[0] != q.shape[0]:
             # (1, 1, H, S, S) → (chunk, 1, H, S, S)
             triangle_bias = triangle_bias.expand(q.shape[0], *triangle_bias.shape[1:])
@@ -803,11 +804,14 @@ def _cueq_triangle_attn(q, k, v, biases, scale):
     o = triangle_attention(q, k, v, bias=triangle_bias, mask=mask_bias, scale=scale)

     # Undo the promotions in reverse order.
-    if is_chunked_input:
+    if len(q.shape) == 4:
+        ##VS: There's a bug in cueq where if the input is missing the batch dim
+        ## the outputs adds it in and so we need to remove it here
+        o = o.squeeze(0)
+    elif is_chunked_input:
         # (chunk, 1, H, S, D) → (chunk, H, S, D)
         o = o.squeeze(1)
-
-    if is_batched_input:
+    elif is_batched_input:
         # (batch*n_tmpl, N, H, S, D) → (batch, n_tmpl, N, H, S, D)
         o = o.view(batch, n_tmpl, *o.shape[1:])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christinaflo : I have tested that code with BS=1 and BS=2 using test_kernels, and it worked for me:

--- a/openfold3/tests/test_kernels.py
+++ b/openfold3/tests/test_kernels.py
@@ -437,8 +437,6 @@ class TestKernels(unittest.TestCase):
         batch_size = consts.batch_size
         if chunk_size is not None and (
             use_deepspeed_evo_attention
-            or use_cueq_triangle_kernels
-            or use_triton_triangle_kernels
         ):

(actually, triton_kernels also worked with BS>1, with minor accuracy error).


# Convert additive mask (0 = keep, -inf = mask) to boolean (True = keep).
if mask_bias.dtype != torch.bool:
mask_bias = mask_bias == 0

# At this point all tensors are 5D with the shapes expected by the kernel.
o = triangle_attention(q, k, v, bias=triangle_bias, mask=mask_bias, scale=scale)

if len(q.shape) == 4:
##VS: There's a bug in cueq where if the input is missing the batch dim
## the outputs adds it in and so we need to remove it here
o = o.squeeze(0)
# Undo the promotions in reverse order.
if is_chunked_input:
# (chunk, 1, H, S, D) → (chunk, H, S, D)
o = o.squeeze(1)

if is_batched_input:
# (batch*n_tmpl, N, H, S, D) → (batch, n_tmpl, N, H, S, D)
o = o.view(batch, n_tmpl, *o.shape[1:])

o = o.transpose(-2, -3)
Expand Down
15 changes: 11 additions & 4 deletions openfold3/tests/compare_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

import torch

from openfold3.core.kernels.cueq_utils import is_cuequivariance_available
from openfold3.core.kernels.cueq_utils import (
is_cuequivariance_available,
is_cuequivariance_installed,
)


def skip_if_rocm():
Expand All @@ -39,9 +42,13 @@ def skip_unless_ds4s_installed():


def skip_unless_cueq_installed():
return unittest.skipUnless(
is_cuequivariance_available(), "Requires CU-Equivaraince to be installed"
)
if not is_cuequivariance_installed():
reason = "Requires cuequivariance to be installed"
elif not torch.cuda.is_available():
reason = "Requires CUDA (cuequivariance is installed but no GPU available)"
else:
reason = ""
return unittest.skipUnless(is_cuequivariance_available(), reason)


def skip_unless_triton_installed():
Expand Down
Loading
Loading