Repository navigation
[CUDA] MatMulNBits: optional M chunking for the fpA_intB path - #32810
Conversation
Large-vocabulary LM heads make the fpA_intB tactic profiler allocate an M x N output buffer per profiled M bucket, and any runtime M above the largest profiled bucket triggers slow lazy profiling of a huge bucket. Add ep.cuda.matmul_nbits_m_chunk_size (env ORT_MATMULNBITS_M_CHUNK_SIZE), default off. When M > chunk and M * (N + K) * sizeof(T) > 256 MiB, the fpA_intB GEMM/GEMV runs in row chunks that share one chunk-sized workspace, and constructor profiling is capped accordingly. ORT_MATMULNBITS_FORCE_CHUNKED=1 bypasses the size gate. The Level-2 workspace declaration uses the chunk size. Empty input now returns before runtime g_idx validation.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Profiler bucket rounding defeats the promised memory cap, and configuration documentation and overflow handling need correction.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (3)
What changed in this PR
Adds optional, size-gated row chunking to reduce CUDA MatMulNBits fpA_intB workspace and profiling memory.
Changes:
- Adds chunk-size configuration and chunked GEMM/GEMV execution.
- Updates workspace estimation and empty-input handling.
- Adds documentation and correctness/configuration tests.
| File | Description |
|---|---|
matmul_nbits.h |
Parses configuration and determines chunk sizes. |
matmul_nbits.cc |
Executes row chunks and adjusts workspace allocation. |
onnxruntime_session_options_config_keys.h |
Exposes the session configuration key. |
matmul_nbits.md |
Documents behavior and measurements. |
matmul_4bits_test.cc |
Tests chunked CUDA correctness. |
matmul_nbits_e2e_workspace_test.cc |
Tests workspace size gating. |
test_op_matmulnbits_prepacked_cuda.py |
Tests parity, validation, and empty inputs. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Baiju Meswani (baijumeswani)
left a comment
There was a problem hiding this comment.
Reviewed live head 7407337da3b66187edec0ee7f2b6d324eeb99b93. The existing Copilot threads already cover the actionable overflow, profiler-bucket cap, and public configuration documentation issues, so I am not duplicating those comments.
The row-offset execution, trailing GEMV handling, shared workspace, and empty-input path otherwise look sound. Before using the standalone H200 results to select a default, I recommend adding:
- an end-to-end long-prefill benchmark that reports TTFT and peak memory for the full model, not only the LM-head node;
- correctness/workspace coverage where the natural 256 MiB gate activates, since current chunking parity tests use
ORT_MATMULNBITS_FORCE_CHUNKEDand the workspace test only exercises a small shape; - CUDA graph coverage for a shape with a trailing partial chunk, verifying warmup populates both tactic buckets;
- CUDA plugin EP coverage, since the new session configuration is documented as supported there but is not exercised end to end.
The current measurements support keeping this opt-in: the main TTFT benefit is avoiding first-use profiling of an unseen large M bucket, while an M already within the profiled range shows slight first-run overhead.



Description
Adds optional M (row) chunking to the CUDA
MatMulNBitsfpA_intB path to bound memory that grows withM, targeting large-vocabulary LM heads during chunked prefill. It is off by default and size-gated, so small layers are never split.ep.cuda.matmul_nbits_m_chunk_size(session config) /ORT_MATMULNBITS_M_CHUNK_SIZE(env)Aper fpA_intB launch.0/unset disables. Session config wins. Also works for the CUDA plugin EP.ORT_MATMULNBITS_FORCE_CHUNKED=1(existing)A node is chunked only when
M > chunkandM * (N + K) * sizeof(T) > 256 MiB(the tactic profiler'sAandCbuffers for an unchunkedM; same budget as the existing N-chunked fallback). For Qwen3.8-27B in fp16/bf16:M>N + K <= 16384Changes
matmul_nbits.h/.ccFpAIntBRowsPerLaunch); fpA_intB GEMM/GEMV loop over row chunks sharing one chunk-sized workspace, with at most two tactic lookups (full chunks + trailing chunk, which may use the GEMV below 16 rows); cap constructor profiling atmax(chunk, 256 MiB / ((N + K) * sizeof(T))); Level-2DeclareWorkspaceRequirementsuses the chunk rows; empty input (M == 0) now returns before runtimeg_idxvalidation, which may sync the stream.onnxruntime_session_options_config_keys.hkOrtSessionOptionsCudaMatMulNBitsMChunkSize.docs/contrib_ops/cuda/matmul_nbits.mdMatMulNBitsWorkspace.MChunkSizeGate), Python parity / invalid-value / empty-input tests.The dequantize + cuBLAS fallback and the fused GEMV are unchanged: neither has
M-proportional scratch (the fallback's dequant buffer is already bounded by N chunking).Motivation and Context
For an LM head, the fpA_intB tactic profiler allocates an
M x Noutput buffer per profiled bucket, and a runtimeMabove the largest profiled bucket (2048 by default) triggers lazy profiling of that larger bucket at run time. Measured on H200 for the Qwen3.8-27B LM head alone (FP16, INT4, block 32,kSameAsRequestedarena, NVML sampled from a separate process):MYoutput.Y, both weight copies, and the CUDA context (breakdown in §6.2).Limitations / follow-ups
Testing
Release build, CUDA 13.0,
CMAKE_CUDA_ARCHITECTURES=90-real,onnxruntime_USE_FPA_INTB_GEMM=ON(compact), H200:onnxruntime_provider_test --gtest_filter='MatMulNBits*.*:MatMul2Bits*.*': 125 passed.onnxruntime_provider_test --gtest_filter='CUDA_EP_Unittest.*'(withonnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS=ON): 291 passed, includingMatMulNBitsWorkspace.MChunkSizeGateand the existing workspace agreement / zero-M tests.python -m pytest onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py: 7 passed, 9 skipped (offline weight packer not in the build).ORT_FPA_INTB_DEBUG=1trace confirms a small node stays a single launch while the LM head atM=2048, chunk 256 runs 256-row chunks.