# SPDX-FileCopyrightText: Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""Rotary position embedding (RoPE) modules and primitives.
Overview
--------
Rotary Position Embedding (RoPE) encodes token position by *rotating* query
and key vectors before the attention dot-product. Because the dot-product of
a rotated query and a rotated key depends only on the *relative* angle between
them, RoPE gives attention position-awareness without adding any learned
parameters. No positional vectors are added to the token features — instead, the
position is woven into the rotation of each head's Q/K projections.
This module exposes two levels of API:
**Shared table-provider modules** (owners of the cos/sin tables):
- :class:`RotaryEmbedding2DTables` — owns axial 2D RoPE cos/sin tables for an
:math:`h \times w` token grid, in explicit ``(h, w, head_dim)`` layout.
- :class:`RotaryEmbedding1DTables` — owns standard 1D sequence RoPE cos/sin
tables of shape ``(max_seq_len, head_dim)``.
A provider holds *no* projections and applies *no* rotation itself: its
``forward`` simply returns the ``(cos, sin)`` tables. The intended pattern is
that a top-level, multi-block model constructs a *single* provider and passes
the returned tables into every attention block's ``forward`` (which rotates
Q/K with the functional :func:`apply_rotary_pos_emb`), so the tables are
built, stored, and — under domain parallelism — sharded exactly once instead
of once per block. See
:class:`~physicsnemo.nn.module.dit_layers.RopeNatten2DSelfAttention` and
:class:`~physicsnemo.models.dit.DiT` for a reference wiring.
**Low-level functional helpers** (:func:`build_axial_rope_cos_sin_2d`,
:func:`build_rope_cos_sin_1d`, :func:`apply_rotary_pos_emb`):
Used internally by the providers above and by attention implementations that
need direct control over the table layout (e.g. NATTEN windowed attention,
which keeps explicit spatial ``(h, w)`` dimensions, or domain-parallel
paths that shard the tables across GPUs).
Choosing the right API
----------------------
* Building a multi-block transformer (2D grid or 1D sequence)? Construct one
:class:`RotaryEmbedding2DTables` / :class:`RotaryEmbedding1DTables` at the top
level of the model and share its tables across every block, applying them
with :func:`apply_rotary_pos_emb`.
* Implementing a single attention block or need full control over the table
layout? Call the functional helpers directly and apply them with
:func:`apply_rotary_pos_emb`.
Math (axial 2D RoPE)
--------------------
``head_dim`` is split in half: the first half rotates by row index, the second
by column index. Each axis has ``head_dim/4`` rotation pairs sharing a frequency
:math:`\theta_k = \text{base}^{-2k/(head\_dim/2)}` for
:math:`k = 0 \ldots head\_dim/4 - 1`. For an adjacent channel pair
:math:`(x_a, x_b)` at angle :math:`\phi`, the rotation is
:math:`(x_a \cos\phi - x_b \sin\phi,\ x_a \sin\phi + x_b \cos\phi)`.
"""
from __future__ import annotations
from typing import Optional, Tuple
import torch
from jaxtyping import Float
from physicsnemo.core import Module
[docs]
def build_axial_rope_cos_sin_2d(
h: int,
w: int,
head_dim: int,
theta: float = 10000.0,
device: Optional[torch.device] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
r"""Precompute axial 2D RoPE cos/sin tables for an :math:`h \times w` token grid.
The first ``head_dim/2`` channels are rotated by the row index, the last
``head_dim/2`` by the column index. Within each axis-half, frequency
:math:`\theta_k = \text{theta}^{-2k/(head\_dim/2)}` drives the adjacent
channel pair ``(2k, 2k+1)``.
Parameters
----------
h : int
Token grid height.
w : int
Token grid width.
head_dim : int
Per-head channel dimension. Must be divisible by 4 (half per axis, then
adjacent pairs within each half).
theta : float, optional, default=10000.0
Base used for the RoPE frequency schedule.
device : torch.device, optional
Device for the generated tables.
Returns
-------
Tuple[torch.Tensor, torch.Tensor]
``(cos, sin)``, each of shape :math:`(h, w, head\_dim)` in fp32.
"""
if head_dim % 4 != 0:
raise ValueError(
f"head_dim={head_dim} must be divisible by 4 for axial 2D RoPE "
f"(half per axis, then adjacent pairs within each half)."
)
half = head_dim // 2 # channels per axis
# Frequencies for one axis: head_dim/4 unique values, each shared across an
# adjacent channel pair via repeat_interleave below.
k = torch.arange(0, half, 2, dtype=torch.float32, device=device)
freqs = theta ** (-k / half) # (head_dim/4,)
row_idx = torch.arange(h, dtype=torch.float32, device=device)
row_ang = row_idx[:, None] * freqs[None, :] # (h, head_dim/4)
col_idx = torch.arange(w, dtype=torch.float32, device=device)
col_ang = col_idx[:, None] * freqs[None, :] # (w, head_dim/4)
# repeat_interleave(2) sends [a, b, c, ...] -> [a, a, b, b, c, c, ...] so that
# the adjacent channel pair (2k, 2k+1) shares frequency theta_k.
cos_row = row_ang.cos().repeat_interleave(2, dim=-1) # (h, half)
sin_row = row_ang.sin().repeat_interleave(2, dim=-1)
cos_col = col_ang.cos().repeat_interleave(2, dim=-1) # (w, half)
sin_col = col_ang.sin().repeat_interleave(2, dim=-1)
cos = torch.cat(
[
cos_row[:, None, :].expand(h, w, half),
cos_col[None, :, :].expand(h, w, half),
],
dim=-1,
) # (h, w, head_dim)
sin = torch.cat(
[
sin_row[:, None, :].expand(h, w, half),
sin_col[None, :, :].expand(h, w, half),
],
dim=-1,
)
return cos.contiguous(), sin.contiguous()
[docs]
def build_rope_cos_sin_1d(
seq_len: int,
head_dim: int,
theta: float = 10000.0,
device: Optional[torch.device] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
r"""Precompute 1D RoPE cos/sin tables for a length-``seq_len`` sequence.
The standard sequence RoPE: every channel rotates by the token position,
with ``head_dim/2`` frequencies :math:`\theta_k = \text{theta}^{-2k/head\_dim}`
for :math:`k = 0 \ldots head\_dim/2 - 1`, each driving the adjacent channel
pair ``(2k, 2k+1)``.
Parameters
----------
seq_len : int
Number of positions in the sequence.
head_dim : int
Per-head channel dimension. Must be even (rotation acts on adjacent
channel pairs).
theta : float, optional, default=10000.0
Base used for the RoPE frequency schedule.
device : torch.device, optional
Device for the generated tables.
Returns
-------
Tuple[torch.Tensor, torch.Tensor]
``(cos, sin)``, each of shape :math:`(seq\_len, head\_dim)` in fp32.
"""
if head_dim % 2 != 0:
raise ValueError(
f"head_dim={head_dim} must be even for 1D RoPE "
f"(rotation acts on adjacent channel pairs)."
)
# head_dim/2 unique frequencies, each shared across an adjacent channel pair
# via repeat_interleave below.
k = torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
freqs = theta ** (-k / head_dim) # (head_dim/2,)
pos = torch.arange(seq_len, dtype=torch.float32, device=device)
ang = pos[:, None] * freqs[None, :] # (seq_len, head_dim/2)
cos = ang.cos().repeat_interleave(2, dim=-1) # (seq_len, head_dim)
sin = ang.sin().repeat_interleave(2, dim=-1)
return cos.contiguous(), sin.contiguous()
[docs]
def apply_rotary_pos_emb(
x: Float[torch.Tensor, "..."],
cos: Float[torch.Tensor, "..."],
sin: Float[torch.Tensor, "..."],
) -> Float[torch.Tensor, "..."]:
r"""Apply precomputed RoPE cos/sin tables to a query or key tensor.
Rotates each adjacent channel pair :math:`(x_a, x_b)` in
``x`` by the angle encoded in the corresponding position of ``cos``/``sin``:
.. math::
(x_a,\, x_b) \;\mapsto\;
(x_a \cos\phi - x_b \sin\phi,\;\; x_a \sin\phi + x_b \cos\phi)
This is the standard *rotate-half* formulation
``x * cos + rotate_half(x) * sin``. The arithmetic is promoted to fp32
regardless of ``x``'s dtype (the sign-flipped term accumulates error in
half precision) and cast back before returning.
Call this directly when you manage the cos/sin tables
yourself — for example, inside a custom NATTEN or domain-parallel attention
block where you obtain the tables from a
:class:`RotaryEmbedding2DTables` / :class:`RotaryEmbedding1DTables` provider
(or build them with :func:`build_axial_rope_cos_sin_2d` /
:func:`build_rope_cos_sin_1d`) and need to apply them independently to
queries and keys.
Parameters
----------
x : torch.Tensor
Query or key tensor of shape :math:`(\ldots, \text{positions}, head\_dim)`.
cos, sin : torch.Tensor
Rotation tables broadcastable to ``x`` over the trailing
``(positions, head_dim)`` dimensions (e.g. shape
:math:`(\text{positions}, head\_dim)`), as produced by
:func:`build_axial_rope_cos_sin_2d` or :func:`build_rope_cos_sin_1d`.
Returns
-------
torch.Tensor
Rotated tensor of the same shape and dtype as ``x``.
"""
in_dtype = x.dtype
x = x.float()
# rotate_half: swap adjacent channel pairs with a sign flip, mapping
# (x0, x1, x2, x3, ...) -> (-x1, x0, -x3, x2, ...). Stacking (-x_odd, x_even)
# along a new trailing axis and flattening interleaves them back into the
# original (2k, 2k+1) channel order.
x_even = x[..., 0::2]
x_odd = x[..., 1::2]
rotate_half = torch.stack((-x_odd, x_even), dim=-1).flatten(-2)
return (x * cos + rotate_half * sin).to(in_dtype)
[docs]
class RotaryEmbedding2DTables(Module):
r"""Shared owner of axial 2D RoPE cos/sin tables for an :math:`h \times w` grid.
This module *owns* the cos/sin tables and nothing else: it holds no
projections and applies no rotation. Its :meth:`forward` returns the
``(cos, sin)`` tables in explicit ``(h, w, head_dim)`` spatial layout, which
a consumer applies to its query/key with :func:`apply_rotary_pos_emb`.
The intended pattern is that a top-level, multi-block model constructs a
*single* instance and passes the returned tables into every attention
block's ``forward`` (see
:class:`~physicsnemo.nn.module.dit_layers.RopeNatten2DSelfAttention` and
:class:`~physicsnemo.models.dit.DiT`). Building, storing, and — under domain
parallelism — sharding the tables then happens exactly once for the whole
model instead of once per block.
The tables are stored as ``persistent=False`` buffers named ``rope_cos`` /
``rope_sin``: they are deterministically reconstructed from
``(latent_hw, head_dim, theta)`` and do not need to be saved with the model
weights. The names and the height-first ``(h, w, head_dim)`` layout are
chosen so that domain-parallel sharding along dimension 0 (height) gives
each rank globally-correct rows with no explicit rank offset in model code.
Parameters
----------
head_dim : int
Per-head channel dimension. Must be divisible by 4 (half per spatial
axis, then adjacent channel pairs within each half).
latent_hw : Tuple[int, int]
Spatial size :math:`(h, w)` of the token grid.
theta : float, optional, default=10000.0
Base used for the RoPE frequency schedule.
Forward
-------
latent_hw : Tuple[int, int], optional
Override the spatial grid size at call time. If given and different from
the current grid, the cos/sin tables are rebuilt in place before being
returned (off the ``torch.compile`` fast path). Under domain parallelism
the in-place rebuild replaces the sharded buffers with plain tensors, so
it is only appropriate for single-device variable-resolution inference.
Outputs
-------
Tuple[torch.Tensor, torch.Tensor]
``(rope_cos, rope_sin)``, each of shape :math:`(h, w, head\_dim)`.
Examples
--------
>>> import torch
>>> from physicsnemo.nn.module.rope import (
... RotaryEmbedding2DTables,
... apply_rotary_pos_emb,
... )
>>> rope = RotaryEmbedding2DTables(head_dim=16, latent_hw=(4, 4))
>>> cos, sin = rope()
>>> cos.shape
torch.Size([4, 4, 16])
>>> # q reshaped to spatial layout (B, heads, h, w, head_dim)
>>> q = torch.randn(2, 8, 4, 4, 16)
>>> q_rot = apply_rotary_pos_emb(q, cos.unsqueeze(0).unsqueeze(0), sin.unsqueeze(0).unsqueeze(0))
>>> q_rot.shape
torch.Size([2, 8, 4, 4, 16])
"""
def __init__(
self,
head_dim: int,
latent_hw: Tuple[int, int],
theta: float = 10000.0,
):
super().__init__()
if head_dim % 4 != 0:
raise ValueError(
f"head_dim={head_dim} must be divisible by 4 for axial 2D RoPE."
)
self.head_dim = int(head_dim)
self.theta = float(theta)
self._latent_hw: Tuple[int, int] = (int(latent_hw[0]), int(latent_hw[1]))
cos, sin = build_axial_rope_cos_sin_2d(
*self._latent_hw, self.head_dim, theta=self.theta
)
# persistent=False: not in state_dict (rebuilt deterministically from
# latent_hw + head_dim + theta), so checkpoints stay lean. Names
# rope_cos/rope_sin and (h, w, head_dim) layout let domain parallelism
# shard parameters along desired axes.
self.register_buffer("rope_cos", cos, persistent=False) # (h, w, head_dim)
self.register_buffer("rope_sin", sin, persistent=False) # (h, w, head_dim)
def _maybe_rebuild(self, h: int, w: int) -> None:
r"""Rebuild the cos/sin tables for a new latent shape if it changed.
Reached only when :meth:`forward` is called with a ``latent_hw`` that
differs from the current grid (e.g. variable-resolution inference); not
part of the training-time hot path.
"""
if (int(h), int(w)) == self._latent_hw:
return
target_dtype = self.rope_cos.dtype
target_device = self.rope_cos.device
cos, sin = build_axial_rope_cos_sin_2d(
h, w, self.head_dim, theta=self.theta, device=target_device
)
self.register_buffer("rope_cos", cos.to(dtype=target_dtype), persistent=False)
self.register_buffer("rope_sin", sin.to(dtype=target_dtype), persistent=False)
self._latent_hw = (int(h), int(w))
[docs]
def forward(
self,
latent_hw: Optional[Tuple[int, int]] = None,
) -> Tuple[
Float[torch.Tensor, "h w head_dim"],
Float[torch.Tensor, "h w head_dim"],
]:
if latent_hw is not None:
self._maybe_rebuild(int(latent_hw[0]), int(latent_hw[1]))
return self.rope_cos, self.rope_sin
[docs]
class RotaryEmbedding1DTables(Module):
r"""Shared owner of standard 1D RoPE cos/sin tables for a token sequence.
This module *owns* the cos/sin tables and nothing else: it holds no
projections and applies no rotation. Its :meth:`forward` returns the
``(cos, sin)`` tables of shape :math:`(seq\_len, head\_dim)`, which a
consumer applies to its query/key with :func:`apply_rotary_pos_emb`. This is
the same RoPE variant used by most autoregressive and encoder transformer
architectures (LLaMA, GPT-NeoX, etc.).
A top-level, multi-block transformer constructs a *single* instance and
shares its tables across all blocks, so the tables are built and stored once
instead of once per block. Sequences shorter than ``max_seq_len`` are served
by returning the leading positions of the precomputed table, so one instance
covers any length up to ``max_seq_len`` without rebuilding.
The tables are stored as ``persistent=False`` buffers (they are
deterministically reconstructed from ``(max_seq_len, head_dim, theta)`` and
do not need to be saved with the model weights).
Parameters
----------
head_dim : int
Per-head channel dimension. Must be even (rotation acts on adjacent
channel pairs).
max_seq_len : int
Maximum sequence length for which to precompute tables.
theta : float, optional, default=10000.0
Base used for the RoPE frequency schedule.
Forward
-------
seq_len : int, optional
Number of leading positions to return. If ``None``, the full
``max_seq_len`` table is returned.
Outputs
-------
Tuple[torch.Tensor, torch.Tensor]
``(cos, sin)``, each of shape :math:`(seq\_len, head\_dim)`.
Examples
--------
>>> import torch
>>> from physicsnemo.nn.module.rope import (
... RotaryEmbedding1DTables,
... apply_rotary_pos_emb,
... )
>>> rope = RotaryEmbedding1DTables(head_dim=16, max_seq_len=128)
>>> cos, sin = rope(seq_len=100)
>>> cos.shape
torch.Size([100, 16])
>>> q = torch.randn(2, 8, 100, 16) # (B, heads, seq, head_dim)
>>> q_rot = apply_rotary_pos_emb(q, cos, sin)
>>> q_rot.shape
torch.Size([2, 8, 100, 16])
"""
def __init__(
self,
head_dim: int,
max_seq_len: int,
theta: float = 10000.0,
):
super().__init__()
if head_dim % 2 != 0:
raise ValueError(f"head_dim={head_dim} must be even for 1D RoPE.")
self.head_dim = int(head_dim)
self.theta = float(theta)
self.max_seq_len = int(max_seq_len)
cos, sin = build_rope_cos_sin_1d(
self.max_seq_len, self.head_dim, theta=self.theta
)
self.register_buffer("cos", cos, persistent=False) # (max_seq_len, head_dim)
self.register_buffer("sin", sin, persistent=False)
[docs]
def forward(
self,
seq_len: Optional[int] = None,
) -> Tuple[
Float[torch.Tensor, "seq head_dim"],
Float[torch.Tensor, "seq head_dim"],
]:
if seq_len is None:
return self.cos, self.sin
if not torch.compiler.is_compiling() and seq_len > self.max_seq_len:
raise ValueError(
f"sequence length {seq_len} exceeds max_seq_len={self.max_seq_len}"
)
# Slice the leading positions so one instance serves any length <= max.
return self.cos[:seq_len], self.sin[:seq_len]