JAX: Single-GPU Attention with TransformerEngine
This document walks through replacing a plain JAX implementation of BSHD
attention with TransformerEngine’s fused DotProductAttention.
The example uses
grouped-query attention (GQA) and
sliding-window attention (SWA).
← Back to the Attention overview
1. Baseline: native JAX BSHD GQA + SWA
Start with the imports shared by the native JAX and Transformer Engine implementations.
from typing import Optional, Tuple
import jax
import jax.numpy as jnp
import numpy as np
from flax import linen as nn
import quickstart_jax_utils as utils
from transformer_engine.jax.attention import SequenceDescriptor
from transformer_engine.jax.flax import DotProductAttention
Next, create reproducible BSHD Q/K/V tensors and the sequence descriptor.
The SequenceDescriptor supplies TE with sequence lengths and, for packed
inputs, segment boundaries and padding metadata.
batch, seq, num_query_heads, num_kv_heads, head_dim = 2, 4096, 128, 8, 128
window_size = (128, 0)
dtype = jnp.bfloat16
timing_iters = 20
warmup_iters = 10
def create_qkv_inputs(
*,
seed: int,
kv_heads: int = num_kv_heads,
qk_head_dim: int = head_dim,
v_head_dim: int = head_dim,
):
"""Create separate BSHD query, key, value tensors and an output gradient."""
q_key, k_key, v_key, dout_key = jax.random.split(jax.random.PRNGKey(seed), 4)
q = jax.random.normal(q_key, (batch, seq, num_query_heads, qk_head_dim)).astype(dtype)
k = jax.random.normal(k_key, (batch, seq, kv_heads, qk_head_dim)).astype(dtype)
v = jax.random.normal(v_key, (batch, seq, kv_heads, v_head_dim)).astype(dtype)
dout = jax.random.normal(dout_key, (batch, seq, num_query_heads, v_head_dim)).astype(dtype)
return q, k, v, dout
def create_full_sequence_descriptor():
"""Describe a BSHD batch with no padding."""
seqlens = jnp.full((batch,), seq, dtype=jnp.int32)
return SequenceDescriptor.from_seqlens(seqlens)
q, k, v, dout = create_qkv_inputs(seed=2026)
qkv = (q, k, v)
sequence_descriptor = create_full_sequence_descriptor()
The native JAX baseline repeats K/V heads for GQA and applies the causal sliding-window mask explicitly.
def _repeat_kv_for_gqa(x, query_heads):
"""Repeat each KV head across its group of query heads."""
repeats = query_heads // x.shape[2]
return jnp.repeat(x, repeats, axis=2)
def _make_causal_swa_mask(q_len, kv_len, window: Optional[Tuple[int, int]]):
"""Create a boolean causal mask, optionally restricted to an SWA window."""
q_pos = jnp.arange(q_len)[:, None]
kv_pos = jnp.arange(kv_len)[None, :]
if window is None:
return kv_pos <= q_pos
left, right = window
allowed = kv_pos <= q_pos + right
if left >= 0:
allowed = allowed & (kv_pos >= q_pos - left)
return allowed
class FlaxNativeGQAAttention(nn.Module):
"""Plain JAX/Flax GQA used as the bf16 baseline."""
window_size: Optional[Tuple[int, int]] = None
@nn.compact
def __call__(self, qkv_tensors):
query, key, value = qkv_tensors
key = _repeat_kv_for_gqa(key, query.shape[2])
value = _repeat_kv_for_gqa(value, query.shape[2])
scale = query.shape[-1] ** -0.5
scores = jnp.einsum(
"bqhd,bkhd->bhqk",
query.astype(jnp.float32),
key.astype(jnp.float32),
)
scores *= scale
mask = _make_causal_swa_mask(query.shape[1], key.shape[1], self.window_size)
scores = jnp.where(mask[None, None, :, :], scores, jnp.finfo(jnp.float32).min)
probs = jax.nn.softmax(scores, axis=-1)
out = jnp.einsum("bhqk,bkhd->bqhd", probs, value.astype(jnp.float32))
return out.astype(query.dtype)
baseline = FlaxNativeGQAAttention(window_size=window_size)
baseline_vars = baseline.init(jax.random.PRNGKey(2026), qkv)
2. Transformer Engine DotProductAttention
The Transformer Engine version keeps the same separate BSHD inputs. The important arguments are
num_gqa_groups for GQA, attn_mask_type="causal" for autoregressive
attention, and window_size for SWA.
class TEDotProductAttention(nn.Module):
"""Thin Flax wrapper around TE's DotProductAttention."""
num_query_heads: int
num_kv_heads: int
qk_head_dim: int = head_dim
attn_mask_type: str = "causal"
qkv_layout: str = "bshd_bshd_bshd"
window_size: Optional[Tuple[int, int]] = None
@nn.compact
def __call__(
self,
qkv_tensors,
sequence_descriptor: Optional[SequenceDescriptor] = None,
*,
deterministic: bool = False,
):
query, key, value = qkv_tensors
return DotProductAttention(
head_dim=self.qk_head_dim,
num_attention_heads=self.num_query_heads,
num_gqa_groups=self.num_kv_heads,
attn_mask_type=self.attn_mask_type,
qkv_layout=self.qkv_layout,
attention_dropout=0.0,
transpose_batch_sequence=False,
window_size=self.window_size,
)(
query,
key,
value,
sequence_descriptor=sequence_descriptor,
deterministic=deterministic,
)
te_model = TEDotProductAttention(
num_query_heads=num_query_heads,
num_kv_heads=num_kv_heads,
window_size=window_size,
)
te_vars = te_model.init(
jax.random.PRNGKey(2026),
qkv,
sequence_descriptor=sequence_descriptor,
deterministic=False,
)
3. Single-GPU performance
speedometer runs a JIT-compiled forward+backward loop with warmup for both
implementations.
def run_single_gpu_bench():
forward_kwargs = {
"sequence_descriptor": sequence_descriptor,
"deterministic": False,
}
print("Native JAX bf16 GQA + SWA:")
utils.speedometer(
model_apply_fn=baseline.apply,
variables=baseline_vars,
input=qkv,
output_grad=dout,
timing_iters=timing_iters,
warmup_iters=warmup_iters,
)
print("\nTE DotProductAttention GQA + SWA:")
utils.speedometer(
model_apply_fn=te_model.apply,
variables=te_vars,
input=qkv,
output_grad=dout,
forward_kwargs=forward_kwargs,
timing_iters=timing_iters,
warmup_iters=warmup_iters,
)
Native JAX bf16 GQA + SWA:
Mean time: 5.109810829162598 ms
TE DotProductAttention GQA + SWA:
Mean time: 0.09856224060058594 ms
On a single GB200, this run is roughly 52x faster for the fwd+bwd of this
BSHD GQA + SWA example. This compares TE DotProductAttention against the
native JAX baseline above, which materializes attention scores with XLA ops; it
is not a comparison against jax.nn.dot_product_attention(...,
implementation="cudnn").
4. DeepSeek-style MLA head dimensions
This example covers the attention-kernel interface used after
DeepSeek-style MLA projections, not the
latent projection layers themselves. At this point, separate Q, K, and V
tensors can use different per-head dimensions for Q/K and V. Keep
qkv_layout="bshd_bshd_bshd" so TE can see the Q/K head dimension and the V
head dimension separately.
mla_head_dim_qk, mla_head_dim_v = 128, 64
mla_q, mla_k, mla_v, mla_dout = create_qkv_inputs(
seed=2027,
kv_heads=num_kv_heads,
qk_head_dim=mla_head_dim_qk,
v_head_dim=mla_head_dim_v,
)
mla_qkv = (mla_q, mla_k, mla_v)
mla_model = TEDotProductAttention(
num_query_heads=num_query_heads,
num_kv_heads=num_kv_heads,
qk_head_dim=mla_head_dim_qk,
window_size=None,
)
mla_vars = mla_model.init(
jax.random.PRNGKey(4),
mla_qkv,
sequence_descriptor=sequence_descriptor,
deterministic=False,
)
def run_mla_variant():
out = mla_model.apply(
mla_vars,
mla_qkv,
sequence_descriptor=sequence_descriptor,
deterministic=False,
)
loss, grads = run_forward_backward(mla_model, mla_vars, mla_qkv, mla_dout, sequence_descriptor)
jax.block_until_ready((out, loss, grads))
print(
"TE DeepSeek-style MLA head dimensions: "
f"q/k head dim={mla_head_dim_qk}, v head dim={mla_head_dim_v}"
)
print(f"Output shape={tuple(out.shape)}, dtype={out.dtype}")
print(f"Grad shapes={[tuple(grad.shape) for grad in grads]}")
TE DeepSeek-style MLA head dimensions: q/k head dim=128, v head dim=64
Output shape=(2, 4096, 128, 64), dtype=bfloat16
Grad shapes=[(2, 4096, 128, 128), (2, 4096, 8, 128), (2, 4096, 8, 64)]
Other attention knobs
The examples above represent a subset of attention features. Other
DotProductAttention features can be enabled through the same module
arguments as below:
Dropout: set
attention_dropout > 0, call withdeterministic=False, and pass a FlaxdropoutRNG toapply.Bias: pass
biasand setattn_bias_typewhen the selected fused kernel supports that bias mode.Sink attention: use
softmax_type="off_by_one"or"learnable".Score scaling: set
scale_factorto override the default1 / sqrt(head_dim)scaling.Determinism: set
NVTE_ALLOW_NONDETERMINISTIC_ALGO=0before launching the process if deterministic fused kernels are required.Score modification (experimental): use
score_modfor a FlexAttention-style cuDNN frontend callback, with runtime operands inscore_mod_tensorsand optional custom backward logic inscore_mod_bprop. This path requires fused attention and currently cannot be combined with masks, bias, dropout, SWA, CP, or packed/ragged sequence metadata.
Next steps
Context-parallel attention: packed THD attention over a context-parallel mesh.