Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cmake/onnxruntime_providers_cuda.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,15 @@
if(ORT_HAS_SM90_OR_LATER)
target_compile_options(${target} PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-Xptxas=-w>)
target_compile_options(${target} PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-DCUTLASS_ENABLE_GDC_FOR_SM90=1>)
target_compile_definitions(${target} PRIVATE COMPILE_HOPPER_TMA_GEMMS)
if(NOT MSVC)
# The native SM90 (Hopper) TMA/WGMMA launchers pass CUTLASS TMA descriptor types through
# NVCC-generated host stubs. With CUDA 13 + MSVC those stubs contain 128-byte over-aligned
# by-value formal parameters, which triggers MSVC C2719 ("formal parameter with requested
# alignment of 128 won't be aligned"). Disable the native SM90 fpA_intB (COMPILE_HOPPER_TMA_GEMMS)
# and grouped MoE (COMPILE_HOPPER_TMA_GROUPED_GEMMS) TMA kernels on MSVC; the launcher bodies
# become throwing stubs and the SM80 compatibility path still runs on Hopper at runtime.
# See docs/contrib_ops/cuda/moe_qmoe.md section 14.1.
target_compile_definitions(${target} PRIVATE COMPILE_HOPPER_TMA_GEMMS)
target_compile_definitions(${target} PRIVATE COMPILE_HOPPER_TMA_GROUPED_GEMMS)
endif()
if (MSVC)
Expand Down
9 changes: 8 additions & 1 deletion cmake/onnxruntime_providers_cuda_plugin.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,15 @@ if(ORT_HAS_SM90_OR_LATER)
list(APPEND _cuda_plugin_shared_compile_options
"$<$<COMPILE_LANGUAGE:CUDA>:-Xptxas=-w>"
"$<$<COMPILE_LANGUAGE:CUDA>:-DCUTLASS_ENABLE_GDC_FOR_SM90=1>")
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE COMPILE_HOPPER_TMA_GEMMS)
if(NOT MSVC)
# The native SM90 (Hopper) TMA/WGMMA launchers pass CUTLASS TMA descriptor types through
# NVCC-generated host stubs. With CUDA 13 + MSVC those stubs contain 128-byte over-aligned
# by-value formal parameters, which triggers MSVC C2719 ("formal parameter with requested
# alignment of 128 won't be aligned"). Disable the native SM90 fpA_intB (COMPILE_HOPPER_TMA_GEMMS)
# and grouped MoE (COMPILE_HOPPER_TMA_GROUPED_GEMMS) TMA kernels on MSVC; the launcher bodies
# become throwing stubs and the SM80 compatibility path still runs on Hopper at runtime.
# See docs/contrib_ops/cuda/moe_qmoe.md section 14.1.
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE COMPILE_HOPPER_TMA_GEMMS)
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE COMPILE_HOPPER_TMA_GROUPED_GEMMS)
endif()
endif()
Expand Down
15 changes: 14 additions & 1 deletion docs/contrib_ops/cuda/matmul_nbits.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Source files:
| `bits` | Quantization bit width: `4` or `8`. |
| `block_size` | Quantization group size along `K` (16 / 32 / 64 / 128). One scale (and optional zero point) per group. |
| `accuracy_level` | Minimum accuracy level for internal handling of `A`; default `0` means unset. |
| `weight_prepacked` | CUDA fpA_intB weight-layout selector. `0` (default): `B` is in standard MatMulNBits layout and may be runtime-prepacked. `1`: `B` is already prepacked in the CUDA SM80 fpA_intB layout. `2`: `B` is prepacked in the CUDA SM90 (Hopper) fpA_intB layout, consumed by the native SM90 kernel (requires an SM90 device and `block_size` in {64, 128}). |
| `weight_prepacked` | CUDA fpA_intB weight-layout selector. `0` (default): `B` is in standard MatMulNBits layout and may be runtime-prepacked. `1`: `B` is already prepacked in the CUDA SM80 fpA_intB layout. `2`: `B` is prepacked in the CUDA SM90 (Hopper) fpA_intB layout, consumed by the native SM90 kernel (requires an SM90 device and `block_size` in {64, 128}). The native SM90 kernel is not compiled on Windows/MSVC builds (CUDA 13 host stubs hit MSVC `C2719` with over-aligned TMA parameters — see [moe_qmoe.md §14.1](./moe_qmoe.md)); on those builds the default `0`/`1` layouts run the SM80 compatibility kernel on Hopper instead. |

