Skip to content

Mixer Benchmarks - #137

Open
farhadrgh wants to merge 33 commits into
mainfrom
farhadr/2d_bench
Open

Mixer Benchmarks#137
farhadrgh wants to merge 33 commits into
mainfrom
farhadr/2d_bench

Conversation

@farhadrgh

Copy link
Copy Markdown
Collaborator

Summary

Environment setup

Create the conda environment (required to run tests):

bash setup_conda_env.sh
conda activate nvsubquadratic

Test plan

  • pre-commit run --all-files passes (pre-commit install if not yet set up).
  • Existing tests pass (pytest tests/).
  • New tests added, or explain why not needed:

Documentation checklist

For every new or modified public symbol in nvsubquadratic/ or experiments/:

  • Every new module has a module-level docstring explaining what it contains and why.
  • Every new public class has a class docstring covering purpose, math/motivation, and key attributes.
  • Every new public method / function has Args: and Returns: blocks with tensor shapes where applicable.
  • Math notation is consistent with the paper (or a comment explains any deviation).
  • Docstrings containing backslashes use r"""...""" (required by ruff D301).
  • If a new file was added, a row has been added to docs-tracker.md with status [x].

See CONVENTIONS.md for the full style guide.

farhadrgh added 17 commits July 20, 2026 08:03
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
@farhadrgh
farhadrgh requested a review from saeepaliwal as a code owner July 23, 2026 21:46
farhadrgh and others added 12 commits July 23, 2026 14:46
JSONL data and time/memory plots for the GB200 forward-time sweeps:
- results_8_hidden_16M_tokens/: hidden 8 reach sweep (HyenaND / Attention /
  Mamba2 out to 16M tokens; attention is the non-flash head_dim-4 path).
- results_512_hidden/: hidden 512, head_dim 128 flash-kernel comparison
  (HyenaND vs SDPA / FlexAttention / FlashAttention-4 / Mamba2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Consolidate the two benchmark sets under benchmarks/results/ and document
them in one README (protocol, configs, headline numbers, reproduce steps):
- reach_hidden8/   (hidden 8, head_dim 4 — scaling reach to 16M tokens)
- flash_hidden512/ (hidden 512, head_dim 128 — SDPA/Flex/FA4/Mamba2 comparison)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `subquadratic_ops_torch.fused_fft_conv2d` as a first-class 2D FFT-conv
path. Unlike every other FFT backend it runs the whole rfft2/multiply/irfft2
pipeline in a single cuFFTDx launch and natively in fp32/fp16/bf16 rather than
upcasting to fp32. Measured on H200 (B=8, H=768, fwd+bwd, bf16): 3.6-3.9x over
`torch_fft` and 1.2-2.4x over the existing `subq_ops` path.

Two entry points:

* `fft_backend="subq_ops_fused"` on CKConvND — explicit and predictable.
  Restricted to data_dim=2, non-causal, zero padding, and spatial extents of
  at most 64 per axis (the kernel's largest FFT tile is 128 and it requires
  max(X, Y) <= fft_size // 2). The spatial cap is enforced on the first
  forward pass, not at construction, since input size is unknown there.

* `nvsubquadratic.ops.fftconv_lowering` — an inductor pre-grad pass that
  rewrites fftconv.py's chain onto the fused kernel, so a model already on
  `fft_backend="torch_fft"` picks it up without a config change. This matters
  because inductor cannot codegen complex operators and otherwise falls back
  to eager cuFFT for the entire chain. Enable per-callable with
  `torch.compile(model, options=fused_fftconv2d_options())`, or for a scope
  with the `fused_fftconv2d_lowering()` context manager.

  Pre-grad rather than post-grad so autograd is derived from the custom op's
  registered backward instead of requiring a consistent forward+backward
  rewrite. It fires only on an exact match of the reference recipe (padding
  rule, crop offset, shape limits, CUDA device, and a compute capability that
  supports the required tile — the 128 tile needs SM90+, as SM80/SM86 lack the
  shared memory). `lowering_stats()` reports rewrite and per-reason skip counts,
  since a silent pass is otherwise indistinguishable from one that never ran.

Crop-offset reconciliation: the upstream kernel crops the 'same' window at
fft_size // 2 whereas fftconv.py crops at K // 2. The wrapper pre-pads the
filter's top/left by the difference, making results interchangeable with the
other backends (~3e-7 normwise in fp32, ~3e-3 in bf16). Without it the output
is shifted by fft_size // 2 - K // 2 pixels — which reads as a ~1.41 (sqrt 2,
fully decorrelated) error, not as reduced accuracy. A regression test pins this
so the pre-pad cannot be optimised away.

Also registers the fused operators eagerly when the pass is constructed:
inductor's on-disk FX cache lets a compiled artifact call
torch.ops.subquadratic_ops_torch.* on a cache hit without any Python call
having triggered the wrappers' lazy import, which failed with an opaque
op-namespace AttributeError.

Fixes a pre-existing test-gate bug: tests/conftest.py resolved the kernel
version from the `subquadratic-ops-torch-cu12` distribution only. On the
`-cu13` install that pyproject.toml pins it returned (0, 0, 0), silently
turning `requires_subq_ops_v2` into a blanket xfail and hiding every
`subq_ops` test result. It now checks both distributions.

Includes the in-flight CUDA 13.2 build changes already present in the working
tree (Dockerfile base image, cu132 torch/DALI pins, cu13 kernel distribution).

Tests: 138 new (op-level equivalence across dtypes/shapes/layouts/FiLM, forward
and backward, CKConvND integration, and lowering fire/decline behaviour).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Dockerfile: base nvcr.io/nvidia/cuda:13.0.3-devel (matched to torch cu130 so
  apex/mamba's CUDA-version check passes), torch/torchvision cu130,
  nvidia-dali-cuda130. The [cuda] extra resolves subquadratic-ops-torch-cu13
  (>=0.2.2) from the internal GitLab registry via a build secret; FlashAttention-4
  uses the [cu13] extra.
- build_sqsh.sh: pass the gitlab_token secret (+ preflight), and refresh QEMU
  binfmt for arm64-on-x86 cross-builds (fixes the nvcc SIGSEGV under stale QEMU).
- pyproject: [cuda] → subquadratic-ops-torch-cu13, [dali] → nvidia-dali-cuda130.
- Docs / examples / conftest / CHANGELOG updated to CUDA 13.0 / cu130 / cu13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Farhad Ramezanghorbani <farhadr@nvidia.com>
Brings in fft_backend="subq_ops_fused" and the torch.compile lowering pass so
the 2D forward-time benchmarks can exercise the fused cuFFTDx kernel.

Conflict resolutions:
  * pyproject.toml — take #138's subquadratic-ops-torch-cu13>=0.2.2 pin (needed
    for fused_fft_conv2d), noting that 0.2.2 currently ships only from the
    internal NVIDIA GitLab registry (public PyPI tops out at 0.2.1).
  * Dockerfile — keep this branch's CUDA 13.0 base + torch 2.10.0/cu130 pins
    rather than #138's 13.2/2.12.1: apex and mamba only build when the base nvcc
    CUDA matches torch's exactly, and the benchmark image builds both. Adopted
    #138's ARG parameterisation with cu130 defaults so a cu132 image is a
    build-arg away, and added SUBQ_OPS_INDEX_URL for the 0.2.2 registry.
  * tests/conftest.py — take #138's version (queries both the cu12 and cu13
    distributions instead of only cu13).
  * CHANGELOG.md — keep both entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
