Migrate pybind11 to TORCH_LIBRARY - #393
Conversation
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>
| def adjust_for_c_py_wrapper( | ||
| name: str, | ||
| parameters: list[dict], | ||
| wrapper_defaults: dict[str, dict[str, str]] | None = None, |
There was a problem hiding this comment.
🔴 critical: Defer evaluation of modern type annotations: On supported Python 3.9, setup.py imports this module and immediately raises TypeError because | unions require Python 3.10; Python 3.8 fails earlier on annotations such as list[str]. This prevents installation and wheel builds for two versions still present in the release matrix; add postponed annotation evaluation or use compatible typing forms.
🤖 v6
| }, | ||
| ext_modules=get_ext_modules(), | ||
| zip_safe=False, | ||
| options={'bdist_wheel': {'py_limited_api': 'cp39'}}, |
There was a problem hiding this comment.
🔴 critical: Align the abi3 tag with the release matrix: With the existing per-Python release matrix, every Python 3.9+ job for a given Torch/ABI combination now produces the same cp39-abi3 wheel filename, so concurrent release uploads collide and fail; Python 3.8 cannot produce this tag at all. Collapse the matrix to one compatible Python build per binary combination or retain per-version wheel tags.
🤖 v6
| const torch::Tensor& b, const torch::Tensor& sfb, | ||
| const torch::Tensor& d, const c10::optional<torch::Tensor>& c, | ||
| const c10::optional<c10::List<int64_t>>& recipe) { | ||
| einsum::fp8_einsum(expr, {a, sfa}, {b, sfb}, d, c, list_to_tuple3(recipe.value())); |
There was a problem hiding this comment.
🔵 suggestion: fp8_einsum's TORCH_LIBRARY impl calls list_to_tuple3(recipe.value()) unconditionally, but the schema declares int[]? recipe=None. Calling the raw op torch.ops.deep_gemm.fp8_einsum(..., recipe=None) (or omitting it, since the schema default is None) will throw from c10::optional::value(). In practice the deep_gemm/_C.py wrapper always supplies a recipe (default (1, 128, 128), passing list(recipe)), so the public path is safe and this matches the pre-migration behavior. Consider either dropping the ?/default from the schema to make the contract explicit, or guarding with recipe.has_value() and asserting a clear error, so direct torch.ops callers get a meaningful message rather than a bad-optional-access.
🤖 v3
| 'fp8_paged_mqa_logits': fp8_paged_mqa_logits, | ||
| }) | ||
|
|
||
| # DG_TENSORMAP_COMPATIBLE — gemm.hpp (BF16 impl conditional) |
There was a problem hiding this comment.
🔵 suggestion: _bind_guarded_ops groups 'einsum', 'tf32_hc_prenorm_gemm', and 'get_paged_mqa_logits_metadata' as one guard group. This is currently correct (all three are under #if DG_FP8_COMPATIBLE and DG_TENSORMAP_COMPATIBLE in einsum.hpp/hyperconnection.hpp/attention.hpp), but the grouping relies on an implicit cross-file invariant that isn't obvious from the Python side. Since _bind_guarded_ops is all-or-nothing, if any one of these later moves to a different guard the whole group silently fails to bind. Consider adding a short comment noting the three source files/guard each name comes from (as done for the mega/layout groups) to make the coupling auditable.
🤖 v3
| m.def( | ||
| "get_symm_buffer_size_for_mega_moe(int num_ranks, int num_experts, int num_max_tokens_per_rank, int num_topk, int hidden, int intermediate_hidden, str mma_type, str activation, int num_shared_experts=0) -> int", | ||
| TORCH_FN(deep_gemm::torch_registration::get_symm_buffer_size_for_mega_moe)); | ||
| m.def( |
There was a problem hiding this comment.
🔵 suggestion: _slice_symm_buffer_for_mega_moe and get_symm_buffer_size_for_mega_moe both call build_symm_buffer_layout independently, so the size op and the slice op each recompute the full layout (including the num_sms-dependent ring sizing). SymmBuffer.init calls both back-to-back, doubling the layout computation per buffer allocation. This is functionally fine and matches the stated 'compute layout once per kernel launch' goal within each op, but if buffer construction ever becomes hot it may be worth exposing a single op that returns both size and slices. Non-blocking.
🤖 v3
There was a problem hiding this comment.
Fixed: get_symm_buffer_size_for_mega_moe now returns (int64_t, int[]), where the int[] is the fully-computed SymmBufferLayoutInfo flattened via a new to_int_list()/from_int_list() pair. _slice_symm_buffer_for_mega_moe's schema changed to (Tensor buffer, int[] layout_info), so it reconstructs the layout directly from what was already computed instead of calling build_symm_buffer_layout a second time. SymmBuffer.__init__ computes the layout exactly once per allocation now.
| num_ranks, num_experts, num_max_tokens_per_rank, num_topk, | ||
| hidden, intermediate_hidden, mma_type, activation, num_shared_experts).num_bytes; | ||
| } | ||
|
|
There was a problem hiding this comment.
🔵 suggestion: build_symm_buffer_layout notes 'we temporarily assume the SM count is consistent with the runtime value' and derives num_sms from device_runtime->get_num_sms(). Because get_symm_buffer_size_for_mega_moe and _slice_symm_buffer_for_mega_moe are now separate ops called at different times (allocation vs. slicing) and the mega_moe kernel entry builds its own layout at launch, an intervening deep_gemm.set_num_sms() between SymmBuffer construction and the kernel call could make the sizing/slicing layout diverge from the launch-time layout. The kernel does assert sym_buffer.nbytes() >= layout_info.num_bytes, which catches undersizing, but the slice views captured in SymmBuffer would be stale. Worth documenting the assumption that num_sms must not change across a SymmBuffer's lifetime.
🤖 v3
There was a problem hiding this comment.
This divergence risk existed before this PR. num_sms is expected to stay constant for the lifetime of a process/run in practice, and a proper fix (caching the layout across launches, or asserting num_sms hasn't drifted) is more involved than this PR's scope. Non-blocking, as noted.
| def sanitize_param_name(name: str) -> str: | ||
| if name in {'def', 'class', 'from', 'import', 'None', 'True', 'False'}: | ||
| return f'{name}_' | ||
| return name |
There was a problem hiding this comment.
🔵 suggestion: _promote_int_list_tuple_types hardcodes per-parameter-name promotions (head_splits -> tuple[int,int,int], recipe -> tuple[int,int,int], recipe for transform_sf_into_required_layout -> tuple[int,int] | tuple[int,int,int], etc.). These implicit name-based rules must be kept in sync by hand with both the TORCH_LIBRARY schemas and the _C.py wrapper unpacking logic; a future schema rename (e.g. a new recipe-like arg) will silently fall back to list[int] in the stub without any warning. Consider driving these promotions from the _C.py wrapper signatures/annotations (which are already AST-parsed for defaults) rather than a hardcoded name table, to reduce the risk of stub/runtime drift like the fused_kv_cache/kv_cache bug this MR set out to eliminate.
🤖 v3
🤖 ds-review-bot Code Reviewv6The migration breaks supported Python 3.8/3.9 builds and makes the current release matrix generate conflicting stable-ABI wheel assets. v4v3This MR migrates DeepGEMM's C++ extension from pybind11 (PYBIND11_MODULE + per-header register_apis) to TORCH_LIBRARY / TORCH_LIBRARY_IMPL registration. Ops now self-register as torch.ops.deep_gemm.* via TORCH_LIBRARY_FRAGMENT/TORCH_LIBRARY_IMPL static initializers in each csrc/apis/.hpp. A new pure-Python deep_gemm/_C.py shim loads the renamed _C_extension.so (built as limited-API/abi3), re-exports the legacy C surface, and restores pybind-era conveniences (tuple unpacking, default args, guarded op binding). The overall design is sound and consistent: all 7 apis headers use the new registration pattern, no pybind11/register_apis/py::arg references remain, the empty PyInit symbol keeps the .so importable, and the mega MoE symm-buffer sizing/slicing is cleanly split into get_symm_buffer_size_for_mega_moe (int) + _slice_symm_buffer_for_mega_moe, both consumed correctly by deep_gemm/mega/init.py's SymmBuffer. The build (setup.py), stub generation (scripts/generate_pyi.py, now parsing TORCH_LIBRARY schemas + _C.py AST defaults), and init.py wiring are all consistent. The #if guard groups in _C.py's _bind_guarded_ops calls correctly match the C++ conditional-compilation guards (verified for DG_TENSORMAP_COMPATIBLE and DG_FP8_COMPATIBLE and DG_TENSORMAP_COMPATIBLE groups). No pybind11/register_apis/py::arg references remain anywhere in csrc/, and mega/init.py correctly consumes the split size/slice ops. I found no blocking defects; the notes below are minor robustness/maintainability suggestions, not correctness regressions relative to the pybind baseline. Files reviewed: 36 |
…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>
Summary
Migrates DeepGEMM's C++ extension from pybind11 (
PYBIND11_MODULE+ per-headerregister_apis) toTORCH_LIBRARY/TORCH_LIBRARY_IMPLregistration. Ops are now registered astorch.ops.deep_gemm.*.To preserve the existing Python API (
import deep_gemm; deep_gemm._C.fp8_gemm_nt(...)), a newdeep_gemm/_C.pyshim loads the extension and re-exports the legacy surface. The compiled module is renamed todeep_gemm._C_extensionand built as a limited-API (abi3) extension.Also updates
.pyistub generation to readTORCH_LIBRARYschemas fromcsrc/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).Note: Bumped the abi3 floor to Python 3.10 which is supported by every torch release already in the build matrix (2.4–2.8), and it gets ahead of a floor bump we'll need once DeepGEMM moves to the LibTorch Stable ABI, which requires torch 2.10+ and Python 3.10+.
PR vllm-project/vllm#48962 is used to test this on vLLM.
WARNING:
Before,
get_symm_buffer_size_for_mega_moereturned anintand afunction(std::tuple<int64_t, std::function...>) butTORCH_LIBRARYdoesn't allow returning function objects/closures. Now, it returns anintand the computed buffer layout as anint[](std::tuple<int64_t, c10::List<int64_t>>), and a separate function,_slice_symm_buffer_for_mega_moe, consumes thatint[]instead of the closure. This breaks backwards compatibility forget_symm_buffer_size_for_mega_moe, but this method is not re-exported fromdeep_gemm/__init__.py(only used internally bySymmBuffer.__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._Cmodule. Responsibilities:torch.ops.load_library(...)on_C_extension*.soat import timeRe-export
torch.ops.deep_gemm.*under the legacy_CnamesRestore pybind-era conveniences that are not expressible in
TORCH_LIBRARYschemas alone: tuple unpacking (q/kv/(weight, scale)pairs), default arguments, and guarded op binding for#if DG_*_COMPATIBLEbuild variantsPreviously
_Cwas the compiled.soitself; now_Cis pure Python and the.sois_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>forrecipe/head_splits, and optional list →std::vector<int>. Without this, everyTORCH_LIBRARYwrapper would duplicate the same list/tuple parsing logic.csrc/utils/registration.h
Defines the REGISTER_EXTENSION(NAME) macro used by csrc/python_api.cpp. Under TORCH_LIBRARY, ops self-register via static initializers when the .so loads, so there's no real pybind11 module to build — this macro just emits an empty PyInit_ function so the compiled .so still satisfies Python's import machinery (which requires that symbol to exist).
Significantly changed files
csrc/apis/mega.hpp
build_symm_buffer_layout, which computes the fullSymmBufferLayoutInfo(every sub-buffer's offset/size) once.get_symm_buffer_size_for_mega_moeandfp8_fp4_mega_moe/bf16_mega_moecall 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.SymmBufferLayoutInfoalso gainedto_int_list/from_int_listso the layout computed byget_symm_buffer_size_for_mega_moecan be flattened into a plainint[]and handed to_slice_symm_buffer_for_mega_moe, instead of that op recomputing the same layout (including thenum_sms-dependent ring sizing) from scratch.get_symm_buffer_size_for_mega_moeused to return (int64_t,std::function<...>) under pybind;TORCH_LIBRARYcan't return closures. It now returns the int plus the computed layout as anint[], and slicing moved to a separate op,_slice_symm_buffer_for_mega_moe(Tensor buffer, int[] layout_info)(leading underscore since it's an internal implementation detail — onlydeep_gemm/mega/__init__.py'sSymmBuffercalls it, it isn't part of the public API).csrc/python_api.cpp
Replaces the old
PYBIND11_MODULE(...)entry point. Ops now self-register viaTORCH_LIBRARY_FRAGMENT/STABLE_TORCH_LIBRARY_IMPLstatic initializers inside eachcsrc/apis/*.hpp, which run automatically when the.sois loaded — so this file no longer needs to call per-headerregister_apis(m)functions. It just#includeseveryapis/*.hpp(to trigger their static registration) and defines an emptyPyInit_symbol via theREGISTER_EXTENSIONmacro, purely so the compiled.sois still a valid, importable Python extension module — no actual bindings go through the Python C API anymore.scripts/generate_pyi.py
Previously generated
.pyitype hints by regex-parsing the C++ function declarations incsrc/— but under pybind, the real Python-callable keyword names come frompy::arg("...")strings inPYBIND11_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 namedfused_kv_cache, but it's bound aspy::arg("kv_cache"), so the old generated stub advertised a keyword (fused_kv_cache) that would actually raiseTypeErrorif used — the true kwarg iskv_cache. The script now parses TORCH_LIBRARY schema strings fromcsrc/and overlays defaults from an AST parse ofdeep_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
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 a813b41 to see the
smoke_test_all_ops.pyscript 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).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 cleonard530/DeepGEMM#5 .
All test pass!