| Input | Index | Notes |
|-------|-------|-------|
Expand Down Expand Up @@ -307,6 +307,19 @@ present. `ComputeInternal` then:

## 9. Testing

- CUDA EP internal tests run through `CUDA_EP_Unittest` in
[onnxruntime/test/providers/cuda/cuda_provider_test.cc](../../../onnxruntime/test/providers/cuda/cuda_provider_test.cc).
Run them from `onnxruntime_provider_test` with:

```bash
./onnxruntime_provider_test --gtest_filter=CUDA_EP_Unittest.*
```

This wrapper executes the internal CUDA-UT shared library and covers the
fpA_intB / MatMulNBits groupwise GEMM tests under
[onnxruntime/test/contrib_ops/cuda_kernels/fpA_intB_gemm_kernel_test.cc](../../../onnxruntime/test/contrib_ops/cuda_kernels/fpA_intB_gemm_kernel_test.cc)
as well as the SM90 validation tests in
[onnxruntime/test/contrib_ops/cuda_kernels/matmul_nbits_sm90_validation_test.cc](../../../onnxruntime/test/contrib_ops/cuda_kernels/matmul_nbits_sm90_validation_test.cc).
- Python operator tests: `onnxruntime/test/python/transformers` (see the QMoE /
GEMV profiling helpers, e.g. `profile_qmoe_gemv.sh`).
- CUDA prepacked-weight parity tests:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,17 @@ void sm90_generic_mixed_gemm_kernelLauncher(ActivationType const*, WeightType co
float const, OutputType*, int, int, int, int const, tkc::CutlassGemmConfig,
char*, size_t, cudaStream_t, int*) {
ORT_LLM_LOG_ENTRY();
#if defined(_MSC_VER)
// On Windows/MSVC the native SM90 (Hopper) TMA/WGMMA fpA_intB kernels are intentionally not
// compiled: CUDA 13 NVCC host stubs hit MSVC C2719 with over-aligned (128-byte) by-value TMA
// parameters, so COMPILE_HOPPER_TMA_GEMMS is left undefined on MSVC (see
// docs/contrib_ops/cuda/moe_qmoe.md section 14.1). Recompiling with 90a-real does not help here.
ORT_THROW(
"[fpA_intB_gemm] The native SM90 (Hopper) fpA_intB kernel is not available on Windows/MSVC "
"builds. Use the SM80-compatible weight layout (weight_prepacked=0 or 1) instead.");
#else
ORT_THROW("[fpA_intB_gemm] Please recompile with support for hopper by passing 90a-real as an arch.");
#endif
}
#endif // COMPILE_HOPPER_TMA_GEMMS

Expand Down
33 changes: 33 additions & 0 deletions onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "contrib_ops/cuda/llm/fpA_intB_gemm_adaptor.h"
#include "contrib_ops/cuda/llm/fpA_intB_gemm_preprocessors.h"
#include "contrib_ops/cuda/llm/common/cuda_runtime_utils.h"
#include "contrib_ops/cuda/quantization/matmul_nbits_sm90_validation.h"
#endif
#include "contrib_ops/cuda/llm/common/logger.h"
#include "contrib_ops/cpu/quantization/matmul_nbits_helper.h"
Expand Down Expand Up @@ -56,6 +57,38 @@ int64_t MatMulNBits<T>::RequiredWeightPrepackedFormat() const {
return FpAIntBPackingSmForKernel() == 90 ? kMatMulNBitsWeightPrepackedSm90 : kMatMulNBitsWeightPrepackedSm80;
}

// See matmul_nbits_sm90_validation.h for why these two functions are declared there but defined
// here (non-inline, single definition, inside the CUDA EP translation unit that observes
// COMPILE_HOPPER_TMA_GEMMS).
bool IsNativeSm90FpAIntBGemmCompiled() {
#if defined(COMPILE_HOPPER_TMA_GEMMS)
return true;
#else
return false;
#endif
}