benchmark_forward_time_nd_resolution.py
  * --fft-backend gains "subq_ops_fused".
  * resolve_hyena_backend() picks the effective backend PER POINT instead of
    once per run. The fused kernel is 2D-only and capped at 64 per axis (its
    largest FFT tile is 128 and it needs max(X,Y) <= fft_size//2), so a single
    --fft-backend choice cannot cover a 16..16M sweep: 2D R<=64 runs fused, 2D
    R>=128 and all of 1D fall back to subq_ops, 3D to torch_fft. The per-point
    backend is echoed before the sweep and recorded in each JSONL row, so the
    plots do not imply the fused kernel ran where it could not.

submit_forward_time_nd.sh
  * Sweeps every power of two from a 16-wide grid to ~16M tokens (1D 16..16M,
    2D 16..4096, 3D 16..256).
  * FFT_BACKEND now defaults to subq_ops_fused.
  * SUBQ_OPS_WHEEL_DIR mounts a host directory of pre-staged wheels and installs
    them offline (--no-index) at job start, so the newer kernel can be swapped in
    without rebuilding the .sqsh. Verifies fused_fft_conv2d imports and aborts if
    it does not, rather than silently timing the fallback path.

submit_forward_time_flash_kernels.sh
  * Extend the same sweeps down to R=16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merge pinned subquadratic-ops-torch-cu13>=0.2.2 for fused_fft_conv2d, but
public PyPI only carries 0.2.1, so the Dockerfile's `.[all]` layer now fails on a
clean build. Resolve it from an internal GitLab index instead.

The token is passed as a BuildKit secret rather than a --build-arg: a build-arg
is recorded in the image's layer history, so the token would ship with every
.sqsh built from it. build_sqsh.sh reads GITLAB_TOKEN from the environment or
~/.gitlab_token, builds the index URL, and fails early with the token-creation
steps rather than letting the build run for hours before dying on pip install.

Drop the secret once 0.2.2 is published to public PyPI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build_sqsh.sh needs `docker buildx`, plus QEMU binfmt emulation to produce an
arm64 image from an x86 host. Cluster login nodes here have neither — no docker
daemon or any other builder, no qemu-aarch64 registered, and no root to install
either — so there was no way to produce a GB200 image from the login node.
`enroot import` is not a way around it: /tmp is a 2 GB tmpfs (too small to
extract a CUDA devel image) and redirecting it to lustre fails because lustre
cannot hold the capability xattrs enroot sets while extracting layers.

Instead, let pyxis pull the base image onto a GB200 node, take root inside it
with --container-remap-root, replay the Dockerfile's steps natively, and write
the result out with --container-save. Building on the target architecture also
removes QEMU, so the MAX_JOBS=1 throttle build_sqsh.sh needs to survive emulated
apex/mamba compiles does not apply: this defaults to MAX_JOBS=32.

The token reaches the container as a read-only mounted file rather than an env
var — srun's environment is visible via `scontrol show job`, and an exported var
would be captured into the saved image.

This script REPLAYS the Dockerfile rather than parsing it, so the version pins
are duplicated in both and will drift if only one is edited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Farhad Ramezanghorbani and others added 4 commits August 10, 2026 14:01
PR #138 raised pyproject's floor to torch>=2.12.0,<2.13.0, but the Dockerfile
kept TORCH_VERSION=2.10.0. Those two contradict, and the failure was silent:
apex, mamba-ssm and causal-conv1d compiled against 2.10.0 in the middle of the
build, then the final `.[all]` step resolved nvsubquadratic's own requirement and
upgraded torch to 2.12.1 underneath them — leaving compiled extensions built
against headers that no longer matched the installed torch.

Nothing caught this, because the build's verification printed package METADATA
versions, which report happily regardless of what the extension was built
against. Add a guard that compares torch.__version__ to the pin and fails the
build, and note the coupling on both pins so they are not edited apart.

Also fix the FA4 probe in both files: nvidia_cutlass_dsl exposes no __version__,
so reading the attribute raised AttributeError and made a perfectly working FA4
install report as failed on every build. Read the version from metadata instead.

Verified on the rebuilt image: torch 2.12.1+cu130, apex / causal_conv1d /
flash_attn.cute / Mamba2 / fused_fft_conv2d all import, and causal_conv1d_fn
executes on-GPU.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HyenaND's short conv was always a torch ConvNd with symmetric padding, in every
config. SubqOpsCausalConv1d — the wrapper around the fused
subquadratic_ops_torch.causal_conv1d kernel — existed and was unit-tested, but
nothing on any Hyena path used it (the module docstring cites two example configs
that in fact select CausalConv1D / torch.nn.Conv1d).

Make the short conv follow the operator's causality rather than always padding
symmetrically:

  * is_causal (the 1D sweeps) -> left-only padding: SubqOpsCausalConv1d with
    --short-conv=subq_ops (the default), else CausalConv1D. The kernel is
    depthwise-only, which this config already satisfies (groups == in_channels
    == out_channels == 3*hidden_dim), stride/dilation 1.
  * non-causal (2D/3D) -> symmetric torch.nn.ConvNd, unchanged. A causal 1D
    kernel does not apply there, so --short-conv=subq_ops falls back with a note.

This also closes a causality hole in the 1D config: it paired a causal long conv
with a symmetric short conv, so the operator could see one token of future
context. 1D Hyena timings will therefore shift slightly against earlier runs.

The effective short conv is printed before the sweep and recorded per JSONL row,
so a plot cannot silently mix short-conv implementations across points.

Mamba keeps its own causal_conv1d (Dao-AILab's) — untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fused kernel picks its specialisation from the INPUT dtype and then requires
every tensor to match exactly. Under torch.autocast the activations arrive as
bf16/fp16 while nn.Module parameters stay fp32, so passing self.weight through
unchanged aborted with:

    ValueError: in_w expected dtype (code=4, bits=16) but got (code=2, bits=32)

That made the module unusable on the standard mixed-precision path — it only
worked when the caller had already narrowed the parameters by hand, which is why
the existing tests (all same-dtype) did not catch it.

Cast weight/bias to the input dtype in forward, mirroring what autocast does for
the built-in conv ops: fp32 master parameters are untouched, only the values
handed to the kernel are narrowed.

Verified on GB200 against CausalConv1D: fp32 max|fused-ref| = 2.4e-07, bf16
1.6e-02, fp16 2.0e-03 (rounding), and perturbing position t changes outputs at
t..L-1 while leaving 0..t-1 bit-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every existing test feeds fp32 activations to fp32 parameters, so all tensors
agree by construction and the kernel's exact-dtype check passes. That is why the
module could not run under torch.autocast at all without any test noticing.

Add TestAutocast: that the module runs under bf16/fp16 autocast, that the fp32
master weight is not mutated (only the kernel's inputs are narrowed), and that
the result still matches CausalConv1D at a tolerance appropriate to the narrowed
dtype rather than the fp32 ATOL.

14 passed on GB200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants