Skip to content

Migrate DeepGEMM from pybind11 to TORCH_LIBRARY op registration - #2

Draft
cleonard530 wants to merge 29 commits into
nv_devfrom
migrate_pybind_to_torch_library
Draft

cleonard530 wants to merge 29 commits into
nv_devfrom
migrate_pybind_to_torch_library

Conversation

@cleonard530

@cleonard530 cleonard530 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates DeepGEMM's C++ extension from pybind11 (PYBIND11_MODULE + per-header register_apis) to TORCH_LIBRARY / TORCH_LIBRARY_IMPL registration. Ops are now registered as torch.ops.deep_gemm.*.

To preserve the existing Python API (import deep_gemm; deep_gemm._C.fp8_gemm_nt(...)), a new deep_gemm/_C.py shim loads the extension and re-exports the legacy surface. The compiled module is renamed to deep_gemm._C_extension and built as a limited-API (abi3) extension.

Also updates .pyi stub generation to read TORCH_LIBRARY schemas from csrc/ and overlay defaults/types from _C.py, and splits mega MoE symm-buffer sizing/slicing into two separate ops (replacing the old pybind callback return value).

WARNING:

Before, get_symm_buffer_size_for_mega_moe returned an int and a function (std::tuple<int64_t, std::function...>) but TORCH_LIBRARY doesn't allow returning function objects/closures. Now, it just returns an int, and a separate function, _slice_symm_buffer_for_mega_moe, is used instead of the closure. This breaks backwards compatibility for get_symm_buffer_size_for_mega_moe, but this method is not re-exported from deep_gemm/__init__.py (only used internally by SymmBuffer.__init__), and a quick look on GitHub shows no other public repo calls this function directly either.

New files

deep_gemm/_C.py

Python compatibility shim for the old deep_gemm._C module. Responsibilities:

torch.ops.load_library(...) on _C_extension*.so at import time
Re-export torch.ops.deep_gemm.* under the legacy _C names
Restore pybind-era conveniences that are not expressible in TORCH_LIBRARY schemas alone: tuple unpacking (q/kv/(weight, scale) pairs), default arguments, and guarded op binding for #if DG_*_COMPATIBLE build variants
Previously _C was the compiled .so itself; now _C is pure Python and the .so is _C_extension.

csrc/torch_library_utils.hpp

(namespace deep_gemm::torch_utils) Conversion helpers for bridging PyTorch schema types to the C++ types the existing kernel implementations expect — e.g. c10::List<int64_t>std::tuple<int,int,int> for recipe/head_splits, and optional list → std::vector<int>. Without this, every TORCH_LIBRARY wrapper would duplicate the same list/tuple parsing logic.

Significantly changed files

csrc/apis/mega.hpp

  • Added build_symm_buffer_layout, which computes the full SymmBufferLayoutInfo (every sub-buffer's offset/size) once. get_symm_buffer_size_for_mega_moe, slice_symm_buffer_for_mega_moe, and fp8_fp4_mega_moe/bf16_mega_moe all call it instead of duplicating layout math — the kernel entry points build the layout and slice the buffer directly rather than going through a second registered op.
  • get_symm_buffer_size_for_mega_moe used to return (int64_t, std::function<...>) under pybind; TORCH_LIBRARY can't return closures. It now returns just the int, and slicing moved to a separate op, _slice_symm_buffer_for_mega_moe (leading underscore since it's an internal implementation detail — only deep_gemm/mega/__init__.py's SymmBuffer calls it, it isn't part of the public API).

csrc/python_api.cpp