void ValidateSm90PrepackedWeightSupport(int sm, int64_t block_size) {
// The native SM90 (Hopper TMA/WGMMA) mixed-GEMM kernel requires a compute-capability 9.0
// device and a block_size that is a multiple of the Hopper K tile (128 / sizeof(half) = 64).
// block_size=32 is only supported by the SM80/Ampere-class kernel + GEMV path.
ORT_ENFORCE(sm == 90,
"weight_prepacked=2 (SM90 layout) requires a compute capability 9.0 (Hopper) device, but got sm ", sm);
#if !defined(COMPILE_HOPPER_TMA_GEMMS)
// The native SM90 (Hopper) fpA_intB TMA/WGMMA kernel is not compiled in this build (for
// example Windows/MSVC, where CUDA 13 NVCC host stubs hit MSVC C2719 with over-aligned TMA
// parameters; see docs/contrib_ops/cuda/moe_qmoe.md section 14.1). The SM90 weight layout
// cannot be consumed by the SM80 kernel, so fail early here with a clear message instead of
// dispatching to the throwing launcher stub during tactic profiling.
ORT_THROW(
"weight_prepacked=2 (SM90 layout) is not supported by this ONNX Runtime build "
"(the native SM90 Hopper fpA_intB kernel is unavailable, e.g. on Windows/MSVC). "
"Re-export the model with weight_prepacked=0 or 1 to use the SM80-compatible fpA_intB layout.");
#endif
ORT_ENFORCE(block_size == 64 || block_size == 128,
"weight_prepacked=2 (SM90 layout) supports block_size 64 or 128 only, but got ", block_size);
}

