Skip to content

[CUDA] Add persistent fpA_intB MatMulNBits tactic autotune cache#29588

Draft
tianleiwu wants to merge 11 commits into
mainfrom
tlwu/20260707/fpa_intb_autotune_cache
Draft

[CUDA] Add persistent fpA_intB MatMulNBits tactic autotune cache#29588
tianleiwu wants to merge 11 commits into
mainfrom
tlwu/20260707/fpa_intb_autotune_cache

Conversation

@tianleiwu

Copy link
Copy Markdown
Contributor

Stacked on #29585 (fpA_intB SM90/bs32/bias). Review/merge that first; this PR's base is its branch, so the diff here is cache-only. Rebase to main after #29585 merges.

Description

Adds an opt-in, hardware-keyed on-disk tactic cache for the fpA_intB weight-only MatMulNBits path, plus a reduced first-time profiling sweep with lazy fallback. Persistence is off by default — with no cache location configured, behavior is unchanged (in-process only).

Cache subsystem (gemm_tactic_cache.{h,cc})

  • HardwareSignature (GPU name, SM, CUDA runtime, ORT version/commit/build config) names and strictly guards cache files, so tactics are never reused across incompatible GPUs/toolkits/builds.
  • TSV format: percent-encoded free-text fields, a column-name header (appended columns are backward/forward compatible), file locking + atomic temp+rename write, and in-memory merge on flush so concurrent sessions tuning different shapes don't lose updates.
  • MatMulNBitsTacticCache keyed by (N16b, K, act dtype, weight type, bits, block_size, zero-points, gemv_enabled, packing_sm). packing_sm keeps the SM80-compat and native-SM90 tactics for the same shape distinct on one device (e.g. H200).

Profiler (gemm_profiler.h, fpA_intB_gemm_profiler.{h,cc})

  • Reduced initial sweep to a small M bucket set ({1,2,4,…,2048} default; override with ORT_FPA_INTB_PROFILE_M). getBestConfigOrProfile lazily profiles any missing bucket on first use.
  • Persistent-cache load/store hooks are no-ops unless a cache is attached.

Wiring (matmul_nbits.{h,cc}): cache location resolved from session config (ep.cuda.gemm_tactic_cache_dir / _prefix) or env (ORT_CUDA_GEMM_TACTIC_CACHE_DIR / _PREFIX).

Offline tuning tool: onnxruntime/python/tools/fpa_intb_tune.py.

Production hardening (5 gaps addressed)

  1. CUDA-graph-capture safetyComputeInternal uses lookup-only getBestConfig while the compute stream is capturing (lazy profiling launches kernels/events/allocs, illegal during capture); run a warmup inference before capture to tune the needed buckets.
  2. No hot-path disk I/O — lazily profiled buckets stay in the in-process map and are not flushed to disk on the inference path; disk writes happen only off the hot path (construction-time sweep + offline tool).
  3. Per-location cache — the process-global cache is keyed by resolved location, so sessions with different dirs/prefixes each get their own cache instead of the first kernel's location winning.
  4. Reduced-sweep ceiling — large M above the initial ceiling is covered by lazy profiling off-capture.
  5. Stale-tactic guard — CUTLASS tactics loaded from disk are validated against the runner's current getConfigs() and dropped (re-profiled) if incompatible.

Testing

  • gemm_tactic_cache_test.cc (8 cases): TSV round-trip, SM80/SM90 config serialize/parse, null tactic, store/load, signature-mismatch rejection, appended-column tolerance, concurrent flush-merge. Built with onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS=ON, run via --gtest_filter=CUDA_EP_Unittest.All. 8/8 pass on H200.
  • C++ *MatMulNBits* integration: 65 passed / 1 skipped (no regression with cache present).
  • Python test_op_matmulnbits_prepacked_cuda.py: 7 passed.
  • End-to-end persistence smoke test on H200: session with ep.cuda.gemm_tactic_cache_dir writes NVIDIA_H200_sm90.matmulnbits_fpa_intb.tsv and a second session reuses it.

tianleiwu added 2 commits July 7, 2026 02:05
… MatMulNBits

Extends the CUDA fpA_intB weight-only MatMulNBits path (FP16/BF16 int4/int8):

Native SM90 (Hopper) kernel:
- cutlass_extensions GemmFpAIntB::operator(): reuse the SM80 (Ampere) mixed-GEMM
  kernel for __CUDA_ARCH__>=900 (Hopper/Blackwell) instead of stubbing it out, so
  the SM80-compat dispatch produces a real kernel on Hopper (mirrors MoeFCGemm).
- fpA_intB_launcher_sm90.inl: gate the host-callable launcher on
  COMPILE_HOPPER_TMA_GEMMS only (not __CUDA_ARCH__/__NV_SASS_VERSION__, unset in
  the host pass), so the native SM90 TMA/WGMMA launcher symbol is emitted instead
  of the "recompile with 90a" stub.
