Repository navigation
Migrate deep gemm to stable abi - #21
Conversation
| namespace deep_gemm { | ||
| static void ensure_jit() { (void)jit.get(); } | ||
| } | ||
| STABLE_TORCH_LIBRARY(deep_gemm, m) { | ||
| m.def("_ensure_jit() -> ()"); | ||
| } | ||
| STABLE_TORCH_LIBRARY_IMPL(deep_gemm, CompositeExplicitAutograd, m) { | ||
| m.impl("_ensure_jit", TORCH_BOX(&deep_gemm::ensure_jit)); |
There was a problem hiding this comment.
JitRuntimeHandle was removed because the PyTorch stable ABI does not support registering custom C++ classes. The Python Runtime wrapper preserves the existing get_jit() API while _ensure_jit() still initializes DeepJIT; no runtime methods are lost because DeepJIT’s Python binding did not expose any.
There was a problem hiding this comment.
One qualification: this preserves the API names and initialization side effect, but not the original handle semantics. Previously, get_jit() returned an object backed by std::shared_ptr<Runtime<Backend>>, which could be retained or passed to another compatible C++ binding. The replacement Runtime object does not contain that C++ handle. I found no current DeepGEMM usage of this behavior, but internal downstream users could potentially depend on it.
There was a problem hiding this comment.
I think what we should document is the highlevel impact of this change. What behavior is held constant and what is now different? This would give maintainers a better picture of how to decide. Also, is it even better to "spoof" the old behavior? We should be as clear as possible about what is changing, and not do extra work to look the same if we cannot promise identical behavior.
| class Runtime: | ||
| """Opaque compatibility type formerly returned by the pybind custom class.""" | ||
|
|
||
|
|
||
| def get_jit() -> Runtime: | ||
| """Initialize and return the opaque runtime handle kept by the legacy API.""" | ||
| _torch_ops._ensure_jit() | ||
| return Runtime() |
There was a problem hiding this comment.
The wrapper to preserve the existing get_jit() API.
The Python Runtime placeholder preserves the legacy API’s surface semantics, but not the original Torch custom-class type identity. It is intentionally empty because DeepJIT exposed no runtime methods through Python.
|
|
||
| // Create the cuBLASLt handle ourselves | ||
| cublasLtHandle_t cublaslt_handle; | ||
| bool use_pytorch_managed_cublaslt_handle; |
There was a problem hiding this comment.
PyTorch does not have a stable ABI equivalent of at::cuda::getCurrentCUDABlasLtHandle() so we remove this option. I have not been able to find any test that use this and by default it is not used, so I don't know of any major impact, but this should be looked at closely.
There was a problem hiding this comment.
Huh I think we should be able to undo this deletion:
Codex says:
torch_get_current_cuda_blas_handle (torch/csrc/stable/c/shim.h:126) exists. The unstable C++ at::cuda::getCurrentCUDABlasLtHandle() does exist, but on CUDA it obtains the ordinary managed cuBLAS handle and casts it.
Therefore DeepGEMM can safely do:
void* handle = nullptr;
TORCH_ERROR_CODE_CHECK(torch_get_current_cuda_blas_handle(&handle));
return reinterpret_cast<cublasLtHandle_t>(handle);
| DG_HOST_ASSERT(not disable_ue8m0_cast); | ||
| const auto broadcasted = gran_mn == 1 ? sf : | ||
| sf.index_select(-2, torch::arange(mn, at::TensorOptions().device(sf.device())).floor_divide_(gran_mn)); | ||
| torch_compat::index_select(sf, -2, torch_compat::floor_divide(deep_gemm_arange_int64(mn, sf.device()), gran_mn)); |
There was a problem hiding this comment.
There is no stable shim for torch arange, so we implemented a deep_gemm kernel for this. This now uses DeepJIT for compilation on the first use.
There was a problem hiding this comment.
We could dispatch to repeat_interleave.Tensor and it would be more tested/stable, but come at the cost of performance after warmup.
| static pybind11::dict get_bf16_mega_gate_config(const int& num_tokens, const int& hidden, | ||
| static std::vector<int64_t> get_bf16_mega_gate_config(const int& num_tokens, const int& hidden, |
There was a problem hiding this comment.
c10::Dict<std::string, int64_t> cannot be used in a stable operator schema. The stable ABI supports int[], so the C++ implementation returns std::vector<int64_t>. The Python wrapper converts that vector back into the public dictionary
| DG_HOST_ASSERT(c10::cuda::currentStreamCaptureStatusMayInitCtx() == c10::cuda::CaptureStatus::None); | ||
| workspace = torch::zeros({kNumWorkspaceBytes}, options.dtype(torch::kByte)); | ||
| DG_HOST_ASSERT(not torch_compat::is_capturing(stream.second)); | ||
| workspace = torch::stable::new_zeros(reference, {kNumWorkspaceBytes}, torch::headeronly::ScalarType::Byte); |
There was a problem hiding this comment.
The new not torch_compat::is_capturing(stream.second) check is more explicit and stable-ABI compatible: it checks capture status on the exact CUDA stream associated with the reference tensor’s device. The previous c10::cuda::currentStreamCaptureStatusMayInitCtx() == c10::cuda::CaptureStatus::None call used an unstable PyTorch API and queried PyTorch’s current stream instead.
059e168 to
8081737
Compare
0bcda86 to
4907ca9
Compare
Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
…table arange method. Fixed merge issues. removed unnecessaary torch_compat::from_blob and megred 3 namespace blocks into one (they were probbaly disjoint before due to Agent iteration) Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
…ion. Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
…M100, and SM120 heuristics to avoid repeated stable ABI lookups. Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
…nging it back to current api name Signed-off-by: Chris Leonard <chleonar@redhat.com>
Co-authored-by: codex Signed-off-by: Chris Leonard <chleonar@redhat.com>
a641d39 to
4ce76bb
Compare
Summary
Migrate DeepGEMM off of the unstable libtorch ABI and onto the torch Stable ABI. Part of the vllm stable abi migration planned outlined in vllm-project/vllm#26946.
This is "stacked" on top of the PR to migrate DeepGEMM off of pybind and onto TORCH_LIBRARY here #15.Design choices
A few methods did not have a direct stable equivelent method, so a few wrapper methods were defined in csrc/utils/torch_compat.hpp. Some of these are just wrappers around
torch_call_dispatcher(a method that allows us to call the aten methods directly through the dispatcher, so no need for external unstable binary symbols), some had to have their input types converted (e.g. from an int to a Tensor), and some where just added to because the current stable shims could not handle const dtypes yet. There is a short comment above each wrapper describing why it is there.torch::arange was handled as a special case since there is no stable equivalent and it could not be wrapped by torch_call_dispatcher. A new method,
deep_gemm_arange_int64, in csrc/jit_kernels/impls/smxx_layout.hpp was added to provide a fast arange function for deep_gemm. This is compiled using DeepJIT so will be slower on the first run thantorch::arange, and is used in thetransform_sf_into_required_layoutmethod in csrc/apis/layout.hpp. A stable wrapper usingrepeat_interleavewas considered but showed to have a non-negligible slowdown compared to this custom kernel.When DeepGEMM still registered it's ops with pybind,
get_jitreturned an emptyRuntimeobject backed by astd::shared_ptr<Runtime<Backend>. No C++ Runtime methods were exposed as Python attributes, so it acted as an opaque handle. This method was replaced with aget_jitmethod that callsjit.get()like it did before (through_ensure_jit) and then wraps the output with a blankRuntimeclass in python (see DeepJIT Runtime comment2). This class was added to preserve API names to support checks such asisinstance(_C.get_jit(), _C.Runtime)but can be removed if not used anywhere. This returned object lost the ability for the method to be used by another binding which accepts astd::shared_ptr<Runtime<Backend>>. Searching open repos on GitHub, I could not find any uses of this object this way (or any calls to this method at all).For
get_bf16_mega_gate_config,STABLE_TORCH_LIBRARYcannot register a dict, so a list was registered instead and the Python compatibility shim (deep_gemm/_C.py) wraps the dict around the list values so that python end users can still use it the same way.In a few locations, the unstable
c10::cuda::CUDAStreamwas replaced with astd::pair<int32_t, cudaStream_t>(StreamKey)since the device info was needed as well as the stream. SinceStreamKeyis not hashable,std::unordered_map<c10::cuda::CUDAStream...were replaced withstd::map<torch_compat::StreamKey.... These are small maps so the difference in runtime should be negligible.In csrc/apis/attention.hpp, we used to have
DG_HOST_ASSERT(c10::cuda::currentStreamCaptureStatusMayInitCtx() == c10::cuda::CaptureStatus::None);which doesn't have a stable equivalent. It was replaced withnot torch_compat::is_capturing(stream.second)which checks capture status on the exact CUDA stream associated with the reference tensor’s device, instead of PyTorch’s current stream.csrc/apis/epilogue_class.hpp used to register anm.class_<Epilogue>("Epilogue");and multiple methods took inepilogue_class::EpilogueArg& epilogueas an argument.STABLE_TORCH_LIBRARYcan't register classes, so the wrapper to register these ops now takes inint64_t& epilogue_kind,std::optional<double>& epilogue_alpha, andstd::optional<torch::stable::Tensor>& epilogue_sfdand passes them intoepilogue_class::unwrap(epilogue_kind, epilogue_alpha, epilogue_sfd)(e.g.fp8_fp4_gemm_ntingemm.hpparound line 922). Before, in pythonepilogue.Identity,epilogue.Alpha,epilogue.FP8Quantization, andepilogue.BF16StochasticRoundingwere registered throughTORCH_LIBRARYwith something like"epilogue_identity() -> __torch__.torch.classes.deep_gemm.Epilogue". Now, the same calls create a python object_EpilogueSpec, the value is unwrapped in the python method_unpack_epilogue, and is passed into the torch.ops. It's more complicated this way, but gets us around the python torch.ops barrier with only allowed stable types, and since there is no more dispatch overhead, it's slightly faster than before (I think the runtime is still basically noise either way, so IDK how much that matters). The final C++ behavior of theEpilogueclasses are unchanged. What changed is its transport to get there.You can also see these changes in the comments below
Test
Summary: 9/9 passed
RESULT: PASSED
Test that require Blackwell GPUs have not been tested yet. These include
and a few test in the other files.
Torch abi audit (torch-abi-audit) of a vLLM build with this commit:
Making vLLM completely stable!!!
Performance Benchmarks
Link to performance benchmarks: cleonard530/Benchmarks#1
The tldr; most gemm kernels stayed the same
except thethe runtime increase infp8_fp4_gemm_{nn,nt,tn,tt}kernels which had a 10-20% runtime increase (2-6 µs/call). I ran another workload with them in a vLLM-like workload. In eager mode the regression was about the same, but it disappeared with the CUDA-graph run.fp8_fp4_gemm_{nn,nt,tt}went away when the data element_size was cached.fp8_fp4_gemm_tnstill has a slight runtime regression.