template <typename T>
void MatMulNBits<T>::InitGemmProfiler(int sm) {
gemmProfiler_ = s_profilerManager.createGemmPluginProfiler(/*inference*/ false);
Expand Down
11 changes: 4 additions & 7 deletions onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "core/providers/cuda/cuda_kernel.h"
#include "core/providers/cuda/shared_inc/fpgeneric.h"
#include "contrib_ops/cuda/llm/fpA_intB_gemm_profiler.h"
#include "contrib_ops/cuda/quantization/matmul_nbits_sm90_validation.h"
#include "core/platform/env_var_utils.h"

namespace onnxruntime {
Expand Down Expand Up @@ -129,13 +130,9 @@ class MatMulNBits final : public CudaKernel {
"weight_prepacked must be 0 (not prepacked), 1 (SM80 layout), or 2 (SM90 layout), but got ",
weight_prepacked_);
if (weight_prepacked_ == kMatMulNBitsWeightPrepackedSm90) {
// The native SM90 (Hopper TMA/WGMMA) mixed-GEMM kernel requires a compute-capability 9.0
// device and a block_size that is a multiple of the Hopper K tile (128 / sizeof(half) = 64).
// block_size=32 is only supported by the SM80/Ampere-class kernel + GEMV path.
ORT_ENFORCE(sm_ == 90,
"weight_prepacked=2 (SM90 layout) requires a compute capability 9.0 (Hopper) device, but got sm ", sm_);
ORT_ENFORCE(block_size_ == 64 || block_size_ == 128,
"weight_prepacked=2 (SM90 layout) supports block_size 64 or 128 only, but got ", block_size_);
// See matmul_nbits_sm90_validation.h / matmul_nbits.cc for the validation logic (extracted
// into a pure function of (sm, block_size) so it can be unit-tested without a Hopper GPU).
ValidateSm90PrepackedWeightSupport(sm_, block_size_);
}

if constexpr (std::is_same<T, MLFloat16>::value || std::is_same<T, BFloat16>::value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//
// Pure, host-only (no GPU required) helpers for validating a MatMulNBits node that requests
// weight_prepacked=2 (the native SM90/Hopper mixed-GEMM weight layout). These are declared here,
// separate from matmul_nbits.h, so that unit tests can exercise the validation logic with synthetic
// (sm, block_size) values -- e.g. a Hopper `sm` on a machine/build that has no GPU at all -- which is
// not otherwise possible since MatMulNBits<T> normally reads `sm` from the real device properties.
//
// Both functions are DEFINED (non-inline) in matmul_nbits.cc, NOT in this header. matmul_nbits.cc is
// compiled as part of the CUDA execution provider target (onnxruntime_providers_cuda /
// onnxruntime_providers_cuda_plugin), which is the only place where the COMPILE_HOPPER_TMA_GEMMS
// macro -- recording whether this build actually compiles the native SM90 (Hopper TMA/WGMMA)
// fpA_intB kernel; it is left undefined on Windows/MSVC, see cmake/onnxruntime_providers_cuda.cmake
// -- is defined consistently. If IsNativeSm90FpAIntBGemmCompiled() were instead defined inline in
// this header, each translation unit that includes it (including one that does not receive that
// target-scoped compile definition) could observe a different answer for whether the native SM90
// kernel is available, silently producing an ODR violation / macro-skew bug. Keeping a single
// non-inline definition inside matmul_nbits.cc avoids that hazard.
//
#pragma once

#include <cstdint>

#if USE_FPA_INTB_GEMM

namespace onnxruntime {
namespace contrib {
namespace cuda {

// Returns true iff this build compiled the native SM90 (Hopper TMA/WGMMA) fpA_intB mixed-GEMM
// kernel, i.e. iff COMPILE_HOPPER_TMA_GEMMS was defined when matmul_nbits.cc was compiled.
// Pure/host-only: does not touch the GPU and can be called without a CUDA device present.
bool IsNativeSm90FpAIntBGemmCompiled();

// Validates that a MatMulNBits node requesting weight_prepacked=2 (the native SM90 weight layout)
// can actually be served, given the device compute capability `sm` (e.g. as computed from
// GetDeviceProp().major*10+minor) and the node's `block_size` attribute. Throws (ORT_ENFORCE /
// ORT_THROW) with a diagnostic message if the request cannot be served; returns normally otherwise.
// Pure/host-only: `sm` and `block_size` are plain parameters (not read from real hardware), so this
// can be unit-tested with synthetic values on any machine, without a GPU.
void ValidateSm90PrepackedWeightSupport(int sm, int64_t block_size);

} // namespace cuda
} // namespace contrib
} // namespace onnxruntime

#endif // USE_FPA_INTB_GEMM
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ class KernelTestFixture : public ::testing::Test {
using WType = typename cutlassTypeMapper<KT>::WType;
using onnxruntime::llm::kernels::cutlass_kernels::CutlassFpAIntBGemmRunner;
auto runner = std::make_shared<CutlassFpAIntBGemmRunner<AType, WType, cutlassTypeMapper<KT>::QuantOp>>();
if (onnxruntime::llm::common::getSMVersion() == 90) {
runner->setUseSm90Native(true);
}
Comment thread
tianleiwu marked this conversation as resolved.
auto& gemm_runner = *runner;
int ws_bytes = gemm_runner.getWorkspaceSize(m_, n_, k_);
CudaBuffer ws_buffer(ws_bytes);
Expand Down Expand Up @@ -565,8 +568,6 @@ TEST_F(Fp16Int8GroupwiseTest, Fp16_Int8_Gemm_CudaKernel) {
for (const auto& [n, k] : get_n_k_list()) {
InitBuffers(m, n, k, 64);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
Comment thread
tianleiwu marked this conversation as resolved.
InitBuffers(m, n, k, 128);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
}
}
}
Expand All @@ -582,8 +583,6 @@ TEST_F(Fp16Int4GroupwiseTest, Fp16_Int4_Gemm_CudaKernel) {
for (const auto& [n, k] : get_n_k_list()) {
InitBuffers(m, n, k, 64);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
Comment thread
tianleiwu marked this conversation as resolved.
InitBuffers(m, n, k, 128);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
}
}
}
Expand All @@ -599,8 +598,6 @@ TEST_F(Bf16Int8GroupwiseTest, BF16_Int8_Gemm_CudaKernel) {
for (const auto& [n, k] : get_n_k_list()) {
InitBuffers(m, n, k, 64);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
Comment thread
tianleiwu marked this conversation as resolved.
InitBuffers(m, n, k, 128);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
}
}
}
Expand All @@ -616,8 +613,6 @@ TEST_F(Bf16Int4GroupwiseTest, BF16_Int4_Gemm_CudaKernel) {
for (const auto& [n, k] : get_n_k_list()) {
InitBuffers(m, n, k, 64);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
Comment thread
tianleiwu marked this conversation as resolved.
InitBuffers(m, n, k, 128);
EXPECT_TRUE(BenchmarkAndVerifyKernel());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

// GPU-free coverage for the SM90 (Hopper) weight_prepacked=2 validation logic used by
// contrib_ops/cuda/quantization/matmul_nbits.h's MatMulNBits<T> constructor.
//
// MatMulNBits<T> normally reads the device compute capability from real hardware
// (GetDeviceProp()), so the "native SM90 kernel is not compiled in this build" throw (e.g. on
// Windows/MSVC, see the COMPILE_HOPPER_TMA_GEMMS macro in cmake/onnxruntime_providers_cuda.cmake) is
// only reachable on an actual Hopper GPU that was built without the native kernel -- a combination
// existing tests cannot produce, since they all GTEST_SKIP() when no CUDA device is present (see
// e.g. MatMulNBits.Fp16_Int4_PrepackedSm90BlockSize32Rejected in test/contrib_ops/matmul_4bits_test.cc).
// ValidateSm90PrepackedWeightSupport() takes sm/block_size as plain parameters instead of reading
// real hardware, so it can be exercised here with synthetic values and no GPU at all.
//
// This lives alongside the other CUDA-EP-internal tests under contrib_ops/cuda_kernels/ (rather than
// in test/contrib_ops/matmul_4bits_test.cc) because onnxruntime_providers_cuda is a runtime-loaded
// shared library (see core/providers/cuda/symbols.def, which exports only the small provider-bridge
// interface): matmul_nbits.cc's free functions are not exported from that DLL/so, so a normal
// provider-test binary cannot call them directly (it never links onnxruntime_providers_cuda at
// compile time -- see AddTest()/DEPENDS in cmake/onnxruntime_unittests.cmake). Files under
// contrib_ops/cuda_kernels/ are instead compiled directly into the onnxruntime_providers_cuda_ut
// module together with the CUDA EP's own object files whenever
// onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS is set (as it is in the windows_cuda.yml / linux_cuda_ci.yml
// CI legs), so this test both links successfully and observes COMPILE_HOPPER_TMA_GEMMS exactly as the
// real onnxruntime_providers_cuda target does -- no ODR/macro-skew risk.
//
// Test can be run like the following:
// ./onnxruntime_provider_test --gtest_filter=CUDA_EP_Unittest.*
#if USE_FPA_INTB_GEMM
#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <string>

#include "core/common/common.h"
#include "contrib_ops/cuda/quantization/matmul_nbits_sm90_validation.h"

namespace onnxruntime {
namespace test {

namespace {
// Runs `fn` and returns the message of any thrown exception, or "" if it did not throw.
template <typename Fn>
std::string CaughtMessage(Fn&& fn) {
std::string message;
ORT_TRY {
fn();
}
ORT_CATCH(const std::exception& ex) {
ORT_HANDLE_EXCEPTION([&]() { message = ex.what(); });
}
return message;
}
} // namespace

// Non-Hopper compute capability is always rejected, regardless of whether this build compiled the
// native SM90 kernel. This assertion is build-independent.
TEST(MatMulNBitsSm90ValidationTest, RejectsNonHopperComputeCapability) {
const std::string message = CaughtMessage([]() {
onnxruntime::contrib::cuda::ValidateSm90PrepackedWeightSupport(/*sm=*/80, /*block_size=*/64);
});
EXPECT_THAT(message, ::testing::HasSubstr("weight_prepacked=2 (SM90 layout) requires a compute capability 9.0"));
}

// A Hopper (sm=90) request is validated differently depending on whether this build actually
// compiled the native SM90 (Hopper TMA/WGMMA) fpA_intB kernel.
TEST(MatMulNBitsSm90ValidationTest, Sm90SupportMatchesBuildCapability) {
using onnxruntime::contrib::cuda::IsNativeSm90FpAIntBGemmCompiled;
using onnxruntime::contrib::cuda::ValidateSm90PrepackedWeightSupport;

if (IsNativeSm90FpAIntBGemmCompiled()) {
// The native SM90 kernel is available: a supported block_size (64 or 128) must be accepted ...
EXPECT_NO_THROW(ValidateSm90PrepackedWeightSupport(/*sm=*/90, /*block_size=*/64));

// ... but block_size=32 is SM80/GEMV-only and must still be rejected.
const std::string message = CaughtMessage([]() {
ValidateSm90PrepackedWeightSupport(/*sm=*/90, /*block_size=*/32);
});
EXPECT_THAT(message, ::testing::HasSubstr("supports block_size 64 or 128 only"));
} else {
// The native SM90 kernel is not compiled in this build (e.g. Windows/MSVC): even a Hopper
// device with an otherwise-supported block_size must be rejected with a build-support message.
const std::string message = CaughtMessage([]() {
ValidateSm90PrepackedWeightSupport(/*sm=*/90, /*block_size=*/64);
});
EXPECT_THAT(message, ::testing::HasSubstr("is not supported by this ONNX Runtime build"));
}
}

} // namespace test
} // namespace onnxruntime
#endif // USE_FPA_INTB_GEMM
Loading
Loading