Skip to content

Commit dd32f35

Browse files
Copilottianleiwu
andauthored
Fix libcudart.so.13 hard dependency in pybind module breaking import on CPU-only Linux (#29590)
### Description `onnxruntime-gpu` 1.27 introduced a hard `NEEDED libcudart.so.13` entry in `onnxruntime_pybind11_state.so`, causing `ImportError` at `import onnxruntime` on CPU-only Linux machines — before any provider is selected. **Root cause:** `cmake/onnxruntime_python.cmake` was changed to compile `fpA_intB_gemm_adaptor.cu` and `fpA_intB_gemm_preprocessors_impl.cu` directly into `onnxruntime_pybind11_state.so` and link `CUDA::cudart` (dynamic). This embeds a load-time CUDA dependency in the Python module itself. **Fix:** Move the CUDA weight-preprocessing entry point (`pack_weights_for_cuda_mixed_gemm`) out of the main pybind module and into a **standalone extension module**, `onnxruntime_cuda_quant_preprocess`, that links `CUDA::cudart` on its own. The main `onnxruntime_pybind11_state.so` no longer compiles or links any CUDA code, so `import onnxruntime` has no `libcudart` dependency. The new module is imported **lazily** by `onnxruntime/python/tools/quantization/cuda_quantizer.py` only when weight prepacking is actually requested — never at `import onnxruntime` time. These preprocessing APIs are **offline-only** helpers: they are used by quantization tooling and model builders to produce prepacked weight initializers ahead of time, and are not part of the inference runtime hot path. Because nothing in the runtime imports them, isolating them into a separate, on-demand DLL has no runtime cost and cleanly keeps CUDA out of the base `import onnxruntime` path. **Why not the provider bridge:** An earlier iteration routed the call through the `ProviderInfo_CUDA` virtual interface (`TryGetProviderInfo_CUDA()`). That does not work for the CUDA-EP-as-plugin build (`onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON`): `cuda_provider_factory.cc` is excluded from the plugin sources and there is no provider bridge, so `TryGetProviderInfo_CUDA()` returns `nullptr` and the call throws. The standalone module has no such dependency and works for **both** the legacy in-tree CUDA EP build and the plugin build. ### Key Changes | File | Change | |---|---| | `onnxruntime/python/onnxruntime_pybind_cuda_quant.cc` | **New.** Self-contained `pack_weights_for_cuda_mixed_gemm` (device malloc + transpose/convert + arch permutation) and a `PYBIND11_MODULE(onnxruntime_cuda_quant_preprocess, …)` entry point. | | `cmake/onnxruntime_python.cmake` | Add the `onnxruntime_cuda_quant_preprocess` module target (built when `onnxruntime_USE_CUDA AND NOT WIN32`, compiling the two `fpA_intB` `.cu` files + `CUDA::cudart` + cutlass, hidden visibility) and copy it into `onnxruntime/capi/`. Main pybind module keeps no CUDA sources/links. | | `onnxruntime/python/onnxruntime_pybind_quant.cc` | Remove the `USE_CUDA` `PackWeightsForMixedGemm` and its registration. The CPU-only `pack_fp4_weights_for_cuda_moe_gemm` stays in the main module. | | `onnxruntime/core/providers/cuda/cuda_provider_factory.{h,cc}` | Revert the `PackWeightsForMixedGemm` `ProviderInfo_CUDA` addition (no longer needed; absent in plugin builds). | | `onnxruntime/python/tools/quantization/cuda_quantizer.py` | `_get_pack_weights_for_cuda_mixed_gemm()` now imports `onnxruntime.capi.onnxruntime_cuda_quant_preprocess` lazily; add `has_cuda_weight_prepacking()` capability helper. | | `setup.py` | Package `onnxruntime_cuda_quant_preprocess.so` in the Linux/macOS wheels. | | `onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py` | Point the prepacked-weight parity test and its skip guard at the new module. | | `docs/contrib_ops/cuda/matmul_nbits.md` | Update the offline-packer code snippets to import the new module. | ### Motivation and Context `import onnxruntime` must succeed on CPU-only machines even when the GPU wheel is installed. CUDA dependency errors should surface only when a CUDA provider is explicitly loaded/selected, or when offline CUDA weight prepacking is explicitly requested. This restores the 1.26 behavior where `onnxruntime_pybind11_state.so` had no `NEEDED libcudart.so.*` entry, and — unlike the provider-bridge approach — it also works in the CUDA-EP-as-plugin build. ### Testing Notes - Built both modules in the CUDA build; `readelf -d onnxruntime_pybind11_state.so` shows **no** `libcudart` `NEEDED` entry, while `onnxruntime_cuda_quant_preprocess.so` has `NEEDED libcudart.so.13`. - `import onnxruntime` and lazy loading of `onnxruntime.capi.onnxruntime_cuda_quant_preprocess` both succeed; `has_cuda_weight_prepacking()` returns `True` on a CUDA machine. - `test_op_matmulnbits_prepacked_cuda.py` passes (INT4/INT8 prepacked-vs-runtime parity), confirming the relocated packer produces byte-identical prepacked weights. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Tianlei Wu <tlwu@microsoft.com>
1 parent bcc9d30 commit dd32f35

13 files changed

Lines changed: 519 additions & 221 deletions

File tree

cmake/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ cmake_dependent_option(onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS "Build with CUD
7777

7878
cmake_dependent_option(onnxruntime_USE_CUDA_NHWC_OPS "Build CUDA with NHWC op support" ON "onnxruntime_USE_CUDA" OFF)
7979
cmake_dependent_option(onnxruntime_BUILD_CUDA_EP_AS_PLUGIN "Build CUDA EP as a separate plugin shared library instead of the legacy in-tree provider" OFF "onnxruntime_USE_CUDA" OFF)
80+
option(onnxruntime_BUILD_CUDA_QUANT_PREPROCESS "Build CUDA weight-packing module onnxruntime_cuda_quant_preprocess.so" ON)
8081
option(onnxruntime_CUDA_MINIMAL "Build CUDA without any operations apart from memcpy ops. Usefuel for a very minial TRT build" OFF)
8182
option(onnxruntime_ENABLE_CUDA_LINE_NUMBER_INFO "When building with CUDA support, generate device code line number information." OFF)
8283
option(onnxruntime_USE_OPENVINO "Build with OpenVINO support" OFF)

cmake/onnxruntime_python.cmake

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ file(GLOB onnxruntime_pybind_srcs CONFIGURE_DEPENDS
2020
${onnxruntime_pybind_srcs_pattern}
2121
)
2222

23+
# onnxruntime_pybind_cuda_quant.cc is compiled into the standalone
24+
# onnxruntime_cuda_quant_preprocess extension module (see below), not into
25+
# onnxruntime_pybind11_state. It includes <cuda_runtime.h> and links CUDA::cudart,
26+
# so compiling it into the main pybind module would break CPU-only builds and
27+
# re-introduce the hard libcudart dependency this design avoids.
28+
list(REMOVE_ITEM onnxruntime_pybind_srcs ${ONNXRUNTIME_ROOT}/python/onnxruntime_pybind_cuda_quant.cc)
29+
2330
if(onnxruntime_ENABLE_TRAINING)
2431
list(REMOVE_ITEM onnxruntime_pybind_srcs ${ONNXRUNTIME_ROOT}/python/onnxruntime_pybind_module.cc)
2532
endif()
@@ -231,22 +238,11 @@ target_link_libraries(onnxruntime_pybind11_state PRIVATE
231238
Python::NumPy
232239
)
233240

234-
# Starting with Python 3.8 on Windows, PATH environment variable are no longer used to resolve DLL dependencies
235-
# for extension modules or libraries loaded via ctypes.
236-
# To avoid package import issues, we do not link pybind module against the CUDA runtime on Windows, instead of
237-
# os.add_dll_directory() to deal with CUDA paths.
238-
if (onnxruntime_USE_CUDA AND NOT WIN32)
239-
target_sources(onnxruntime_pybind11_state PRIVATE
240-
"${ONNXRUNTIME_ROOT}/contrib_ops/cuda/llm/fpA_intB_gemm_adaptor.cu"
241-
"${ONNXRUNTIME_ROOT}/contrib_ops/cuda/llm/fpA_intB_gemm_preprocessors_impl.cu"
242-
)
243-
include(cutlass)
244-
target_include_directories(onnxruntime_pybind11_state PRIVATE ${cutlass_SOURCE_DIR}/include)
245-
target_link_libraries(onnxruntime_pybind11_state PRIVATE CUDA::cudart)
246-
endif()
247-
if (onnxruntime_USE_CUDA AND WIN32)
248-
target_compile_definitions(onnxruntime_pybind11_state PRIVATE ORT_NO_CUDA_IN_PYBIND)
249-
endif()
241+
# The CUDA quantization helpers (pack_weights_for_cuda_mixed_gemm) are built into a
242+
# separate extension module (onnxruntime_cuda_quant_preprocess) that is imported on
243+
# demand. Do NOT compile CUDA source files directly into onnxruntime_pybind11_state or
244+
# link CUDA::cudart from it: that would create a hard libcudart.so dependency that
245+
# prevents importing the Python module on CPU-only machines.
250246

251247
set(onnxruntime_pybind11_state_dependencies
252248
${onnxruntime_EXTERNAL_DEPENDENCIES}
@@ -315,6 +311,71 @@ else()
315311
set_target_properties(onnxruntime_pybind11_state PROPERTIES SUFFIX ".so")
316312
endif()
317313

314+
# ---------------------------------------------------------------------------
315+
# Standalone CUDA weight-preprocessing extension module.
316+
#
317+
# The CUDA weight-packing kernels (pack_weights_for_cuda_mixed_gemm) are compiled
318+
# into their OWN Python extension module instead of onnxruntime_pybind11_state.
319+
# This keeps the hard libcudart dependency out of the main pybind module so that
320+
# `import onnxruntime` still works on CPU-only machines.
321+
#
322+
# Production weight packing is done in PyTorch (cuda_quantizer.py); this module is
323+
# retained only as a byte-parity oracle for that PyTorch packer. It is gated by
324+
# onnxruntime_BUILD_CUDA_QUANT_PREPROCESS.
325+
#
326+
# It does NOT go through the provider bridge / ProviderInfo_CUDA, so it works for
327+
# both the legacy in-tree CUDA EP build and the CUDA-EP-as-plugin build.
328+
#
329+
# Not built on Windows: matching the previous behavior where CUDA runtime was not
330+
# linked into Python extension modules (DLL search path constraints since
331+
# Python 3.8), so pack_weights_for_cuda_mixed_gemm was unavailable there.
332+
if (onnxruntime_USE_CUDA AND NOT WIN32 AND onnxruntime_BUILD_CUDA_QUANT_PREPROCESS)
333+
onnxruntime_add_shared_library_module(onnxruntime_cuda_quant_preprocess
334+
"${ONNXRUNTIME_ROOT}/python/onnxruntime_pybind_cuda_quant.cc"
335+
"${ONNXRUNTIME_ROOT}/contrib_ops/cuda/llm/fpA_intB_gemm_adaptor.cu"
336+
"${ONNXRUNTIME_ROOT}/contrib_ops/cuda/llm/fpA_intB_gemm_preprocessors_impl.cu"
337+
)
338+
include(cutlass)
339+
onnxruntime_add_include_to_target(onnxruntime_cuda_quant_preprocess Python::Module onnxruntime_common)
340+
target_include_directories(onnxruntime_cuda_quant_preprocess PRIVATE
341+
${ONNXRUNTIME_ROOT}
342+
${pybind11_INCLUDE_DIRS}
343+
${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}
344+
${cutlass_SOURCE_DIR}/include
345+
${cutlass_SOURCE_DIR}/tools/util/include
346+
)
347+
target_compile_definitions(onnxruntime_cuda_quant_preprocess PRIVATE USE_CUDA)
348+
target_link_libraries(onnxruntime_cuda_quant_preprocess PRIVATE
349+
onnxruntime_common
350+
Boost::mp11
351+
safeint_interface
352+
${ABSEIL_LIBS}
353+
CUDA::cudart
354+
${pybind11_lib}
355+
Python::NumPy
356+
)
357+
if (NOT MSVC)
358+
target_compile_options(onnxruntime_cuda_quant_preprocess PRIVATE "-fvisibility=hidden")
359+
endif()
360+
set_target_properties(onnxruntime_cuda_quant_preprocess PROPERTIES PREFIX "" SUFFIX ".so" FOLDER "ONNXRuntime")
361+
if (APPLE)
362+
set_target_properties(onnxruntime_cuda_quant_preprocess PROPERTIES
363+
INSTALL_RPATH "@loader_path"
364+
BUILD_WITH_INSTALL_RPATH TRUE
365+
INSTALL_RPATH_USE_LINK_PATH FALSE)
366+
elseif (NOT CMAKE_SYSTEM_NAME MATCHES "AIX")
367+
target_link_options(onnxruntime_cuda_quant_preprocess PRIVATE "LINKER:-rpath=\$ORIGIN")
368+
endif()
369+
# Place the module next to the main pybind module inside onnxruntime/capi.
370+
add_custom_command(
371+
TARGET onnxruntime_cuda_quant_preprocess POST_BUILD
372+
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:onnxruntime_common>/onnxruntime/capi
373+
COMMAND ${CMAKE_COMMAND} -E copy
374+
$<TARGET_FILE:onnxruntime_cuda_quant_preprocess>
375+
$<TARGET_FILE_DIR:onnxruntime_common>/onnxruntime/capi/
376+
)
377+
endif()
378+
318379
# Generate version_info.py in Windows build.
319380
# Has to be done before onnxruntime_python_srcs is set.
320381
if (WIN32)

docs/contrib_ops/cuda/matmul_nbits.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ step is **not** performed.
8787
The offline CUDA packer exposed through Python produces this layout:
8888

8989
```python
90-
from onnxruntime.capi import _pybind_state as _pybind
90+
from onnxruntime.capi import onnxruntime_cuda_quant_preprocess as _cuda_quant
9191

92-
prepacked_flat = _pybind.pack_weights_for_cuda_mixed_gemm(
92+
prepacked_flat = _cuda_quant.pack_weights_for_cuda_mixed_gemm(
9393
q_weight.reshape(N, -1), N, K, bits, 80
9494
)
9595
prepacked_b = np.asarray(prepacked_flat, dtype=np.int8).view(np.uint8).reshape(q_weight.shape)
@@ -309,7 +309,7 @@ present. `ComputeInternal` then:
309309
GEMV profiling helpers, e.g. `profile_qmoe_gemv.sh`).
310310
- CUDA prepacked-weight parity tests:
311311
[onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py](../../../onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py).
312-
These use `_pybind_state.pack_weights_for_cuda_mixed_gemm(..., 80)` to produce
312+
These use `onnxruntime_cuda_quant_preprocess.pack_weights_for_cuda_mixed_gemm(..., 80)` to produce
313313
`weight_prepacked=1` initializers and compare their outputs against runtime
314314
fpA_intB prepacking for int4/int8 and GEMV/GEMM-shaped `M` values.
315315
- Constructor failure tests for unsupported prepacked configurations live in

docs/cuda_plugin_ep/QUICK_START.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ sess = ort.InferenceSession(
146146

147147
**Python `OrtValue` host/device copies:**
148148

149-
`OrtValue.update_inplace()` and `OrtValue.numpy()` work with CUDA plugin tensors after the plugin has been registered. On Linux, the ONNX Runtime Python binding links the CUDA runtime and can fall back to direct `cudaMemcpy` if the legacy CUDA provider bridge is unavailable. On Windows, the Python binding is built with `ORT_NO_CUDA_IN_PYBIND`, so it cannot call CUDA runtime APIs directly; host/device copies must use the data-transfer implementation registered by the CUDA plugin library. If `OrtValue.update_inplace()` fails with a message about the CUDA provider interface or an unsupported GPU device, verify that the plugin library is registered before creating or updating CUDA `OrtValue` objects.
149+
`OrtValue.update_inplace()` and `OrtValue.numpy()` work with CUDA plugin tensors after the plugin has been registered. The Python binding cannot call CUDA runtime APIs directly; host/device copies must use the data-transfer implementation registered by the CUDA plugin library. If `OrtValue.update_inplace()` fails with a message about the CUDA provider interface or an unsupported GPU device, verify that the plugin library is registered before creating or updating CUDA `OrtValue` objects.
150150

151151
### External GPU Allocator Options
152152

docs/cuda_plugin_ep/cuda_plugin_ep_design.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,12 @@ This is intentionally conservative and correct for the plugin EP's first sync in
344344

345345
The Python `OrtValue` helpers (`update_inplace()` for host-to-device and `numpy()` for device-to-host) historically reached CUDA copies through the legacy provider bridge (`GetProviderInfo_CUDA()`). That bridge requires the provider shared library to export `GetProvider()`, which the CUDA plugin intentionally does not export.
346346

347-
The fallback path is platform-specific:
347+
To keep working when the bridge is absent (as with the plugin EP), the pybind can reach CUDA copies two ways: the legacy provider bridge (`TryGetProviderInfo_CUDA()`) and a plugin-registered `OrtDataTransfer` copy function (`CreateDataTransferMemCpy()`, backed by the plugin EP's `IDataTransfer`). It tries whichever is available and throws if neither is, in which case a CUDA `OrtValue` copy cannot be performed.
348348

349-
- On non-Windows CUDA builds, `onnxruntime_pybind11_state` links `CUDA::cudart`. If `TryGetProviderInfo_CUDA()` fails, pybind can copy directly with `cudaMemcpy`; host-to-device copies synchronize the default stream, matching `ProviderInfo_CUDA::cudaMemcpy_HostToDevice()`.
350-
- On Windows CUDA builds, pybind is compiled with `ORT_NO_CUDA_IN_PYBIND` and does not link CUDA runtime APIs. If `TryGetProviderInfo_CUDA()` fails, pybind must obtain an `OrtDataTransfer` copy function from the registered plugin EP. Without a registered plugin data-transfer implementation, CUDA `OrtValue.update_inplace()` cannot copy host data into the plugin-owned device tensor.
349+
The two code paths differ only in which mechanism they try first, and this does not change the outcome (exactly one applies in a given build):
350+
351+
- `OrtValue.update_inplace(numpy_array)` / `OrtValue.numpy()` (in `onnxruntime_pybind_ortvalue.cc`) try the provider bridge first, then fall back to the plugin `OrtDataTransfer`.
352+
- `OrtValue.update_inplace(OrtValue)` (`UpdateOrtValueInplace` in `onnxruntime_pybind_mlvalue.cc`) tries the plugin `OrtDataTransfer` first, then falls back to the built-in CUDA provider copy functions.
351353

352354
### 5.2 Handle Access Path
353355

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// Standalone CUDA weight-preprocessing extension module.
5+
//
6+
// This module is intentionally kept SEPARATE from onnxruntime_pybind11_state so
7+
// that `import onnxruntime` never triggers a load-time dependency on the CUDA
8+
// runtime (libcudart). The CUDA weight packing kernels below link CUDA::cudart,
9+
// so this module has a hard libcudart dependency -- but it is imported lazily by
10+
// onnxruntime.python.tools.quantization.cuda_quantizer only when CUDA weight
11+
// prepacking is actually requested.
12+
//
13+
// This approach works for both the legacy in-tree CUDA EP build and the
14+
// CUDA-EP-as-plugin build (onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON), because it
15+
// does not rely on the provider bridge / ProviderInfo_CUDA interface (which is
16+
// not available in plugin builds).
17+
18+
#include <pybind11/pybind11.h>
19+
#include <pybind11/numpy.h>
20+
21+
#include <cuda_runtime.h>
22+
23+
#include <memory>
24+
#include <sstream>
25+
#include <stdexcept>
26+
27+
#include "contrib_ops/cuda/llm/fpA_intB_gemm_adaptor.h"
28+
#include "contrib_ops/cuda/llm/fpA_intB_gemm_preprocessors.h"
29+
30+
namespace py = pybind11;
31+
32+
namespace {
33+
34+
void ThrowIfCudaError(cudaError_t status, const char* expression) {
35+
if (status != cudaSuccess) {
36+
std::ostringstream oss;
37+
oss << expression << " failed: " << cudaGetErrorString(status);
38+
throw std::runtime_error(oss.str());
39+
}
40+
}
41+
42+
struct CudaDeleter {
43+
void operator()(void* p) const {
44+
if (p) cudaFree(p);
45+
}
46+
};
47+
48+
using CudaPtr = std::unique_ptr<void, CudaDeleter>;
49+
50+
// Preprocess quantized weights for CUDA mixed-precision GEMM kernels (FpA_IntB format).
51+
//
52+
// MatMulNBits/QMoE stores quantized weights in (N, K) layout:
53+
// - N = number of output channels (columns in weight matrix W)
54+
// - K = number of input features (rows in weight matrix W)
55+
// - For 4-bit: shape is (N, K/2) bytes where each byte packs 2 elements
56+
// - For 8-bit: shape is (N, K) bytes
57+
//
58+
// FpA_IntB GEMM kernels expect weights in (K, N) layout (transposed) for efficient
59+
// memory access during matrix multiplication. This function:
60+
// 1. Transposes from (N, K) to (K, N) layout
61+
// 2. Converts unsigned quantized values to signed int8 with zero-point adjustment
62+
// - 4-bit: uint4 -> int8 with zero_point=8 (range [0,15] -> [-8,7])
63+
// - 8-bit: uint8 -> int8 with zero_point=128 (range [0,255] -> [-128,127])
64+
// 3. Applies architecture-specific row permutation for optimized tensor core access
65+
//
66+
// Input: q_weights - Quantized weights from MatMulNBits in (N, K) layout
67+
// Output: Preprocessed weights in (K, N) layout ready for fpA_intB GEMM kernels
68+
py::array_t<int8_t> PackWeightsForMixedGemm(
69+
py::array_t<uint8_t> q_weights,
70+
int32_t N,
71+
int32_t K,
72+
int32_t bits,
73+
int32_t force_arch = -1) {
74+
py::buffer_info q_weights_buf = q_weights.request();
75+
76+
if (bits != 4 && bits != 8) {
77+
throw std::invalid_argument("bits must be 4 or 8");
78+
}
79+
if (N <= 0 || K <= 0) {
80+
throw std::invalid_argument("N and K must be positive");
81+
}
82+
if (bits == 4 && K % 2 != 0) {
83+
throw std::invalid_argument("K must be even for 4-bit packed weights");
84+
}
85+
if (q_weights_buf.ndim != 2 || q_weights_buf.shape[0] != N || q_weights_buf.shape[1] != K / (8 / bits)) {
86+
throw std::invalid_argument("q_weights must have shape (N, K / (8 / bits))");
87+
}
88+
89+
int n = static_cast<int>(N);
90+
int k = static_cast<int>(K);
91+
92+
size_t packed_weight_bytes = static_cast<size_t>(n) * static_cast<size_t>(k) / (8 / bits);
93+
py::array_t<int8_t> processed_weights({static_cast<pybind11::ssize_t>(packed_weight_bytes)});
94+
py::buffer_info processed_weights_buf = processed_weights.request();
95+
96+
auto make_cuda_ptr = [](size_t bytes) -> CudaPtr {
97+
void* p = nullptr;
98+
ThrowIfCudaError(cudaMalloc(&p, bytes), "cudaMalloc");
99+
return CudaPtr(p);
100+
};
101+
102+
auto packed_transposed_weight_space = make_cuda_ptr(packed_weight_bytes);
103+
int8_t* packed_transposed_weight = reinterpret_cast<int8_t*>(packed_transposed_weight_space.get());
104+
105+
auto fpA_intB_weight_buffer_ = make_cuda_ptr(packed_weight_bytes);
106+
int8_t* preprocessed_weight = reinterpret_cast<int8_t*>(fpA_intB_weight_buffer_.get());
107+
108+
const uint8_t* blob_data_cpu = static_cast<const uint8_t*>(q_weights_buf.ptr);
109+
110+
auto blob_data_gpu_buf = make_cuda_ptr(packed_weight_bytes);
111+
uint8_t* blob_data_gpu = reinterpret_cast<uint8_t*>(blob_data_gpu_buf.get());
112+
113+
cudaStream_t stream = cudaStreamLegacy;
114+
ThrowIfCudaError(cudaMemcpyAsync(blob_data_gpu, blob_data_cpu, packed_weight_bytes, cudaMemcpyHostToDevice, stream),
115+
"cudaMemcpyAsync host-to-device");
116+
117+
if (bits == 4) {
118+
::onnxruntime::llm::kernels::fpA_intB_gemv::unpack_uint4_transposed_to_int8_direct_cuda(
119+
stream, packed_transposed_weight, blob_data_gpu, n, k);
120+
} else {
121+
// 8 bits
122+
::onnxruntime::llm::kernels::fpA_intB_gemv::transpose_uint8_matrix_and_convert_to_int8(
123+
stream, packed_transposed_weight, blob_data_gpu, n, k);
124+
}
125+
126+
using ::onnxruntime::llm::kernels::weight_only::QuantType;
127+
QuantType quant_type = bits == 4 ? QuantType::W4_A16 : QuantType::W8_A16;
128+
129+
int sm = force_arch;
130+
if (sm < 0) {
131+
int device_id = 0;
132+
ThrowIfCudaError(cudaGetDevice(&device_id), "cudaGetDevice");
133+
cudaDeviceProp device_prop;
134+
ThrowIfCudaError(cudaGetDeviceProperties(&device_prop, device_id), "cudaGetDeviceProperties");
135+
sm = device_prop.major * 10 + device_prop.minor;
136+
}
137+
sm = ::onnxruntime::llm::kernels::weight_only::get_arch_for_mixed_gemm_weight_preprocess(sm);
138+
139+
auto permutation_map_buffer = make_cuda_ptr(32 * sizeof(int32_t));
140+
141+
::onnxruntime::llm::kernels::weight_only::preprocess_weights_for_mixed_gemm_cuda(
142+
stream,
143+
sm,
144+
preprocessed_weight,
145+
packed_transposed_weight,
146+
reinterpret_cast<int32_t*>(permutation_map_buffer.get()),
147+
{static_cast<size_t>(k), static_cast<size_t>(n)},
148+
quant_type);
149+
150+
ThrowIfCudaError(cudaGetLastError(), "preprocess CUDA kernel launch");
151+
ThrowIfCudaError(cudaMemcpyAsync(processed_weights_buf.ptr, preprocessed_weight, packed_weight_bytes,
152+
cudaMemcpyDeviceToHost, stream),
153+
"cudaMemcpyAsync device-to-host");
154+
ThrowIfCudaError(cudaStreamSynchronize(stream), "cudaStreamSynchronize");
155+
156+
return processed_weights;
157+
}
158+
159+
} // namespace
160+
161+
PYBIND11_MODULE(onnxruntime_cuda_quant_preprocess, m) {
162+
m.doc() = "CUDA weight-only quantization preprocessing helpers (loaded on demand).";
163+
m.def("pack_weights_for_cuda_mixed_gemm", &PackWeightsForMixedGemm,
164+
"Pack quantized weights for CUDA mixed-precision GEMM (FpA_IntB format)",
165+
py::arg("q_weights"), py::arg("N"), py::arg("K"), py::arg("bits"), py::arg("force_arch") = -1);
166+
}

onnxruntime/python/onnxruntime_pybind_mlvalue.cc

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@
2323
#include "core/framework/kernel_registry.h"
2424
#include "core/framework/provider_options_utils.h"
2525

26-
#if defined(USE_CUDA) && !defined(ORT_NO_CUDA_IN_PYBIND)
27-
#include <cuda_runtime_api.h>
28-
#endif
29-
3026
#ifdef USE_DML
3127
using Microsoft::WRL::ComPtr;
3228

@@ -184,35 +180,14 @@ int32_t GetTensorProtoType(const OrtValue& ort_value) {
184180
}
185181

186182
#ifdef USE_CUDA
187-
namespace {
188-
189-
#if !defined(ORT_NO_CUDA_IN_PYBIND)
190-
void CudaRuntimeMemCpy(void* dst, const void* src, size_t num_bytes, cudaMemcpyKind kind) {
191-
const auto copy_result = cudaMemcpy(dst, src, num_bytes, kind);
192-
ORT_ENFORCE(copy_result == cudaSuccess, "cudaMemcpy failed: ", cudaGetErrorString(copy_result));
193-
194-
if (kind == cudaMemcpyHostToDevice) {
195-
// Match ProviderInfo_CUDA::cudaMemcpy_HostToDevice: cudaMemcpy() uses the default
196-
// stream, and pageable host-to-device copies can return before DMA to device is done.
197-
const auto sync_result = cudaStreamSynchronize(0);
198-
ORT_ENFORCE(sync_result == cudaSuccess, "cudaStreamSynchronize failed: ", cudaGetErrorString(sync_result));
199-
}
200-
}
201-
#endif
202-
203-
} // namespace
204183

205184
void CpuToCudaMemCpy(void* dst, const void* src, size_t num_bytes) {
206185
if (TryGetProviderInfo_CUDA() != nullptr) {
207186
GetProviderInfo_CUDA().cudaMemcpy_HostToDevice(dst, src, num_bytes);
208187
return;
209188
}
210189

211-
#if !defined(ORT_NO_CUDA_IN_PYBIND)
212-
CudaRuntimeMemCpy(dst, src, num_bytes, cudaMemcpyHostToDevice);
213-
#else
214190
ORT_THROW("CUDA provider interface is not available for host-to-device copy.");
215-
#endif
216191
}
217192

218193
void CudaToCpuMemCpy(void* dst, const void* src, size_t num_bytes) {
@@ -221,11 +196,7 @@ void CudaToCpuMemCpy(void* dst, const void* src, size_t num_bytes) {
221196
return;
222197
}
223198

224-
#if !defined(ORT_NO_CUDA_IN_PYBIND)
225-
CudaRuntimeMemCpy(dst, src, num_bytes, cudaMemcpyDeviceToHost);
226-
#else
227199
ORT_THROW("CUDA provider interface is not available for device-to-host copy.");
228-
#endif
229200
}
230201

231202
const std::unordered_map<OrtDevice, MemCpyFunc>* GetCudaToHostMemCpyFunction(const OrtDevice& device) {

0 commit comments

Comments
 (0)