[Test] Add GPU-free unit coverage for weight_prepacked=2 (SM90) rejection - #29806
Conversation
…tion MatMulNBits<T>'s constructor validates a weight_prepacked=2 (native SM90/ Hopper mixed-GEMM weight layout) request inline, rejecting it with one of three messages: wrong compute capability, native kernel not compiled in this build (e.g. Windows/MSVC, see COMPILE_HOPPER_TMA_GEMMS), or an unsupported block_size. Because `sm_` is read from real device properties and the compute-capability check runs before the "not compiled" check, the build-unavailable throw is only reachable on an actual Hopper GPU built without the native kernel. Every existing prepacked test GTEST_SKIPs without a CUDA device, so this branch has no coverage in CI. Extract the validation into two pure, host-only functions declared in a new header (matmul_nbits_sm90_validation.h) and defined non-inline in matmul_nbits.cc (so they observe COMPILE_HOPPER_TMA_GEMMS exactly as the rest of the CUDA EP target does): - IsNativeSm90FpAIntBGemmCompiled() - ValidateSm90PrepackedWeightSupport(sm, block_size) matmul_nbits.h now calls ValidateSm90PrepackedWeightSupport(sm_, block_size_) instead of inlining the checks. The three ORT_ENFORCE/ ORT_THROW messages are unchanged (byte-identical), so runtime behavior is unchanged. Add onnxruntime_providers_cuda_ut coverage under test/contrib_ops/cuda_kernels/matmul_nbits_sm90_validation_test.cc that calls the new functions directly with synthetic (sm, block_size) values -- no CUDA device required. This location (rather than test/contrib_ops/matmul_4bits_test.cc, which uses OpTester and always GTEST_SKIPs without a CUDA device) is required because onnxruntime_providers_cuda is a runtime-loaded shared module (see core/providers/cuda/symbols.def's narrow export allowlist): free functions in matmul_nbits.cc are not callable from a normal provider-test binary, which never links that module at compile time. Files under contrib_ops/cuda_kernels/ are compiled directly into the onnxruntime_providers_cuda_ut module alongside the CUDA EP's own object files whenever onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS is set (already true in the windows_cuda.yml / linux_cuda_ci.yml CI legs), so this is the established mechanism for unit-testing CUDA EP internals directly. A breadcrumb comment in matmul_4bits_test.cc points to the new test. Verified with a standalone throwaway program (not checked in) that reproduces the extracted function bodies and compiles/runs them under both COMPILE_HOPPER_TMA_GEMMS defined and undefined, confirming both preprocessor branches compile and all three message strings match exactly. Also ran lintrunner -a on all changed files (no issues). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Thanks for the review, @tianleiwu! I've marked this ready for review. One thing worth flagging before it lands: so far only the lint/format/CLA checks have run on this PR — the CUDA pipelines ( Could you kick off those CUDA pipelines so the test is validated in its real target environment? Once they're green this should be ready to fold into #29776. Happy to adjust if you'd prefer the coverage placed differently. |
Summary
Adds GPU-free unit coverage for the
weight_prepacked=2(SM90/Hopper) rejection logic that #29776 / 9ee050b introduced inMatMulNBits<T>'s constructor, plus the minimal, behavior-preserving refactor needed to make that logic testable without a GPU. Stacked ontlwu/fix_windows_build(notmain), since this tests code that only exists on that branch.Coverage gap
MatMulNBits<T>'s constructor validates aweight_prepacked=2request inline with three checks, in order:ORT_ENFORCE(sm_ == 90, ...)— device must be Hopper.#if !defined(COMPILE_HOPPER_TMA_GEMMS)ORT_THROW(...)— the native SM90 kernel must actually be compiled into this build (it isn't on Windows/MSVC, which is exactly what [Build] Fix windows build for sm90 or later #29776 changed).ORT_ENFORCE(block_size_ == 64 || block_size_ == 128, ...)— block_size must be Hopper-tile-aligned.sm_is read from real device properties (GetDeviceProp()), and check 1 runs before check 2. So the "native SM90 kernel not compiled in this build" throw is only reachable on an actual Hopper GPU that was built without the native kernel (e.g. real Hopper hardware + MSVC). Every existing prepacked test (Fp16_Int4_PrepackedSm90BlockSize32Rejected, etc.) usesOpTester+GTEST_SKIP()when no CUDA device is present, so that branch has no coverage at all in CI today.Minimal refactor
Extracted the validation into two pure, host-only functions that take
sm/block_sizeas plain parameters instead of reading real hardware:onnxruntime/contrib_ops/cuda/quantization/matmul_nbits_sm90_validation.h(declarations only, guarded by#if USE_FPA_INTB_GEMM):bool IsNativeSm90FpAIntBGemmCompiled();void ValidateSm90PrepackedWeightSupport(int sm, int64_t block_size);onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.cc(not in the header), so there is a single definition living inside the CUDA EP translation unit that actually observesCOMPILE_HOPPER_TMA_GEMMS— avoiding an ODR/macro-skew hazard if a test TU saw a different value of that macro than the real target does.matmul_nbits.h's constructor now does:sm_/block_size_(members) tosm/block_size(parameters), which only affects the auto-stringifiedORT_ENFORCEcondition text, not the custom messages tests/users actually see. Net runtime behavior is unchanged.Test
Added
onnxruntime/test/contrib_ops/cuda_kernels/matmul_nbits_sm90_validation_test.cc, which callsValidateSm90PrepackedWeightSupport/IsNativeSm90FpAIntBGemmCompileddirectly with synthetic(sm, block_size)values — no CUDA device required:ValidateSm90PrepackedWeightSupport(80, 64)throws containing"weight_prepacked=2 (SM90 layout) requires a compute capability 9.0"(build-independent).IsNativeSm90FpAIntBGemmCompiled():ValidateSm90PrepackedWeightSupport(90, 64)does not throw;ValidateSm90PrepackedWeightSupport(90, 32)throws containing"supports block_size 64 or 128 only".CMAKE_CUDA_ARCHITECTURESbuild such as the sm86 used bywindows_cuda.yml/linux_cuda_ci.yml):ValidateSm90PrepackedWeightSupport(90, 64)throws containing"is not supported by this ONNX Runtime build".A deliberate deviation from a literal "add it to matmul_4bits_test.cc" — please sanity-check this
I initially planned to add this
TEST()directly insidetest/contrib_ops/matmul_4bits_test.ccand call the new function straight from there. That does not link:onnxruntime_providers_cuda(and_plugin) is built as a runtime-loaded shared module (onnxruntime_add_shared_library_module), and on Windows its exports are restricted to a 3-symbol allowlist incore/providers/cuda/symbols.def(GetProvider,CreateEpFactories,ReleaseEpFactory).matmul_nbits.cc's free functions are never exported from that DLL, andonnxruntime_provider_test/onnxruntime_test_allnever link the CUDA provider module at compile time (AddTest()'sDEPENDSonly doesadd_dependencies(), i.e. build-order, nottarget_link_libraries()).The established ORT mechanism for unit-testing CUDA EP internals directly is
test/contrib_ops/cuda_kernels/*.cc(see siblingfpA_intB_gemm_kernel_test.cc,softmax_topk_kernel_test.cc): whenonnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTSis on, these sources are compiled into a separateonnxruntime_providers_cuda_utshared module together with$<TARGET_OBJECTS:onnxruntime_providers_cuda_obj>(the CUDA EP's own object files) — same translation/link domain, so direct calls resolve, and the sameCOMPILE_HOPPER_TMA_GEMMScompile definition applies to both (viaconfig_cuda_provider_shared_module()). This module's gtest cases run via the existingTEST(CUDA_EP_Unittest, All)nested-gtest bridge intest/providers/cuda/cuda_provider_test.cc. I confirmed bothwindows_cuda.ymlandlinux_cuda_ci.ymlalready setonnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS=ONand don't setonnxruntime_BUILD_CUDA_EP_AS_PLUGIN, so the new file is picked up automatically by the existingfile(GLOB ... contrib_ops/cuda_kernels/*.cc)incmake/onnxruntime_unittests.cmake— no CMake changes needed for those two legs. (Both legs build atCMAKE_CUDA_ARCHITECTURES=86, soCOMPILE_HOPPER_TMA_GEMMSis undefined in both regardless of MSVC — they'll exercise the "not compiled" branch of the new test, which is exactly the previously-uncovered branch.)I added a short breadcrumb comment in
matmul_4bits_test.ccnext toFp16_Int4_PrepackedSm90BlockSize32Rejectedpointing at the new file, so this redirection is discoverable from where the original tests live.(Note: the hand-curated
onnxruntime_test_providers_cuda_plugin_internal_test_srclist used only whenonnxruntime_BUILD_CUDA_EP_AS_PLUGINis set was intentionally left untouched, to keep this change minimal — neither target CI leg uses that mode.)Verification
ORT_ENFORCE/ORT_THROWstand-ins, and compiled + ran it twice with MSVC (cl.exe) — once with/DCOMPILE_HOPPER_TMA_GEMMS, once without — confirming both preprocessor branches compile cleanly and all three message strings match exactly what's specified above.lintrunner -aon all changed files: no issues.matmul_nbits.cc), guard placement (#if USE_FPA_INTB_GEMMaround declarations/definitions,#if !defined(COMPILE_HOPPER_TMA_GEMMS)around the build-support throw), and message byte-identity.windows_cuda.yml/linux_cuda_ci.yml) for full build/link/run validation of the new test in its actual target environment.Do not merge — opening for review/discussion.
Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com