Replaces the old PYBIND11_MODULE(...) entry point. Ops now self-register via TORCH_LIBRARY_FRAGMENT/STABLE_TORCH_LIBRARY_IMPL static initializers inside each csrc/apis/*.hpp, which run automatically when the .so is loaded — so this file no longer needs to call per-header register_apis(m) functions. It just #includes every apis/*.hpp (to trigger their static registration) and defines an empty PyInit_ symbol via the REGISTER_EXTENSION macro, purely so the compiled .so is still a valid, importable Python extension module — no actual bindings go through the Python C API anymore.

scripts/generate_pyi.py

Previously generated .pyi type hints by regex-parsing the C++ function declarations in csrc/ — but under pybind, the real Python-callable keyword names come from py::arg("...") strings in PYBIND11_MODULE, which pybind doesn't require to match the C++ parameter names. That divergence caused real bugs: e.g. fp8_fp4_paged_mqa_logits's C++ parameter is named fused_kv_cache, but it's bound as py::arg("kv_cache"), so the old generated stub advertised a keyword (fused_kv_cache) that would actually raise TypeError if used — the true kwarg is kv_cache. The script now parses TORCH_LIBRARY schema strings from csrc/ and overlays defaults from an AST parse of deep_gemm/_C.py, so the stub is generated from the real registration contract and entry point instead of an incidental C++ signature.

Test plan/Results

  1. To help verify that each kernel op is still functioning correctly, I created a script, smoke_test_all_ops.py, that performs a minimal call to every op and verifies that any tensor inputs expected to be immutable remain bit-for-bit identical before and after the call.

Checkout commit 4cb88be to see the smoke_test_all_ops.py script that exercises every TORCH_LIBRARY op. I added the results in the comment at the bottom of the commit (they all passed or were skipped because I didn't have the right SM).

  1. I also compared the generated .pyi file before (using C++ signatures directly) and after (using TORCH_LIBRARY schemas) to make sure types are mapped appropriately. The differences can be found here Gen pyi test #5 .

  pytest tests/test_bf16.py
  pytest tests/test_fp8_fp4.py
  pytest tests/test_layout.py
  pytest tests/test_einsum.py
  pytest tests/test_attention.py
  pytest tests/test_hyperconnection.py
  pytest tests/test_legacy.py
  pytest tests/test_coverage_gaps.py  # (local test that fills in the kernel gaps)
  pytest tests/test_lazy_init.py

All test pass!

Comment thread csrc/apis/attention.hpp
py::arg("max_context_len"), py::arg("clean_logits") = false,
py::arg("indices") = std::nullopt);
static void fp8_gemm_nt_skip_head_mid(
const torch::Tensor& a, const torch::Tensor& sfa,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sfa and sfb are here because TORCH_LIBRARY schemas can’t express pybind-style (tensor, scale) pairs. They need flat tensor arguments.

This shows up a lot in this migration.

Comment thread deep_gemm/mega/__init__.py Outdated
mma_type, activation,
num_ring_tokens,
)
slice_input_buffers = lambda buffer: _C.slice_symm_buffer_for_mega_moe(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TORCH_LIBRARY cannot return a callback like pybind11 can, so a lamda function on the python side is created.

#if DG_FP8_COMPATIBLE and DG_TENSORMAP_COMPATIBLE
static void tf32_hc_prenorm_gemm(const torch::Tensor& a, const torch::Tensor& b,
const torch::Tensor& d, const torch::Tensor& sqr_sum,
const c10::optional<int64_t>& num_splits) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapper is needed since TORCH_LIBRARY int? maps to optional<int64_t>, not optional<int>

Comment thread csrc/utils/torch_compat.hpp Outdated
// re-export ATen types. DeepGEMM csrc uses torch::Tensor throughout; under
// Py_LIMITED_API we cannot include torch/python.h or torch/types.h (autograd
// pulls the full Python C-API). Re-export at:: into torch:: instead.
namespace torch {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows us to use torch::... instead of at::...

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look into another header (e.g torch/all.h) instead of this wrapper

Comment thread csrc/torch_library_macros.hpp Outdated

} // namespace deep_gemm::torch_registration

#define DEEP_GEMM_IMPL(fn) TORCH_FN(deep_gemm::torch_registration::fn)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defines a shorthand for binding adapter functions in deep_gemm::torch_registration

@@ -0,0 +1,64 @@
#pragma once

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper functions to turn c10::List<int64_t> (TORCH_LIBRARY allowed type) to std::tuples<int,...> (existing kernel APIs) are defined in this helper file.

Comment thread deep_gemm/__init__.py
@@ -1,6 +1,5 @@
import os
import subprocess
import torch

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import

Comment thread deep_gemm/_C.py
@@ -0,0 +1,353 @@
import torch

@cleonard530 cleonard530 Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file loads the compiled _C_extension*.so via torch.ops.load_library, wraps torch.ops.deep_gemm with Python shims that preserve the old API (tuple unpacking, guarded exports), and re-exports those ops as the deep_gemm._C module.

This keeps import _C consistency with legacy code

Comment thread scripts/generate_pyi.py
from pathlib import Path


def build_cpp_function_index(root_path):

@cleonard530 cleonard530 Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file used to build the .pyi file by parsing the C++ functions. Now it used the TORCH_LIBRARY schema and _C.py (for default values) to build the .pyi file with type hints.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The differemce in the generated file can be seen here: 02ed6bc

Comment thread scripts/generate_pyi.py
extensions = {'.hpp', '.cpp', '.h', '.cc'}

# Regex: match m.def( ... ), supports multi-line
pattern = re.compile(r'm\.def\s*\(')

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used

Comment thread scripts/generate_pyi.py
line = lines[i]
if 'm.def(' in line:
# Found a potential starting line
start_i = i

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used

Comment thread scripts/generate_pyi.py
break
j += 1
else:
pass

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't do anything in this while loop

Comment thread scripts/generate_pyi.py
cpp_results.append(cpp_item)

pyi_content = generate_pyi_file_content(cpp_results, module_name=name)
wrapper_defaults = {}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get default arguments from _C.py

Comment thread csrc/apis/mega.hpp
const auto num_ranks = static_cast<int>(sym_buffer_ptrs.size());
const auto num_experts_ = num_experts_per_rank * num_ranks;
const auto [num_required_bytes, slice] = get_symm_buffer_size_for_mega_moe(
const auto layout_info = build_symm_buffer_layout(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fp8_fp4_mega_moe and bf16_mega_moe now use the internal build_symm_buffer_layout to get the num_required_bytes and slice (through slice_symm_buffer_from_layout) instead of the registered torch ops get_symm_buffer_size_for_mega_moe, which used to return a callback but is unable to with TORCH_LIBRARY (it was allowed on pybind). This op is still registered and used in deep_gemm/mega/__init__.py as well as slice_symm_buffer_for_mega_moe (to replace the callback) which is a small wrapper around slice_symm_buffer_from_layout.

@cleonard530 cleonard530 changed the title migrated off of pybind and onto TORCH_LIBRARY Migrate DeepGEMM from pybind11 to TORCH_LIBRARY op registration Jul 14, 2026
Comment thread deep_gemm/_C.py Outdated
import torch
from pathlib import Path

_SCALAR_TYPE = {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove this and use dtype

Comment thread csrc/torch_library_utils.hpp Outdated

#include "utils/exception.hpp"

namespace deep_gemm::torch_library_utils {

@cleonard530 cleonard530 Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make shorter (e.g torch_utils)

@cleonard530
cleonard530 force-pushed the migrate_pybind_to_torch_library branch from 441c417 to eb4f3c9 Compare July 21, 2026 16:39
@cleonard530
cleonard530 marked this pull request as ready for review July 21, 2026 16:43
@cleonard530
cleonard530 changed the base branch from main to nv_dev July 21, 2026 16:44
@cleonard530
cleonard530 marked this pull request as draft July 21, 2026 16:44
@cleonard530
cleonard530 force-pushed the migrate_pybind_to_torch_library branch from 61073b9 to 90d0764 Compare September 4, 2026 19:08
AichenF and others added 7 commits September 8, 2026 15:04
Add SM90 FP8 MegaMoE support

Co-authored-by: Jinyan Chen <jinyanc@nvidia.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
… match what was in the legacy code

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…helper code from generate_pyi.py

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…updates so I updated it to inlcude the type hints

Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
…ybind default arguments are no longer there

Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
…ther minor formatting issues. Also added comments with examples to help describe each step. These will be removed (as well as main()) in a followup commit, but wanted them here for reference

Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
…TORCH_LIBRARY; deep_gemm/_C.py can keep using torch.ops.load_library.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
… function that was leftover from migration, replaces torch_compat with torch/all.h, and updated the sm120 files to use torch/all.h instead of torch/python.h
…_library_macros

- Drop torch_library_macros.hpp; register ops via TORCH_FN directly and rename
  torch_library_utils namespace to torch_utils.
- Use at::ScalarType/torch.dtype for logits_dtype instead of int + _SCALAR_TYPE dict.
- Align schema/wrapper param names with C++ impl (fused_kv_cache,
  activation_clamp_opt, *_tuple/*_tuple_opt), propagated to _C.py, mega/__init__.py,
  and tests.
- Prefix slice_symm_buffer_for_mega_moe with _ to mark it private.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
…o align it with the C++ names

Signed-off-by: Chris Leonard <chleonar@redhat.com>
fp8_fp4_mega_moe and bf16_mega_moe both write into this tensor via a device-side red.add reduction (accumulating per-expert recv counts across calls), but the TORCH_LIBRARY schema declared it as immutable (Tensor?). Fix by annotating it Tensor(cumulative_local_expert_recv_stats!)?.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…filenames on release uploads. Bump the floor to cp310, which every torch version in the matrix (2.4-2.8) already supports.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…as. These were declared optional but the C++ impl always called unconditionally, so would have crashed anyway; the Python wrappers already default to a concrete tuple.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…n by having get_symm_buffer_size_for_mega_moe return the computed layout as an int[] alongside num_bytes, which _slice_symm_buffer_for_mega_moe now reuses instead of rederiving it (and its num_sms-dependent ring sizing) from the original args a second time.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…params like recipe/head_splits, so the schema itself is the source of truth for stub generation instead of a hand-maintained per-name promotion table in generate_pyi.py. Add an _as_int_list wrapper helper so int[N]'s scalar-broadcasting behavior can't silently convert a bad scalar argument into a repeated list.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…le-factor pairs like q/q_sf, replacing the special-cased _apply_q_qsf_merge, and sort the csrc/ file scan so op ordering in the generated stub no longer shuffles across machines/checkouts. Also drop sanitize_param_name, since it could only keep the .pyi syntactically valid and not guarantee the stub's keyword name actually matches the real wrapper at runtime.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…atter for this migration but it makes the migration to torch abi stable easier

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…of silently dropping the whole group, so a future C++ #if drift between grouped ops fails loudly at import with the exact op names involved rather than quietly vanishing from the API. Also document which file each op in the einsum/tf32_hc_prenorm_gemm/get_paged_mqa_logits_metadata group comes from, since they're only bundled by coincidence of a shared guard today.

Signed-off-by: Chris Leonard <chleonar@redhat.com>
- serialize NVFP4-specific symmetric buffer layout fields
- register fp4_fp4_mega_moe and preserve SiTU parameters
- add matching Python operator wrappers

Signed-off-by: Chris Leonard <chleonar@redhat.com>
…jit_kernels/impls/sm100_fp4_fp4_mega_moe.hpp file

Signed-off-by: Chris Leonard <chleonar@redhat.com>
Signed-off-by: Chris Leonard <chleonar@redhat.com>
@cleonard530
cleonard530 force-pushed the migrate_pybind_to_torch_library branch from 90d0764 to 343826c Compare September 9, 2026 14:52
… type is explicit so the make_tuple was unnessesary, and this keeps it consistent. with sm90_mega

Signed-off-by: Chris Leonard <chleonar@redhat.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