- CutlassFpAIntBGemmRunnerInterface: add setArch(int) and setUseSm90Native(bool);
  the runner routes SM90 to sm90_dispatch when native, else the SM80 compat path.
- matmul_nbits: FpAIntBPackingSmForKernel() returns 90 for weight_prepacked=2 on an
  SM90 device (else 80); InitGemmProfiler opts in to the native kernel (sm==90) or
  forces the runner to SM80 (sm80-compat on Hopper) so tactic enumeration and
  workspace sizing match the dispatched kernel; GemmIdCore gains an sm field so the
  SM80-compat and SM90-native tactics for the same shape are not confused; the GEMV
  is launched with the packing arch (FpAIntBPackingSmForKernel) rather than the raw
  device SM to match the packed interleave layout.
- weight_prepacked=2 (SM90 layout) is now accepted (was reserved/rejected): requires
  an SM90 device and block_size in {64, 128}. Updated contrib op docs.

block_size=32:
- Relax the group-size gates in the GEMV dispatcher, the CUTLASS fine-grained
  can_implement/kernel-launcher, and the MatMulNBits eligibility check.

Fused bias:
- Drop the !has_bias_ gate: bias (input 5) is supported by the GEMV, the SM80/SM90
  CUTLASS epilogue, and the profiler, so bias-bearing nodes are eligible.

Tests: matmul_4bits_test.cc (bias/bs32/SM90 runtime-prepacked cases) and
test_op_matmulnbits_prepacked_cuda.py (int4/int8, SM80/SM90, bias parity).
Adds an opt-in, hardware-keyed on-disk tactic cache for the fpA_intB weight-only
MatMulNBits path, plus a reduced first-time profiling sweep with lazy fallback.
Persistence is off by default (in-process behavior unchanged).

Cache subsystem (gemm_tactic_cache.{h,cc}):
- HardwareSignature (GPU name, SM, CUDA runtime, ORT version/commit/build config)
  names and strictly guards cache files, so tactics are never reused across
  incompatible GPUs/toolkits/builds.
- TSV format with percent-encoded free-text fields, column-name header (append-
  compatible), file locking + atomic temp+rename write, and in-memory merge on
  flush so concurrent sessions tuning different shapes do not lose updates.
- MatMulNBitsTacticCache keyed by (N16b, K, act dtype, weight type, bits,
  block_size, zero-points, gemv_enabled, packing_sm). packing_sm keeps SM80-compat
  and native-SM90 tactics for the same shape distinct on one device.

Profiler (gemm_profiler.h, fpA_intB_gemm_profiler.{h,cc}):
- Reduced initial sweep to a small M bucket set ({1,2,4,...,2048} default, override
  with ORT_FPA_INTB_PROFILE_M); getBestConfigOrProfile lazily profiles any missing
  bucket on first use and keeps it in the in-process map.
- Persistent-cache hooks (load before sweep, store after) are no-ops unless a cache
  is attached.

Wiring (matmul_nbits.{h,cc}): resolve the cache location from session config
(ep.cuda.gemm_tactic_cache_dir/_prefix) or env vars
(ORT_CUDA_GEMM_TACTIC_CACHE_DIR/_PREFIX).

Offline tuning tool: onnxruntime/python/tools/fpa_intb_tune.py.

Production hardening (5 gaps addressed):
1. CUDA-graph-capture safety: ComputeInternal uses lookup-only getBestConfig while
   the compute stream is capturing (lazy profiling launches kernels/events/allocs,
   illegal during capture); a warmup run before capture tunes needed buckets.
2. No hot-path disk I/O: lazily profiled buckets are kept in the in-process map and
   are NOT flushed to disk on the inference path (disk writes only happen off the
   hot path in the construction-time sweep and the offline tool).
3. Per-location cache: the process-global cache is keyed by resolved location, so
   sessions with different dirs/prefixes get their own cache instead of the first
   kernel's location winning.
4. Reduced sweep ceiling is covered by lazy profiling for large M off-capture.
5. Stale-tactic guard: CUTLASS tactics loaded from disk are validated against the
   runner's current getConfigs() and dropped (re-profiled) if incompatible.

Tests: gemm_tactic_cache_test.cc (serialize/parse round-trip incl. SM80/SM90,
signature-mismatch rejection, appended-column tolerance, store/load, concurrent
flush-merge). Requires onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS=ON.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in, hardware-keyed persistent on-disk tactic autotune cache for the CUDA fpA_intB MatMulNBits path, and updates the profiler to reduce the initial M-bucket sweep with a lazy single-bucket fallback (with CUDA graph capture safety). It also includes a Python offline tuning tool, unit tests, and documentation for the cache format and configuration.

