Getting Started#
This page walks a new user from a fresh checkout to a working Hyena forward pass. For the full installation matrix (dev container, Docker, Apptainer, conda, venv) see the project README.
Requirements#
CUDA-compatible NVIDIA GPU
CUDA Toolkit 12.0 or higher
Python 3.10 or higher
The optional fused RMSNorm kernel (quack-kernels) requires Hopper or
Blackwell (H100, B200, B300); on Ampere the library falls back to a
pure-PyTorch path automatically.
Install#
For users who just want to use the library:
pip install nvsubquadratic # full training/experiment stack
pip install "nvsubquadratic[cuda]" # + accelerated fused FFT-conv / causal-conv CUDA kernels
pip install "nvsubquadratic[quack]" # + fused RMSNorm kernel (Hopper/Blackwell only)
pip install "nvsubquadratic[all]" # everything
The accelerated CUDA kernels ([cuda]) need nvcc to build and are kept out of
core, so the base install also works without the CUDA toolkit (the operators
default to the portable torch.fft backend).
For contributors or developer setup, the recommended path is conda:
bash setup_conda_env.sh
conda activate nvsubquadratic
This creates an environment with Python 3.12 and PyTorch 2.10 (CUDA
12.9), installs the dev dependencies, builds NVIDIA Apex from source,
and installs quack-kernels.
For an alternative venv-based install:
python3 -m venv venv
source venv/bin/activate
pip install torch==2.10.0 torchvision==0.25.0 \
--index-url https://download.pytorch.org/whl/cu129
pip install -r requirements-dev.txt
pip install --no-build-isolation -e .
Docker, Apptainer, enroot/SLURM, and dev-container instructions live in the project README.
Hello, Hyena#
A minimal forward pass through a real 2D Hyena mixer. Everything is
wired with LazyConfig: each LazyConfig(Cls)(...)
records the class and its arguments without constructing anything, and a
single instantiate(...) call at the end builds the whole tree. This is
exactly how the experiments configs
assemble their networks, except there the scalar fields are filled by
"${net.hidden_dim}"-style interpolation instead of the concrete
integers used below.
import torch
from nvsubquadratic.lazy_config import LazyConfig, instantiate
from nvsubquadratic.modules.hyena_nd import Hyena
from nvsubquadratic.modules.ckconv_nd import CKConvND
from nvsubquadratic.modules.kernels_nd import SIRENKernelND
from nvsubquadratic.modules.rms_norm_channel_first import RMSNormChannelFirst
from nvsubquadratic.utils.qk_norm import L2Norm
device = torch.device("cuda")
B, H, X, Y = 2, 64, 32, 32
hyena_cfg = LazyConfig(Hyena)(
# The long-range global convolution. CKConvND owns a SIREN-parameterised
# kernel and applies it as an FFT conv — the kernel is *generated* by the
# SIREN MLP, never random.
global_conv_cfg=LazyConfig(CKConvND)(
data_dim=2,
hidden_dim=H,
kernel_cfg=LazyConfig(SIRENKernelND)(
data_dim=2,
out_dim=H,
mlp_hidden_dim=32,
num_layers=3,
embedding_dim=32,
omega_0=10.0,
hidden_omega_0=1.0,
L_cache=max(X, Y),
use_bias=True,
),
mask_cfg=LazyConfig(torch.nn.Identity)(),
grid_type="double", # linear (non-circular) convolution
fft_padding="zero",
fft_backend="torch_fft", # portable; "subq_ops" uses the fused 2D CUDA kernel
is_causal=False,
),
# Depthwise short conv on the concatenated [Q; K; V] (3 * H channels).
short_conv_cfg=LazyConfig(torch.nn.Conv2d)(
in_channels=3 * H,
out_channels=3 * H,
kernel_size=3,
groups=3 * H,
padding=1,
bias=False,
),
gate_nonlinear_cfg=LazyConfig(torch.nn.SiLU)(), # first gate σ
gate_nonlinear_2_cfg=LazyConfig(torch.nn.Sigmoid)(), # second gate σ₂
pixelhyena_norm_cfg=LazyConfig(RMSNormChannelFirst)(
dim=H, eps=1e-6, use_quack=False
),
output_norm_cfg=LazyConfig(RMSNormChannelFirst)(dim=H, eps=1e-6, use_quack=False),
qk_norm_cfg=LazyConfig(L2Norm)(dim=1), # L2 QK-norm on the channel axis
)
# Build the whole module tree in one call.
hyena = instantiate(hyena_cfg).to(device)
# Hyena consumes channels-last Q, K, V tensors [B, *spatial, C]. In a full
# model these come from a linear projection W·x (see QKVSequenceMixer); here
# we feed random activations to exercise the forward.
q = torch.randn(B, X, Y, H, device=device)
k = torch.randn(B, X, Y, H, device=device)
v = torch.randn(B, X, Y, H, device=device)
y = hyena(q, k, v)
print(y.shape) # torch.Size([2, 32, 32, 64]) -> [B, X, Y, C]
In a real network you rarely hold Q, K, V yourself: the
QKVSequenceMixer (see
mixer_defaults.py)
projects a single activation x into the three tensors and forwards them
to this Hyena. That same factory is what the spatial-recall experiments
instantiate.
Going lower: the FFT conv op directly#
The Hyena above ultimately routes its long-range mixing through one of the
FFT-convolution ops. When you only need the convolution itself, with no
gating, kernel generation, or nn.Module, you can call the op
directly. Here kernel is supplied explicitly (any 2D filter; a SIREN
kernel would normally produce it):
import torch
from nvsubquadratic.ops.fftconv import fftconv2d_fp32_bhl
device = torch.device("cuda")
B, H, X, Y = 2, 64, 32, 32
x = torch.randn(B, H, X, Y, device=device) # channels-first [B, C, X, Y]
kernel = torch.randn(1, H, X, Y, device=device) # per-channel filter [1, C, K_x, K_y]
y = fftconv2d_fp32_bhl(x, kernel) # "same"-size circular-free conv
print(y.shape) # torch.Size([2, 64, 32, 32])
The op casts to fp32 internally for numerical stability and returns the
result in x’s original dtype. Note the layout difference: the ops work
channels-first [B, C, *spatial] (the _bhl suffix), whereas the
Hyena module’s public interface is channels-last [B, *spatial, C].
The lower-level FFT ops in
nvsubquadratic.ops are deliberately
function-only so higher-level mixers can compose them freely. The
nvsubquadratic.modules package wraps them
in nn.Module-shaped mixers (Hyena, Mamba, Attention, CKConv), and
experiments wires those mixers into
Lightning-driven training pipelines.
Next steps#
Architecture: the three layers (nvSubquadratic, subquadratic-ops, megatron-core) and the naming conventions used throughout the library.
examples/: end-to-end training recipes per dataset.API Reference: the full curated API surface.