colabfold-kernels 0.1.0__tar.gz

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.
@@ -0,0 +1,26 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2026 The ColabFold Development Team
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
26
+
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: colabfold-kernels
3
+ Version: 0.1.0
4
+ Summary: Fast fused kernels for ColabFold
5
+ Author-email: Milot Mirdita <milot@mirdita.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mirditalab/colabfold-kernels
8
+ Project-URL: Issues, https://github.com/mirditalab/colabfold-kernels/issues
9
+ Keywords: alphafold,colabfold,pallas,triton,jax,attention,bioinformatics
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # colabfold-kernels
20
+
21
+ Fast fused kernels for ColabFold, written in
22
+ [Pallas](https://docs.jax.dev/en/latest/pallas/index.html) with native CUDA-fallback
23
+ for Volta- and Turing-generation GPUs.
24
+
25
+ ```python
26
+ import jax.numpy as jnp
27
+ from colabfold_kernels import attention, dispatch
28
+
29
+ kernel = attention(dispatch(), jnp.bfloat16, key_dim=96, value_dim=96, layout="seq")
30
+ if kernel is not None:
31
+ out = kernel(q, k, v, mask_bias, nonbatched_bias, scale)
32
+ ```
33
+
34
+ `attention`, `layer_norm` and `gated_dual_proj` each return `None` if not supported.
@@ -0,0 +1,16 @@
1
+ # colabfold-kernels
2
+
3
+ Fast fused kernels for ColabFold, written in
4
+ [Pallas](https://docs.jax.dev/en/latest/pallas/index.html) with native CUDA-fallback
5
+ for Volta- and Turing-generation GPUs.
6
+
7
+ ```python
8
+ import jax.numpy as jnp
9
+ from colabfold_kernels import attention, dispatch
10
+
11
+ kernel = attention(dispatch(), jnp.bfloat16, key_dim=96, value_dim=96, layout="seq")
12
+ if kernel is not None:
13
+ out = kernel(q, k, v, mask_bias, nonbatched_bias, scale)
14
+ ```
15
+
16
+ `attention`, `layer_norm` and `gated_dual_proj` each return `None` if not supported.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "colabfold-kernels"
7
+ version = "0.1.0"
8
+ description = "Fast fused kernels for ColabFold"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Milot Mirdita", email = "milot@mirdita.de" }]
13
+ keywords = ["alphafold", "colabfold", "pallas", "triton", "jax", "attention", "bioinformatics"]
14
+ classifiers = [
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
19
+ ]
20
+ [project.urls]
21
+ Homepage = "https://github.com/mirditalab/colabfold-kernels"
22
+ Issues = "https://github.com/mirditalab/colabfold-kernels/issues"
23
+
24
+ [tool.setuptools]
25
+ package-dir = { "" = "src" }
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """Fused Pallas/Triton kernels for ColabFold's AlphaFold2 and AlphaFold3 backends."""
2
+
3
+ from colabfold_kernels.device import compute_capability, dispatch, shared_memory_limit
4
+ from colabfold_kernels.fused_ops import attention, gated_dual_proj, layer_norm
5
+
6
+ __all__ = ["attention", "compute_capability", "dispatch", "gated_dual_proj",
7
+ "layer_norm", "shared_memory_limit", "__version__"]
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,29 @@
1
+ """What the GPUs here can run, so a caller need not work it out itself."""
2
+
3
+
4
+ def compute_capability():
5
+ """The NVIDIA compute capability as an integer, e.g. 75, or None."""
6
+ try:
7
+ import jax
8
+ cc = str(jax.devices()[0].compute_capability)
9
+ return int(round(float(cc) * 10)) if "." in cc else int(cc)
10
+ except Exception:
11
+ return None
12
+
13
+
14
+ def shared_memory_limit():
15
+ """The smallest shared memory budget across the GPUs here, or None."""
16
+ import jax
17
+ limits = [getattr(d, "shared_memory_per_block_optin", 0) for d in jax.local_devices()
18
+ if d.platform == "gpu"]
19
+ limits = [limit for limit in limits if limit]
20
+ return min(limits) if limits else None
21
+
22
+
23
+ def dispatch(backend="auto"):
24
+ """The mapping the fused_ops selectors read. 'auto' picks what this GPU supports."""
25
+ cc = compute_capability()
26
+ if backend == "auto":
27
+ # XLA gates Pallas/Triton to sm_80+, so older NVIDIA parts take the CUDA kernels
28
+ backend = "cuda_legacy" if cc is not None and cc < 80 else "pallas"
29
+ return {"use_pallas": True, "kernel_backend": backend, "compute_capability": cc}
@@ -0,0 +1,72 @@
1
+ """Pick the fused kernel (volta, Pallas, or None for XLA) for each op."""
2
+ import functools
3
+
4
+
5
+ def _legacy_cc(global_config):
6
+ """Give the compute capability if the CUDA kernels were asked for, else None."""
7
+ if global_config.get('kernel_backend', 'pallas') != 'cuda_legacy':
8
+ return None
9
+ return global_config.get('compute_capability')
10
+
11
+
12
+ def _volta():
13
+ from colabfold_kernels import volta
14
+ return volta
15
+
16
+
17
+ def attention(global_config, dtype, key_dim, value_dim, layout='heads'):
18
+ """-> f(q, k, v, mask_bias, nonbatched_bias, scale) or None.
19
+
20
+ layout 'heads' takes q/k/v as [b, h, S, c], 'seq' as [b, S, h, c].
21
+ """
22
+ import jax.numpy as jnp
23
+ if not global_config.get('use_pallas', False):
24
+ return None
25
+ cc = _legacy_cc(global_config)
26
+ if dtype == jnp.float16 and cc is not None and layout == 'heads':
27
+ va = _volta()
28
+ # The kernels only handle a fixed set of head dims, and need key == value.
29
+ if (va.available(cc) and key_dim == value_dim
30
+ and va.supports(int(key_dim), cc)):
31
+ return functools.partial(va.volta_attention, cc=cc)
32
+ if (global_config.get('kernel_backend', 'pallas') == 'pallas'
33
+ and dtype in (jnp.bfloat16, jnp.float16)):
34
+ from colabfold_kernels.tri_flash import pallas_attention
35
+ if layout == 'seq':
36
+ return functools.partial(pallas_attention, seq_major=True)
37
+ return pallas_attention
38
+ return None
39
+
40
+
41
+ def layer_norm(global_config, dtype):
42
+ """-> f(x, scale, offset, *, eps) or None. Last-axis, scale+offset only."""
43
+ import jax.numpy as jnp
44
+ if not global_config.get('use_pallas', False):
45
+ return None
46
+ cc = _legacy_cc(global_config)
47
+ if dtype == jnp.float16 and cc is not None:
48
+ va = _volta()
49
+ if va.ops_available(cc):
50
+ return va.volta_layer_norm
51
+ if (global_config.get('kernel_backend', 'pallas') == 'pallas'
52
+ and dtype in (jnp.bfloat16, jnp.float32)):
53
+ from colabfold_kernels.tri_mul import pallas_layer_norm
54
+ return pallas_layer_norm
55
+ return None
56
+
57
+
58
+ def gated_dual_proj(global_config, dtype):
59
+ """-> f(x, wp, bp, wg, bg, mask, *, split) or None."""
60
+ import jax.numpy as jnp
61
+ if not global_config.get('use_pallas', False):
62
+ return None
63
+ cc = _legacy_cc(global_config)
64
+ if dtype == jnp.float16 and cc is not None:
65
+ va = _volta()
66
+ if va.ops_available(cc):
67
+ return functools.partial(va.volta_gated_dual_proj, cc=cc)
68
+ if (global_config.get('kernel_backend', 'pallas') == 'pallas'
69
+ and dtype in (jnp.bfloat16, jnp.float16)):
70
+ from colabfold_kernels.tri_mul import gated_dual_proj as gdp
71
+ return gdp
72
+ return None
@@ -0,0 +1,167 @@
1
+ """Pallas/Triton flash attention for the AF2 Evoformer (MSA + triangle).
2
+
3
+ Inference (forward) only, no backward pass
4
+
5
+ Computes, per (batch n, head h):
6
+ softmax_k( scale * Q_q . K_k + bias_{h,q,k} if mask_{n,k} else -inf ) . V_k
7
+ Layout: q/k/v [N, H, S, D]; bias [H, Sq, Sk] (shared across N); kmask [N, Sk] bool.
8
+ """
9
+ import functools, math
10
+ import numpy as np
11
+ import jax, jax.numpy as jnp
12
+ from jax.experimental import pallas as pl
13
+ from jax.experimental.pallas import triton as plgpu
14
+
15
+ # jnp.dot is stable across jax versions, contrary to pl.dot/plgpu.dot
16
+ _dot = functools.partial(jnp.dot, preferred_element_type=jnp.float32)
17
+
18
+ NEG = -0.7 * float(np.finfo(np.float32).max)
19
+ LOG2E = math.log2(math.e)
20
+
21
+
22
+ def _pow2_tile(d):
23
+ """Triton tiles are powers of two, and its dot needs at least 16 columns."""
24
+ return max(16, 1 << (int(d) - 1).bit_length())
25
+
26
+
27
+ def _chunks(d):
28
+ """d as power-of-two dot widths: narrow dots beat one wider tile of padding."""
29
+ out, off, rem = [], 0, int(d)
30
+ while rem > 0:
31
+ w = 1 << (rem.bit_length() - 1) if rem >= 16 else 16
32
+ out.append((off, w))
33
+ off, rem = off + w, rem - w
34
+ tile = _pow2_tile(d)
35
+ return tuple(out) if off < tile else ((0, tile),)
36
+
37
+
38
+ def _kernel(q_ref, k_ref, v_ref, bias_ref, kmask_ref, o_ref, *,
39
+ sm_scale, block_q, block_k, sq, d, chunks):
40
+ # q_ref [block_q, W]; k_ref/v_ref [Sk, W]; bias_ref [block_q, Sk]; kmask_ref [Sk],
41
+ # W being d split into power-of-two dot widths. Sq/Sk are NOT padded to block
42
+ # multiples; boundary blocks are handled with masked loads (offs < Sk) and a
43
+ # masked output store (offs_q < Sq) so no jnp.pad copy of q/k/v is needed.
44
+ sk = k_ref.shape[0]
45
+ # only a last chunk reaching past d needs a lane mask, and those lanes read zero
46
+ cols = [None if off + w <= d else (off + jnp.arange(w)) < d for off, w in chunks]
47
+ qs = [q_ref[:, pl.dslice(off, w)] if c is None else
48
+ plgpu.load(q_ref.at[:, pl.dslice(off, w)], mask=c[None, :], other=0.0)
49
+ for (off, w), c in zip(chunks, cols)]
50
+ m_i = jnp.full(block_q, -float('inf'), jnp.float32)
51
+ l_i = jnp.zeros(block_q, jnp.float32)
52
+ os = [jnp.zeros((block_q, w), jnp.float32) for _, w in chunks]
53
+
54
+ def body(j, carry):
55
+ o_prev, m_prev, l_prev = carry
56
+ start = j * block_k
57
+ kb = (start + jnp.arange(block_k)) < sk # [block_k] in-bounds keys
58
+ sl = pl.dslice(start, block_k)
59
+ qk = None
60
+ for qc, (off, w), c in zip(qs, chunks, cols):
61
+ m = kb[:, None] if c is None else kb[:, None] & c[None, :]
62
+ kc = plgpu.load(k_ref.at[sl, pl.dslice(off, w)], mask=m, other=0.0)
63
+ part = _dot(qc, kc.T) # [block_q, block_k]
64
+ qk = part if qk is None else qk + part
65
+ bias = plgpu.load(bias_ref.at[:, sl], mask=kb[None, :], other=0.0)
66
+ qk = (qk * sm_scale + bias) * LOG2E
67
+ km = plgpu.load(kmask_ref.at[sl], mask=kb, other=False) # OOB keys -> masked
68
+ qk = jnp.where(km[None, :], qk, NEG)
69
+ m_curr = jnp.max(qk, axis=-1)
70
+ m_next = jnp.maximum(m_prev, m_curr)
71
+ corr = jnp.exp2(m_prev - m_next)
72
+ s = jnp.exp2(qk - m_next[:, None])
73
+ l_next = corr * l_prev + s.sum(axis=-1)
74
+ o_next = []
75
+ for oc, (off, w), c in zip(o_prev, chunks, cols):
76
+ m = kb[:, None] if c is None else kb[:, None] & c[None, :]
77
+ vc = plgpu.load(v_ref.at[sl, pl.dslice(off, w)], mask=m, other=0.0)
78
+ o_next.append(corr[:, None] * oc + _dot(s.astype(vc.dtype), vc))
79
+ return o_next, m_next, l_next
80
+
81
+ os, m_i, l_i = jax.lax.fori_loop(0, pl.cdiv(sk, block_k), body, (os, m_i, l_i))
82
+ q_valid = (pl.program_id(0) * block_q + jnp.arange(block_q)) < sq
83
+ for oc, (off, w), c in zip(os, chunks, cols):
84
+ m = q_valid[:, None] if c is None else q_valid[:, None] & c[None, :]
85
+ plgpu.store(o_ref.at[:, pl.dslice(off, w)],
86
+ (oc / l_i[:, None]).astype(o_ref.dtype), mask=m)
87
+
88
+
89
+ @functools.partial(jax.jit, static_argnames=(
90
+ "sm_scale", "block_q", "block_k", "num_warps", "num_stages", "seq_major"))
91
+ def tri_flash(q, k, v, bias, kmask, *, sm_scale, block_q=64, block_k=64,
92
+ num_warps=4, num_stages=2, seq_major=False):
93
+ # q/k/v [N,H,Sq,D], or [N,Sq,H,D] when seq_major; bias [H,Sq,Sk]; kmask [N,Sk]
94
+ # bool. No padding: Sq/Sk need not be block multiples; the kernel masks the
95
+ # partial q-block (output) and the partial k-block (loads), so the big q/k/v
96
+ # tensors are never copied.
97
+ if seq_major:
98
+ N, Sq, H, D = q.shape
99
+ Sk = k.shape[1]
100
+ else:
101
+ N, H, Sq, D = q.shape
102
+ Sk = k.shape[2]
103
+ chunks = _chunks(D)
104
+ dk = chunks[-1][0] + chunks[-1][1] # the tile the chunks span
105
+ # grid (q_block, head, row): row N is innermost so the shared bias[h,q_block,:]
106
+ # stays hot in L2 while sweeping rows (the bias is re-read for every row).
107
+ grid = (pl.cdiv(Sq, block_q), H, N)
108
+ if seq_major:
109
+ bs_qo = pl.BlockSpec((None, block_q, None, dk), lambda i, h, j: (j, i, h, 0))
110
+ bs_kv = pl.BlockSpec((None, Sk, None, dk), lambda i, h, j: (j, 0, h, 0))
111
+ out_shape = jax.ShapeDtypeStruct((N, Sq, H, D), q.dtype)
112
+ else:
113
+ bs_qo = pl.BlockSpec((None, None, block_q, dk), lambda i, h, j: (j, h, i, 0))
114
+ bs_kv = pl.BlockSpec((None, None, Sk, dk), lambda i, h, j: (j, h, 0, 0))
115
+ out_shape = jax.ShapeDtypeStruct((N, H, Sq, D), q.dtype)
116
+ bs_bias = pl.BlockSpec((None, block_q, Sk), lambda i, h, j: (h, i, 0))
117
+ bs_mask = pl.BlockSpec((None, Sk), lambda i, h, j: (j, 0))
118
+ return pl.pallas_call(
119
+ functools.partial(_kernel, sm_scale=sm_scale, block_q=block_q,
120
+ block_k=block_k, sq=Sq, d=D, chunks=chunks),
121
+ grid=grid,
122
+ in_specs=[bs_qo, bs_kv, bs_kv, bs_bias, bs_mask],
123
+ out_specs=bs_qo,
124
+ out_shape=out_shape,
125
+ compiler_params=plgpu.CompilerParams(num_warps=num_warps, num_stages=num_stages),
126
+ name="tri_flash_fwd",
127
+ )(q, k, v, bias, kmask)
128
+
129
+
130
+ def pallas_attention(q, k, v, mask_bias, nonbatched_bias, scale, *, seq_major=False):
131
+ """AF2 attention via the Pallas flash kernel.
132
+
133
+ q/k/v heads-major [b, h, S, c], or seq-major [b, S, h, c] when seq_major.
134
+ mask_bias additive [b,1,1,S_kv] (~-1e9 = invalid); nonbatched_bias
135
+ [h,S_qo,S_kv] shared, or None.
136
+ Returns q's layout.
137
+ """
138
+ if seq_major:
139
+ b, sq, h, c = q.shape
140
+ sk = k.shape[1]
141
+ else:
142
+ b, h, sq, c = q.shape
143
+ sk = k.shape[2]
144
+ # The kernel reuses q's channel dim for v (one BlockSpec), so key_dim must equal value_dim
145
+ # True for AF2's attention
146
+ assert q.shape[-1] == v.shape[-1], (
147
+ f'tri_flash needs key_dim == value_dim, got {q.shape[-1]} vs {v.shape[-1]}')
148
+ # mask_bias is 0 or big_neg(dtype): -1e9, or -1e4 in float16. Template pointwise
149
+ # attention shares one row across the batch, which the row grid index cannot index.
150
+ kmask = jnp.broadcast_to(mask_bias[:, 0, 0, :] > -1e3, (b, sk)) # [b, S_kv] bool
151
+ bias = (jnp.zeros((h, sq, sk), q.dtype)
152
+ if nonbatched_bias is None else nonbatched_bias.astype(q.dtype))
153
+ # Key tile matches the head: a wider tile stages smem the kernel never reads.
154
+ block_k = _pow2_tile(min(64, c))
155
+ # a head dim that is not a power of two, or below 16, is tiled wider and masked
156
+ out = tri_flash(q, k, v, bias, kmask, sm_scale=float(scale),
157
+ block_q=64, block_k=block_k, seq_major=seq_major)
158
+ return out.astype(q.dtype)
159
+
160
+
161
+ def ref_attn(q, k, v, bias, kmask, sm_scale):
162
+ logits = jnp.einsum('nhqd,nhkd->nhqk', q, k) * sm_scale + bias[None]
163
+ logits = jnp.where(kmask[:, None, None, :], logits, NEG)
164
+ w = jax.nn.softmax(logits.astype(jnp.float32), axis=-1).astype(v.dtype)
165
+ return jnp.einsum('nhqk,nhkd->nhqd', w, v)
166
+
167
+
@@ -0,0 +1,213 @@
1
+ """Pallas/Triton fused gated dual projection for AF2 triangle-multiplication.
2
+
3
+ Inference (forward) only, no backward pass
4
+
5
+ Computes the masked, sigmoid-gated dual projection in one kernel so the [M, 2c]
6
+ projection/gate intermediates are never materialized. The triangle matmul itself
7
+ stays a native XLA batched GEMM.
8
+
9
+ out[m, p] = mask[m] * (x[m]·wp[:,p] + bp[p]) * sigmoid(x[m]·wg[:,p] + bg[p])
10
+ where x = layer-normed pair [M=N*N, K=c_z], wp/wg [K, P=2*c_i].
11
+ """
12
+ import functools
13
+ import jax, jax.numpy as jnp
14
+ from jax.experimental import pallas as pl
15
+ from jax.experimental.pallas import triton as plgpu
16
+
17
+ # jnp.dot is stable across jax versions, contrary to pl.dot/plgpu.dot
18
+ _dot = functools.partial(jnp.dot, preferred_element_type=jnp.float32)
19
+
20
+
21
+ def _gdp_kernel(x_ref, wp_ref, bp_ref, wg_ref, bg_ref, mask_ref, o_ref, *,
22
+ activation=jax.nn.sigmoid):
23
+ x = x_ref[...] # [BM, K] bf16
24
+ proj = _dot(x, wp_ref[...]) + bp_ref[...][None, :].astype(jnp.float32)
25
+ gate = _dot(x, wg_ref[...]) + bg_ref[...][None, :].astype(jnp.float32)
26
+ o = mask_ref[...][:, None].astype(jnp.float32) * proj * activation(gate)
27
+ o_ref[...] = o.astype(o_ref.dtype)
28
+
29
+
30
+ def _half(x, wp, bp, wg, bg, mask):
31
+ proj = _dot(x, wp) + bp[None, :].astype(jnp.float32)
32
+ gate = _dot(x, wg) + bg[None, :].astype(jnp.float32)
33
+ return mask[:, None].astype(jnp.float32) * proj * jax.nn.sigmoid(gate)
34
+
35
+
36
+ def _gdp_kernel_split_cm(x_ref, wpl_ref, bpl_ref, wgl_ref, bgl_ref,
37
+ wpr_ref, bpr_ref, wgr_ref, bgr_ref, mask_ref,
38
+ ol_ref, or_ref):
39
+ # _gdp_kernel_split, stored channel-major [ci, BM] for the triangle einsum.
40
+ x = x_ref[...]
41
+ m = mask_ref[...]
42
+ ol_ref[...] = _half(x, wpl_ref[...], bpl_ref[...], wgl_ref[...], bgl_ref[...],
43
+ m).T.astype(ol_ref.dtype)
44
+ or_ref[...] = _half(x, wpr_ref[...], bpr_ref[...], wgr_ref[...], bgr_ref[...],
45
+ m).T.astype(or_ref.dtype)
46
+
47
+
48
+ def _gdp_kernel_split(x_ref, wpl_ref, bpl_ref, wgl_ref, bgl_ref,
49
+ wpr_ref, bpr_ref, wgr_ref, bgr_ref, mask_ref,
50
+ ol_ref, or_ref):
51
+ # Computes the left [BM, ci] and right [BM, ci] gated projections as two
52
+ # separate dots (weights pre-split on the host) and writes them to two
53
+ # contiguous outputs -- so the triangle einsum consumes them directly with no
54
+ # post-kernel strided slice/copy of the combined [N,N,2ci] tensor.
55
+ x = x_ref[...]
56
+ m = mask_ref[...]
57
+ ol_ref[...] = _half(x, wpl_ref[...], bpl_ref[...], wgl_ref[...], bgl_ref[...],
58
+ m).astype(ol_ref.dtype)
59
+ or_ref[...] = _half(x, wpr_ref[...], bpr_ref[...], wgr_ref[...], bgr_ref[...],
60
+ m).astype(or_ref.dtype)
61
+
62
+
63
+ @functools.partial(jax.jit,
64
+ static_argnames=("block_m", "split", "channel_major", "activation"))
65
+ def gated_dual_proj(x, wp, bp, wg, bg, mask, *, block_m=64, split=False,
66
+ channel_major=False, activation=jax.nn.sigmoid):
67
+ # x [M,K]; wp/wg [K,P]; bp/bg [P]; mask [M].
68
+ # split=False -> gated [M,P]; split=True -> (left [M,P/2], right [M,P/2]),
69
+ # the two halves written contiguously so the caller needs no slice.
70
+ M, K = x.shape
71
+ P = wp.shape[1]
72
+ Mp = -(-M // block_m) * block_m
73
+ x = jnp.pad(x, [(0, Mp - M), (0, 0)])
74
+ mask = jnp.pad(mask, [(0, Mp - M)])
75
+ in_specs = [
76
+ pl.BlockSpec((block_m, K), lambda i: (i, 0)),
77
+ pl.BlockSpec((K, P), lambda i: (0, 0)),
78
+ pl.BlockSpec((P,), lambda i: (0,)),
79
+ pl.BlockSpec((K, P), lambda i: (0, 0)),
80
+ pl.BlockSpec((P,), lambda i: (0,)),
81
+ pl.BlockSpec((block_m,), lambda i: (i,)),
82
+ ]
83
+ cp = plgpu.CompilerParams(num_warps=4, num_stages=1)
84
+ if split:
85
+ ci = P // 2
86
+ wpl, wpr = wp[:, :ci], wp[:, ci:]
87
+ wgl, wgr = wg[:, :ci], wg[:, ci:]
88
+ bpl, bpr = bp[:ci], bp[ci:]
89
+ bgl, bgr = bg[:ci], bg[ci:]
90
+ wspec = pl.BlockSpec((K, ci), lambda i: (0, 0))
91
+ bspec = pl.BlockSpec((ci,), lambda i: (0,))
92
+ split_in = [in_specs[0], wspec, bspec, wspec, bspec, wspec, bspec,
93
+ wspec, bspec, in_specs[5]]
94
+ if channel_major:
95
+ bs = pl.BlockSpec((ci, block_m), lambda i: (0, i))
96
+ left, right = pl.pallas_call(
97
+ _gdp_kernel_split_cm,
98
+ grid=(Mp // block_m,), in_specs=split_in, out_specs=[bs, bs],
99
+ out_shape=[jax.ShapeDtypeStruct((ci, Mp), x.dtype),
100
+ jax.ShapeDtypeStruct((ci, Mp), x.dtype)],
101
+ compiler_params=cp, name="gated_dual_proj_split_cm",
102
+ )(x, wpl, bpl, wgl, bgl, wpr, bpr, wgr, bgr, mask)
103
+ return left[:, :M], right[:, :M]
104
+ bs = pl.BlockSpec((block_m, ci), lambda i: (i, 0))
105
+ left, right = pl.pallas_call(
106
+ _gdp_kernel_split,
107
+ grid=(Mp // block_m,), in_specs=split_in, out_specs=[bs, bs],
108
+ out_shape=[jax.ShapeDtypeStruct((Mp, ci), x.dtype),
109
+ jax.ShapeDtypeStruct((Mp, ci), x.dtype)],
110
+ compiler_params=cp, name="gated_dual_proj_split",
111
+ )(x, wpl, bpl, wgl, bgl, wpr, bpr, wgr, bgr, mask)
112
+ return left[:M], right[:M]
113
+ out = pl.pallas_call(
114
+ functools.partial(_gdp_kernel, activation=activation),
115
+ grid=(Mp // block_m,), in_specs=in_specs,
116
+ out_specs=pl.BlockSpec((block_m, P), lambda i: (i, 0)),
117
+ out_shape=jax.ShapeDtypeStruct((Mp, P), x.dtype),
118
+ compiler_params=cp, name="gated_dual_proj",
119
+ )(x, wp, bp, wg, bg, mask)
120
+ return out[:M]
121
+
122
+
123
+ def ref_gdp(x, wp, bp, wg, bg, mask):
124
+ proj = x @ wp + bp
125
+ gate = x @ wg + bg
126
+ return (mask[:, None] * proj * jax.nn.sigmoid(gate))
127
+
128
+
129
+ # Fused LayerNorm: bf16 in/out, fp32 internal. One kernel replaces the
130
+ # bf16->fp32 upcast + layernorm + fp32->bf16 downcast (kills the convert ops
131
+ # and never materializes the fp32 tensor), while keeping fp32 accuracy.
132
+ def _ln_kernel(x_ref, scale_ref, offset_ref, o_ref, *, eps, m, c, block_m):
133
+ bc = x_ref.shape[-1]
134
+ rows = (pl.program_id(0) * block_m + jnp.arange(block_m)) < m # [block_m]
135
+ if bc == c:
136
+ x = plgpu.load(x_ref, mask=rows[:, None], other=0.0).astype(jnp.float32)
137
+ mean = jnp.mean(x, axis=-1, keepdims=True)
138
+ d = x - mean
139
+ var = jnp.mean(d * d, axis=-1, keepdims=True)
140
+ scale, offset = scale_ref[...], offset_ref[...]
141
+ valid = rows[:, None]
142
+ else:
143
+ # c is not a power of two: tile to the next one, mask the lanes past c
144
+ cols = jnp.arange(bc) < c
145
+ valid = rows[:, None] & cols[None, :]
146
+ x = plgpu.load(x_ref, mask=valid, other=0.0).astype(jnp.float32)
147
+ mean = jnp.sum(x, axis=-1, keepdims=True) / c
148
+ d = jnp.where(cols[None, :], x - mean, 0.0)
149
+ var = jnp.sum(d * d, axis=-1, keepdims=True) / c
150
+ scale = plgpu.load(scale_ref, mask=cols, other=0.0)
151
+ offset = plgpu.load(offset_ref, mask=cols, other=0.0)
152
+ y = d * jax.lax.rsqrt(var + eps)
153
+ y = y * scale[None, :].astype(jnp.float32) + offset[None, :].astype(jnp.float32)
154
+ plgpu.store(o_ref, y.astype(o_ref.dtype), mask=valid)
155
+
156
+
157
+ # Triton keeps the tile in registers, so a wider one only spills and compiles slower
158
+ _TILE_BYTES = 32 * 1024
159
+
160
+
161
+ @functools.partial(jax.jit, static_argnames=("eps", "block_m"))
162
+ def pallas_layer_norm(x, scale, offset, *, eps=1e-5, block_m=64):
163
+ # x [..., c]; normalize over last axis; scale/offset [c]. Returns x's shape/dtype.
164
+ c = x.shape[-1]
165
+ bc = max(16, 1 << (c - 1).bit_length()) # Triton tiles are powers of two
166
+ xf = x.reshape(-1, c)
167
+ m = xf.shape[0]
168
+ rows = max(1, _TILE_BYTES // (bc * x.dtype.itemsize))
169
+ block_m = min(block_m, 1 << (rows.bit_length() - 1))
170
+ out = pl.pallas_call(
171
+ functools.partial(_ln_kernel, eps=eps, m=m, c=c, block_m=block_m),
172
+ grid=(pl.cdiv(m, block_m),),
173
+ in_specs=[pl.BlockSpec((block_m, bc), lambda i: (i, 0)),
174
+ pl.BlockSpec((bc,), lambda i: (0,)),
175
+ pl.BlockSpec((bc,), lambda i: (0,))],
176
+ out_specs=pl.BlockSpec((block_m, bc), lambda i: (i, 0)),
177
+ out_shape=jax.ShapeDtypeStruct((m, c), x.dtype),
178
+ compiler_params=plgpu.CompilerParams(num_warps=4, num_stages=2),
179
+ name="pallas_layer_norm",
180
+ )(xf, scale, offset)
181
+ return out.reshape(x.shape)
182
+
183
+
184
+ def ref_layer_norm(x, scale, offset, eps=1e-5):
185
+ xf = x.astype(jnp.float32)
186
+ mean = jnp.mean(xf, -1, keepdims=True)
187
+ var = jnp.mean((xf - mean) ** 2, -1, keepdims=True)
188
+ y = (xf - mean) * jax.lax.rsqrt(var + eps) * scale + offset
189
+ return y.astype(x.dtype)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ import time
194
+ key = jax.random.PRNGKey(0)
195
+ M, K, P = 1000 * 1000, 128, 256
196
+ x = (jax.random.normal(key, (M, K)) * 0.3).astype(jnp.bfloat16)
197
+ wp = (jax.random.normal(key, (K, P)) * 0.1).astype(jnp.bfloat16)
198
+ wg = (jax.random.normal(key, (K, P)) * 0.1).astype(jnp.bfloat16)
199
+ bp = (jax.random.normal(key, (P,)) * 0.1).astype(jnp.bfloat16)
200
+ bg = (jax.random.normal(key, (P,)) * 0.1).astype(jnp.bfloat16)
201
+ mask = (jax.random.uniform(key, (M,)) > 0.1).astype(jnp.bfloat16)
202
+ o = gated_dual_proj(x, wp, bp, wg, bg, mask)
203
+ r = ref_gdp(x.astype(jnp.float32), wp.astype(jnp.float32), bp.astype(jnp.float32),
204
+ wg.astype(jnp.float32), bg.astype(jnp.float32), mask.astype(jnp.float32))
205
+ err = float(jnp.max(jnp.abs(o.astype(jnp.float32) - r)))
206
+ print(f"gated_dual_proj bf16 err={err:.3e}")
207
+ f = jax.jit(gated_dual_proj)
208
+ o = f(x, wp, bp, wg, bg, mask); o.block_until_ready()
209
+ t = time.time()
210
+ for _ in range(50): o = f(x, wp, bp, wg, bg, mask)
211
+ o.block_until_ready()
212
+ print(f"per-call {(time.time()-t)/50*1000:.2f} ms")
213
+
@@ -0,0 +1,169 @@
1
+ """sm_70/75 CUDA kernels (colabfold-legacy-kernels) used instead of Pallas."""
2
+ import ctypes
3
+ import functools
4
+ import os
5
+
6
+ import jax
7
+ import jax.numpy as jnp
8
+ import numpy as np
9
+
10
+ # Head dims each kernel instantiates: mma.sync (sm_75+) and wmma (sm_70).
11
+ _HEAD_DIMS_MMA = (8, 16, 32, 64)
12
+ _HEAD_DIMS_WMMA = (8, 16, 32, 64)
13
+
14
+ _LOADED = {}
15
+
16
+
17
+ def _package_path(kernel, cc):
18
+ """Give the library path from colabfold_legacy_kernels, or None."""
19
+ try:
20
+ import colabfold_legacy_kernels as clk
21
+ except ImportError:
22
+ return None
23
+ try:
24
+ return clk.library_path(kernel, cc)
25
+ except (FileNotFoundError, KeyError):
26
+ return None
27
+
28
+
29
+ def _load(kernel, cc, symbols):
30
+ """dlopen the library for one kernel and register its FFI targets."""
31
+ key = (kernel, cc, tuple(symbols))
32
+ if key in _LOADED:
33
+ return _LOADED[key]
34
+ path = _package_path(kernel, cc)
35
+ if not path or not os.path.exists(path):
36
+ _LOADED[key] = False
37
+ return False
38
+ lib = ctypes.cdll.LoadLibrary(path)
39
+ for sym in symbols:
40
+ jax.ffi.register_ffi_target(
41
+ sym, jax.ffi.pycapsule(getattr(lib, sym)), platform="CUDA")
42
+ _LOADED[key] = True
43
+ return True
44
+
45
+
46
+ def _attn_symbol(cc):
47
+ """sm_75+ gets the CUTLASS kernel; sm_70 the wmma one."""
48
+ return "VoltaMma" if cc >= 75 else "VoltaWmma"
49
+
50
+
51
+ def available(cc):
52
+ """True if the attention library for this device loads."""
53
+ return _load("attention", cc, (_attn_symbol(cc),))
54
+
55
+
56
+ def _native_dims(cc):
57
+ return _HEAD_DIMS_MMA if cc >= 75 else _HEAD_DIMS_WMMA
58
+
59
+
60
+ def _pad_target(head_dim, cc):
61
+ """Smallest supported head dim >= head_dim, or None."""
62
+ for d in sorted(_native_dims(cc)):
63
+ if d >= head_dim:
64
+ return d
65
+ return None
66
+
67
+
68
+ def supports(head_dim, cc):
69
+ if head_dim in _native_dims(cc):
70
+ return True
71
+ # wmma: zero-pad to the next supported head dim (exact, beats XLA on V100).
72
+ if cc < 75:
73
+ return _pad_target(head_dim, cc) is not None
74
+ return False
75
+
76
+
77
+ def ops_available(cc):
78
+ """LayerNorm is Volta-capable; the CUTLASS gdp needs sm_75+."""
79
+ syms = ("VoltaLayerNorm", "VoltaGdp") if cc >= 75 else ("VoltaLayerNorm",)
80
+ ok = _load("layer_norm", cc, syms)
81
+ if ok and cc < 75:
82
+ ok = _load("gated_dual_proj", cc, ("VoltaGdpWmma",))
83
+ return ok
84
+
85
+
86
+ @functools.partial(jax.jit, static_argnames=("sym", "sm_scale", "block_q", "block_k"))
87
+ def _attn_call(q, k, v, bias, kmask, *, sym, sm_scale, block_q, block_k):
88
+ n, h, sq, d = q.shape
89
+ # sequential: the template stack vmaps over templates, and the handlers take a fixed rank.
90
+ return jax.ffi.ffi_call(sym, jax.ShapeDtypeStruct((n, h, sq, d), jnp.float16),
91
+ vmap_method="sequential")(
92
+ q, k, v, bias, kmask,
93
+ scale=np.float32(sm_scale),
94
+ block_q=np.int64(block_q), block_k=np.int64(block_k))
95
+
96
+
97
+ def volta_attention(q, k, v, mask_bias, nonbatched_bias, scale, cc,
98
+ block_q=64, block_k=32):
99
+ """Drop-in for tri_flash.pallas_attention. q/k/v [b,h,S,c] float16."""
100
+ in_dtype = q.dtype
101
+ b, h, sq, c = q.shape
102
+ sk = k.shape[2]
103
+ assert c == v.shape[-1], "kernel needs key_dim == value_dim"
104
+ pad_to = None
105
+ if c not in _native_dims(cc):
106
+ pad_to = _pad_target(c, cc)
107
+ assert pad_to is not None, f"unsupported head_dim {c}"
108
+ pad = [(0, 0)] * 3 + [(0, pad_to - c)]
109
+ q, k, v = (jnp.pad(t.astype(jnp.float16), pad) for t in (q, k, v))
110
+ if cc >= 75 and (pad_to or c) == 64 and (block_q, block_k) == (64, 32):
111
+ block_k = 64 # the mma kernel has no (64, 64, 32) instantiation
112
+ # Template pointwise attention shares one mask row across the batch; the kernel
113
+ # indexes the mask per row, so broadcast instead of reading past its end.
114
+ kmask = jnp.broadcast_to(mask_bias[:, 0, 0, :] > -1e3, (b, sk)).astype(jnp.uint8)
115
+ bias = (jnp.zeros((h, sq, sk), jnp.float16) if nonbatched_bias is None
116
+ else nonbatched_bias.astype(jnp.float16))
117
+ out = _attn_call(q.astype(jnp.float16), k.astype(jnp.float16),
118
+ v.astype(jnp.float16), bias, kmask,
119
+ sym=_attn_symbol(cc), sm_scale=float(scale),
120
+ block_q=block_q, block_k=block_k)
121
+ if pad_to is not None:
122
+ out = out[..., :c]
123
+ return out.astype(in_dtype)
124
+
125
+
126
+ @functools.partial(jax.jit, static_argnames=("eps",))
127
+ def _ln_call(x, scale, offset, *, eps):
128
+ m, c = x.shape
129
+ return jax.ffi.ffi_call(
130
+ "VoltaLayerNorm", jax.ShapeDtypeStruct((m, c), jnp.float16),
131
+ vmap_method="sequential")(
132
+ x, scale, offset, eps=np.float32(eps))
133
+
134
+
135
+ def volta_layer_norm(x, scale, offset, *, eps=1e-5):
136
+ """Drop-in for tri_mul.pallas_layer_norm. Normalises the last axis."""
137
+ c = x.shape[-1]
138
+ out = _ln_call(x.reshape(-1, c).astype(jnp.float16),
139
+ scale.astype(jnp.float32), offset.astype(jnp.float32), eps=eps)
140
+ return out.reshape(x.shape).astype(x.dtype)
141
+
142
+
143
+ @functools.partial(jax.jit, static_argnames=("sym",))
144
+ def _gdp_call(x, wp, bp, wg, bg, mask, *, sym):
145
+ m = x.shape[0]
146
+ n = wp.shape[1]
147
+ return jax.ffi.ffi_call(sym, jax.ShapeDtypeStruct((m, n), jnp.float16),
148
+ vmap_method="sequential")(
149
+ x, wp, bp, wg, bg, mask)
150
+
151
+
152
+ def volta_gated_dual_proj(x, wp, bp, wg, bg, mask, cc, *, split=True,
153
+ channel_major=False):
154
+ """Drop-in for tri_mul.gated_dual_proj. x [M,K]; wp/wg [K,P]; mask [M]."""
155
+ sym = "VoltaGdp" if cc >= 75 else "VoltaGdpWmma"
156
+ f16 = lambda t: t.astype(jnp.float16)
157
+ if not split:
158
+ return _gdp_call(f16(x), f16(wp), f16(bp), f16(wg), f16(bg),
159
+ f16(mask), sym=sym).astype(x.dtype)
160
+ ci = wp.shape[1] // 2
161
+ halves = []
162
+ for lo, hi in ((0, ci), (ci, 2 * ci)):
163
+ halves.append(_gdp_call(f16(x), f16(wp[:, lo:hi]), f16(bp[lo:hi]),
164
+ f16(wg[:, lo:hi]), f16(bg[lo:hi]), f16(mask),
165
+ sym=sym).astype(x.dtype))
166
+ if channel_major:
167
+ # XLA would transpose row-major halves for the einsum anyway.
168
+ halves = [h.T for h in halves]
169
+ return halves[0], halves[1]
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: colabfold-kernels
3
+ Version: 0.1.0
4
+ Summary: Fast fused kernels for ColabFold
5
+ Author-email: Milot Mirdita <milot@mirdita.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mirditalab/colabfold-kernels
8
+ Project-URL: Issues, https://github.com/mirditalab/colabfold-kernels/issues
9
+ Keywords: alphafold,colabfold,pallas,triton,jax,attention,bioinformatics
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # colabfold-kernels
20
+
21
+ Fast fused kernels for ColabFold, written in
22
+ [Pallas](https://docs.jax.dev/en/latest/pallas/index.html) with native CUDA-fallback
23
+ for Volta- and Turing-generation GPUs.
24
+
25
+ ```python
26
+ import jax.numpy as jnp
27
+ from colabfold_kernels import attention, dispatch
28
+
29
+ kernel = attention(dispatch(), jnp.bfloat16, key_dim=96, value_dim=96, layout="seq")
30
+ if kernel is not None:
31
+ out = kernel(q, k, v, mask_bias, nonbatched_bias, scale)
32
+ ```
33
+
34
+ `attention`, `layer_norm` and `gated_dual_proj` each return `None` if not supported.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/colabfold_kernels/__init__.py
5
+ src/colabfold_kernels/device.py
6
+ src/colabfold_kernels/fused_ops.py
7
+ src/colabfold_kernels/tri_flash.py
8
+ src/colabfold_kernels/tri_mul.py
9
+ src/colabfold_kernels/volta.py
10
+ src/colabfold_kernels.egg-info/PKG-INFO
11
+ src/colabfold_kernels.egg-info/SOURCES.txt
12
+ src/colabfold_kernels.egg-info/dependency_links.txt
13
+ src/colabfold_kernels.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ colabfold_kernels