Changes:

  • Introduce gemm_tactic_cache utilities (hardware signature, TSV encoding/decoding, config serialization, atomic/locked merge flush).
  • Update GemmPluginProfiler / fpA_intB profiler to support reduced initial profiling + lazy per-bucket profiling and persistent-cache hooks.
  • Wire cache resolution into MatMulNBits and add an offline tuner + docs/tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
onnxruntime/test/contrib_ops/cuda_kernels/gemm_tactic_cache_test.cc New pure-logic unit tests for TSV/config round-trip and cache merge/signature behavior.
onnxruntime/python/tools/fpa_intb_tune.py New offline tuning CLI tool that generates a persistent cache by constructing a CUDA session and optionally running dummy inferences.
onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.h Use new profiler max-M logic for the initial sweep.
onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.cc Add process-global persistent cache wiring + CUDA graph capture guard for lazy profiling.
onnxruntime/contrib_ops/cuda/llm/gemm_tactic_cache.h New cache API: hardware signature, TSV helpers, MatMulNBits cache key and disk-backed cache wrapper.
onnxruntime/contrib_ops/cuda/llm/gemm_tactic_cache.cc New cache implementation: TSV IO, signature checks, file locking, merge + atomic replace.
onnxruntime/contrib_ops/cuda/llm/gemm_profiler.h Add reduced-bucket sweep support, persistent-cache hooks, and lazy getBestConfigOrProfile.
onnxruntime/contrib_ops/cuda/llm/fpA_intB_gemm_profiler.h Add env-controlled M-bucket override, persistent-cache attachment, and overrides for new hook points.
onnxruntime/contrib_ops/cuda/llm/fpA_intB_gemm_profiler.cc Implement reduced bucket set, persistent cache load/store, and stale-tactic validation.
docs/contrib_ops/cuda/matmul_nbits.md Document cache enablement, reduced sweep, lazy profiling, CUDA graph capture constraints, and tooling/tests.
docs/contrib_ops/cuda/gemm_profiler_cache.md New design/spec doc describing the cache format, signature strictness, and phased plan.

Comment thread onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.cc Outdated
Comment thread onnxruntime/contrib_ops/cuda/llm/gemm_tactic_cache.cc
Comment thread onnxruntime/python/tools/fpa_intb_tune.py
@tianleiwu tianleiwu force-pushed the tlwu/20260707/fpa_intb_sm90_bs32_bias branch from e3dda06 to 7023483 Compare July 7, 2026 16:38
@tianleiwu tianleiwu marked this pull request as draft July 7, 2026 17:00
Base automatically changed from tlwu/20260707/fpa_intb_sm90_bs32_bias to main July 7, 2026 20:58
tianleiwu added 8 commits July 7, 2026 21:03
…b_autotune_cache

# Conflicts:
#	docs/contrib_ops/cuda/matmul_nbits.md
#	onnxruntime/contrib_ops/cuda/llm/gemm_profiler.h
#	onnxruntime/contrib_ops/cuda/quantization/matmul_nbits.cc
#	onnxruntime/test/contrib_ops/matmul_4bits_test.cc
- Drop the redundant per-row schema_version column (file magic already versions
  the format; the column was written but never read on load).
- Relax the cache reuse guard: keep device_name/sm/cuda_runtime/ort_version as
  the strict match, demote ort_git_commit and ort_build_config to diagnostic-only
  header fields (a reused tactic is only ever slightly suboptimal, never wrong,
  and loadPersistentCache already re-validates each tactic against getConfigs()).
- Persist lazily-profiled M buckets best-effort at kernel/session teardown via
  GemmPluginProfiler::persistProfiledTactics(), so the on-disk cache converges to
  the runtime M values the workload actually hits (off the hot path).
- Add a unit test for the relaxed guard and update docs/comments.
Split staging from disk I/O so the tactic cache converges to real runtime M
buckets without per-node file rewrites:
- MatMulNBits kernel destructors only STAGE lazily-profiled tactics into the
  process-global in-memory cache (persistProfiledTactics -> stagePersistentCache,
  no disk write). The construction-time sweep still flushes eagerly so the file
  exists mid-session (the offline tuning tool reads it).
- Add FlushMatMulNBitsTacticCaches() (declared in a dependency-free header,
  no-op without onnxruntime_USE_FPA_INTB_GEMM) and call it from
  ~CUDAExecutionProvider(), which covers both the built-in and the CUDA plugin
  EP (CudaEp wraps a CUDAExecutionProvider). This is the single deterministic
  disk write, replacing the previous static-destructor flush.
- Flush is best-effort and dirty-guarded, so once-per-session-EP-teardown is cheap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants