Skip to content

[MIOpen] Add cross-architecture LGBM-based immediate-mode solver picker#8399

Closed
jdcampbe wants to merge 16 commits into
developfrom
users/jascampb/cross-architecture-solver-selection
Closed

[MIOpen] Add cross-architecture LGBM-based immediate-mode solver picker#8399
jdcampbe wants to merge 16 commits into
developfrom
users/jascampb/cross-architecture-solver-selection

Conversation

@jdcampbe

Copy link
Copy Markdown
Contributor

Motivation

MIOpen's immediate-mode solver selection uses per-architecture TunaNet models, which need a separately trained model per arch and cover only a subset of the GPU lineup. This PR adds a single cross-architecture solver picker driven by a gradient-boosted (LightGBM) rank model that selects a convolution solver from one model across gfx90a, gfx942, gfx950, gfx1151, gfx1201, and the gfx110x family. The picker reads everything it needs about the GPU directly from the device at runtime, so it requires no curated per-SKU data and stays correct on the actual silicon.

Technical Details

The picker is a self-contained inference path with no runtime dependency on libLightGBM or Treelite. The rank model is compiled to plain C via Treelite --source-only and checked in under projects/miopen/src/conv/heuristics/lgbm_models/rank/; each generated translation unit's exported symbols (predict, quantize, predict_unit0..7, the metadata accessors, the is_categorical global) are renamed with an lgbm_rank_ prefix via per-file -D defines in projects/miopen/src/CMakeLists.txt to keep them out of MIOpen's global namespace. The project enables the C language (project(MIOpen C CXX)) to compile these.

Inference (conv/heuristics/lgbm_pick.cpp):

  • Reads gfx_id from Handle::GetDeviceName(); abstains if the arch is not in the model vocab / arch-constants table.
  • Builds a 51-feature row, scores every solver in the vocabulary, and returns the argmax. There is no candidate masking, margin gate, or applicability second model.
  • Maps the chosen name to a solver::Id and abstains if this MIOpen build doesn't know it.

GPU features are sourced two ways:

  • Live device values via the Handle: cu_count, wave_size, l2_cache_total_kb, boost_clock_mhz, lds_size_per_workgroup_kb, vram_bytes. This adds two small Handle getters — GetL2CacheSize() and GetClockRateKhz() — implemented for HIP (hipDeviceGetAttribute), OpenCL (clGetDeviceInfo), and stubbed for NOGPU, mirroring the existing attribute-query getters in src/hip/handlehip.cpp.
  • Arch-invariant values via a compiled-in gfx_id → constants table (conv/heuristics/lgbm_models/gpu_constants.h).

Model metadata (lgbm_model_meta.json) installs to DATABASE_INSTALL_DIR and loads at runtime via GetSystemDbPath(). The hook is an early short-circuit in ai_heuristics.cpp::PredictSolver, gated by MIOPEN_DEBUG_LGBM_PICK (on by default) under MIOPEN_ENABLE_AI_IMMED_MODE_FALLBACK, mirroring the existing TunaNet dispatch.

The 6 derived workload features (flop_cnt/bytes_*/gflops/bandwidth_gbps) are fed as NaN: the values these were trained on are direction-aware in a way a runtime textbook estimate cannot reproduce, and validation showed NaN reproduces more reference picks than an estimate.

LightGBM treats NaN as a real branch direction.

Not changed: the TunaNet path and WTI fallback. When the picker abstains (unknown arch, or model recommends an unavailable solver) it falls through to the existing selection path unchanged.

Test Plan

  • CPU-only gtests in projects/miopen/test/gtest/lgbm_picker.cpp (target test_lgbm_picker):
    • CPU_LgbmMetadata — metadata loads, categorical encoding resolves, solver code matches vocab index.
    • CPU_LgbmPickerFixture.ReproducesReferenceArgmax — feeds each encoded feature row from lgbm_test_vectors.json through the scoring path and asserts the argmax solver matches the reference exactly.
  • Runtime check on a real GPU: build MIOpen and run a conv with MIOPEN_LOG_LEVEL=6, and confirm the lgbm picked …
    log line.

Test Result

  • Reference-fixture replay reproduces the expected argmax solver on 50/50 vectors.
  • Per-TU compile is clean for the picker, metadata loader, regenerated rank C, and the NOGPU handle. The HIP and OpenCL handle TUs are not built in the NOGPU backend, but the new getters mirror the existing attribute-query getters and the required hipDeviceAttribute* / CL_DEVICE_* enums were confirmed present.
  • A real-GPU smoke run are pending hardware; will update with results.

Risk Assessment

Risk level: Low

  • Impacted components: new files under conv/heuristics/lgbm_* and conv/heuristics/lgbm_models/; the PredictSolver hook in ai_heuristics.cpp; two new Handle getters across the HIP/OpenCL/NOGPU backends; src/CMakeLists.txt (sources, install, C-language enable); a new gtest. The TunaNet and WTI paths are untouched, and behavior is identical when MIOPEN_DEBUG_LGBM_PICK is disabled.
  • Potential side effects:
    • Functional: when enabled, immediate-mode picks on supported archs come from this model instead of TunaNet; unknown archs abstain and fall through.
    • Performance: one cached model load plus one predict per solver candidate; sub-millisecond per call, negligible versus tuning/search.
    • ABI/API: none (the new Handle getters are additive internal methods).
  • Mitigation steps:
    • Gated behind MIOPEN_DEBUG_LGBM_PICK (runtime toggle, no rebuild) and compiled only under MIOPEN_ENABLE_AI_IMMED_MODE_FALLBACK.
    • The shipped lgbm_test_vectors.json fixture makes picks reproducible and checkable pre-merge.
    • Self-contained and easily revertible (new files plus one guarded hook and additive getters).

Submission Checklist

jdcampbe and others added 8 commits June 8, 2026 10:39
Adds ai::lgbm::PickSolver, which scores candidate solvers using two
LightGBM models (rank + applicability) ported from
~/AutoResearchAllLGBM. Works across gfx90a-mi210x, gfx942-{mi300a,
mi300x,mi325x}, gfx950-{mi350x,mi355x}, gfx1150, gfx1151, and gfx1201.

When MIOPEN_FIND_MODE=2, short-circuits the ND TunaNet model with the
LGBM pick when applicable; falls through to TunaNet otherwise.
Abstains on unknown spec_id, low-confidence margin (per-spec
threshold), or applicability VETO. Gated by MIOPEN_DEBUG_LGBM_PICK
(on by default).

Inference uses Treelite --source-only generated C (checked in under
src/conv/heuristics/lgbm_models/{rank,appl}) so MIOpen has no runtime
dependency on libtreelite / libLightGBM. The generated predict()
exports are renamed via -Dpredict=lgbm_{rank,appl}_predict at compile
time to avoid symbol collision.

GPU features come from a static lookup table generated from
~/AutoResearchAllLGBM/GPUInfo/*.json at port time; the SKU is
resolved at runtime from Handle::GetDeviceName() +
GetMaxComputeUnits() + GetGlobalMemorySize(), so no JSON shipping is
required. gfx950 mi350x/mi355x are indistinguishable from HIP info
and default to mi355x (more conservative abstain thresholds).

Model metadata (categorical vocabularies, triple_vocab,
per-spec thresholds) ships as two JSON files under
DATABASE_INSTALL_DIR, loaded at runtime via GetSystemDbPath().

Per-TU compile-verified under the HIPNOGPU Docker dev image. No
real-GPU smoke run yet; no test fixture yet.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Treelite-generated rank/ and appl/ C sources both export predict,
postprocess, quantize, predict_unit0..7, the metadata accessors
(get_num_target, get_num_class, get_num_feature, get_threshold_type,
get_leaf_output_type), and the is_categorical global. The previous
wiring only renamed predict via -Dpredict=..., so the other 16
symbols clashed at link time.

Replace the single -D with a CMake loop that emits -D<sym>=lgbm_rank_<sym>
(or lgbm_appl_<sym>) for every exported symbol, applied per-file via
COMPILE_OPTIONS. Verified via nm that all rank/appl object-file
symbols are now disjoint.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
FillGpuFeatures already uses absolute feature-row indices (kIdxCuCount =
37, etc.). The caller was passing row.data() + kIdxCuCount, so each
write was offset twice and scribbled 31 entries past the end of the
69-element std::array on the stack. Symptom: every PickSolver call
that reached the rank-scoring step corrupted the caller's frame and
segfaulted on return, regardless of abstain/select outcome.

Pass the base pointer; FillGpuFeatures is self-indexing.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Upstream re-fit the policy thresholds against a realistic 3x
quick-tuning fallback cost (the previous compact thresholds were tuned
against a fictional 10x invalid penalty and were net-negative under
the corrected cost model):

  - margin abstain disabled: per_spec_thresh = {}, default = 1.0
    (margin = 1 + max(top - runner, 0) >= 1.0, so the gate never fires)
  - applicability VETO loosened: per_spec_appl_thresh = {},
    appl_prob_thresh = 0.10 (only rejects extremely-low-confidence picks)

Reported val metrics under 3x cost: geomean 1.0090, abstain 0.01%,
top-1 95.56%, coverage 99.70%, invalid 0.29%.

Model files (rank, appl) and feature schema (model_meta.json) are
unchanged; thresholds-only refresh.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Tracks the upstream AutoResearchAllLGBM v4/v5 simplification:

  - v4 dropped the applicability model entirely (appl threshold -> 0 with
    no quality loss under the 3x cost model) and removed margin abstain.
    The picker is now one model load, one predict per candidate, argmin.
    Deletes lgbm_models/appl/ and all appl wiring; PickSolver no longer
    runs a VETO or margin gate.
  - v5 pruned the rank feature set 69 -> 59 (10 zero-gain features
    removed: tensor_vect_type/length, convolution_mode, pad_mode,
    max_waves_per_cu, vgpr/sgpr_per_simd, peak_tflops_fp4,
    dtype_support_count, matrix_core_gen). Rewrites the kIdx feature-row
    constants, the GpuFeatures struct, FillProblemFeatures /
    FillGpuFeatures, and regenerates the GPU lookup table accordingly.
  - New rank model source is model_rank_pruned59_t800.txt; regenerated
    Treelite C (rank dir is now 48MB, was 72MB; appl's 29MB is gone).
  - triple_vocab moved to a top-level model_meta.json key; loader accepts
    both the v5 top-level location and the legacy appl-nested one.

Feed NaN for the perf-DB workload features (flop_cnt, bytes_*, gflops,
bandwidth_gbps) instead of textbook estimates. The DB byte counts are
direction-aware (read/write tensors swap for Bwd passes), which a forward
estimate cannot reproduce. A clean-room replay over the new 225-vector
deploy/test_vectors.json fixture confirmed NaN reproduces the reference
pick on 217/225 vectors vs 211/225 for the textbook estimate -- wrong
values mislead the trees more than missing ones do. Feature order, GPU
table values, and categorical encoding were verified bit-exact against
the deploy artifacts.

Adds the validation fixture as test/gtest/lgbm_test_vectors.json.

Per-TU compile-verified under the HIPNOGPU Docker dev image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
CPU-only tests (no GPU required), mirroring the TunaNet
conv_ai_nd_heuristics gtest layout:

  - CPU_LgbmMetadata: metadata loader sanity (52 solvers, triple_vocab
    candidates resolve to valid solver codes, categorical encoding +
    unknown-value handling, SolverCode==vocab index).
  - CPU_LgbmGpuTable: ResolveSpecId disambiguation (gfx942 by CU/VRAM,
    gfx950 -> mi355x, single-SKU archs, unknown archs abstain) and the
    spec_id_code == array-index invariant.
  - CPU_LgbmPickerFixture: rebuilds a ProblemDescription per
    lgbm_test_vectors.json entry, runs PickSolverForSpec, and asserts the
    picker never abstains (v5 property), never returns an unknown solver,
    and reproduces >=90% of the reference picks. The bound is below 100%
    because the ProblemDescription path cannot recover fixture rows with a
    NaN layout or missing perf-DB workload values (a clean-room replay of
    the exact C++ path reproduces 210/225 = 93.3%).

CMake stages lgbm_test_vectors.json into DATABASE_INSTALL_DIR so the
fixture is found via GetSystemDbPath() at run time; the test source is
auto-globbed into miopen_gtest.

Per-TU compile-verified under the HIPNOGPU Docker dev image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The upstream model dropped all curated per-SKU GPU data: v10 sources its
GPU features directly from the live device instead of a shipped JSON
table. Port the MIOpen integration to match.

Model/algorithm changes (v5 -> v10):
  - 51 features (was 59); dropped spec_id, l3_infinity_cache_kb, and all
    six peak_tflops_* (the JSON-only fields). Regenerated the rank
    Treelite C from model_rank_v10_pure_final.txt.
  - No spec_id anywhere: removed the per-SKU lgbm_gpu_table.cpp +
    lgbm_gpu_features.hpp (kGpuTable / ResolveSpecId) and the
    gen_gpu_table.py sidecar.
  - GPU features are now gathered at runtime: cu_count, wave_size,
    l2_cache_total_kb, boost_clock_mhz, lds_size_per_workgroup_kb and
    vram_bytes come from hipDeviceProp_t via the Handle; the 10
    arch-invariant fields come from a compiled-in gfx_id -> constants
    table (gpu_constants.h, generated by gen_gpu_constants_header.py).
  - Score the full solver vocabulary and take the global argmax. v10
    removed the spec_id-keyed candidate masking; a clean-room replay
    confirmed full-vocab argmax reproduces the reference picks (50/50)
    while the old masking matched only 17/50.

Handle: add GetL2CacheSize() and GetClockRateKhz(), implemented for HIP
(hipDeviceGetAttribute), OpenCL (clGetDeviceInfo), and stubbed for NOGPU.

Metadata loader trimmed to vocab + solvers (dropped triple_vocab and the
dead margin/appl threshold machinery); kNumFeatures 59 -> 51; removed the
now-unused lgbm_per_spec_thresh.json.

Tests: rework lgbm_picker.cpp for the new fixture format -- the v10
test_vectors ship the fully-encoded 51-feature row, so the new
ScoreRowArgmaxForTest seam validates the scoring + argmax path exactly
(50/50) without a ProblemDescription round-trip. Dropped the obsolete
GPU-table test.

Per-TU compile-verified under the HIPNOGPU Docker dev image (HIP/OpenCL
handle TUs are not built in that backend, but the new getters mirror the
existing attribute-query getters and the required HIP/CL enums exist).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Refresh the rank model from model_rank_v11_runtime.txt. Same 51-feature
runtime-pure schema as v10 (no spec_id / l3 / peak_tflops; GPU features
from hipDeviceProp_t + gpu_constants.h), so no C++ changes are needed --
only the regenerated Treelite tree TUs, model_meta.json, and the
validation fixture change.

The retrain reverts the v9/v10 data filter that excluded Xdlops solvers
on RDNA3: those solvers are applicable on RDNA3 via Composable Kernel
(MIOpen's IsApplicable has no arch gate), so the filtered training data
was wrong. Oracle val geomean 1.0147, top-1 94.5%.

Validated: full-vocab argmax reproduces the refreshed test_vectors.json
50/50. Regenerated rank C compile-checked under the HIPNOGPU Docker image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@astrelsky

Copy link
Copy Markdown
Contributor

Motivation

MIOpen's immediate-mode solver selection uses per-architecture TunaNet models, which need a separately trained model per arch and cover only a subset of the GPU lineup. This PR adds a single cross-architecture solver picker driven by a gradient-boosted (LightGBM) rank model that selects a convolution solver from one model across gfx90a, gfx942, gfx950, gfx1151, gfx1201, and the gfx110x family.

This is still only a subset of the supported GPU lineup though, unless I misunderstand something.

jdcampbe and others added 2 commits June 15, 2026 08:31
DataTypeName used a switch over miopenDataType_t that listed only the
four dtypes in the model vocab plus a default. MIOpen builds with
-Werror -Wswitch-enum, which flags every unlisted enumerator even when a
default is present, so the full gfx942/gfx950 build failed (the HIPNOGPU
per-TU compile-check does not pass -Wswitch-enum, which is why it slipped
through). Replace the switch with an if-chain; semantics unchanged
(unhandled dtypes still map to "", the missing category).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Remove all curated per-architecture GPU data so the model is sourced
entirely from the runtime device and can project to unseen
architectures.

Model (v5/v10/v11 -> v16): retrained to 41 features. Dropped every GPU
feature not directly readable from hipDeviceProp_t -- the 8 HSA-only
fields (simds_per_cu, l1_cache_kb_per_cu, shader_engines,
cacheline_size_bytes, xcd_count, mfma_shape_count, winograd_support,
asm_implicit_gemm_support) plus lds_size_per_cu_kb and arch_family. The
only GPU inputs are now cu_count, wave_size, lds_size_per_workgroup_kb,
l2_cache_total_kb, boost_clock_mhz, vram_bytes (all hipDeviceProp_t) and
gfx_id (gcnArchName). Oracle val geomean 1.0147, top-1 94.6% --
unchanged from v11.

C++:
  - Deleted the embedded gpu_constants.h table and its generator; nothing
    in the picker reads curated arch data anymore.
  - Rewrote FillGpuFeatures to populate only the six hipDeviceProp_t
    numerics + gfx_id; new 41-feature index map; kNumFeatures 51 -> 41.
  - Architecture gating retained: the picker runs only on gfx_ids in the
    model vocab and abstains (falls through to TunaNet) otherwise. The
    feature set is fully runtime-derived, so lifting the gate later allows
    projection to new silicon without any data change.

Regenerated rank Treelite C from model_rank_v16s_strict.txt; refreshed
model_meta.json and the test fixture. Full-vocab argmax reproduces the
new test_vectors 50/50. Per-TU compile-checked (incl. -Wswitch-enum)
under the HIPNOGPU Docker image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@jdcampbe

Copy link
Copy Markdown
Contributor Author

This is still only a subset of the supported GPU lineup though, unless I misunderstand something.

You're correct. In theory it should work across all architectures, but there is an artificial gate to include architectures that we have some data for. The artificial gates will be removed as we test on other architectures.

@yassinsolim

Copy link
Copy Markdown
Member

I went through the latest CI run (v16, commit 47cf542). Good news is the failures are all mechanical or packaging related, nothing in the model logic itself. Here is the breakdown.

1. miopen_gtest fails to link (breaks HIP Package, gfx94X, gfx950, and gfx1151)

All four of those red checks die at the same place, linking bin/miopen_gtest:

ld.lld: error: undefined symbol: miopen::ai::lgbm::LgbmMetadata::Get()
ld.lld: error: undefined symbol: miopen::ai::lgbm::LgbmMetadata::CategoricalCode(...)
ld.lld: error: undefined symbol: miopen::ai::lgbm::LgbmMetadata::SolverCode(...)
ld.lld: error: undefined symbol: miopen::ai::lgbm::ScoreRowArgmaxForTest(...)

libMIOpen compiles fine, so this is a symbol visibility problem. The library is built with CXX_VISIBILITY_PRESET hidden and VISIBILITY_INLINES_HIDDEN (src/CMakeLists.txt), so anything not explicitly exported stays hidden, and lgbm_picker.cpp cannot resolve those symbols at link time. The declarations in lgbm_metadata.hpp and lgbm_pick.hpp are missing MIOPEN_INTERNALS_EXPORT. ai_candidate_selection.hpp is a good reference since it annotates its test-facing methods for exactly this reason.

Marking LgbmMetadata::Get, CategoricalCode, SolverCode, and ScoreRowArgmaxForTest with MIOPEN_INTERNALS_EXPORT should clear the package build and all three arch builds in one shot.

2. clang-tidy

Two separate things:

  • ai_heuristics.cpp:749 trips readability-redundant-preprocessor (nested redundant #if). The new #if MIOPEN_ENABLE_AI_IMMED_MODE_FALLBACK block is already inside a block guarded by the same macro.
  • The generated Treelite C files (lgbm_models/rank/tu0.c through tu7.c, quantize.c) fail under -warnings-as-errors='*' with readability-function-size (76k statements vs the 800 threshold) and bugprone-assignment-in-if-condition from the LIKELY/UNLIKELY macros. The -w flag silences the compiler but not clang-tidy, so the generated TUs need to be excluded from the tidy target, for example a .clang-tidy with Checks: '-*' dropped into the rank/ directory.

3. pre-commit

Still failing on trailing whitespace, end-of-file-fixer, black, and clang-format (mostly the generated model files plus gen_gpu_constants_header.py), and the GTest naming hook is rejecting the CPU_LgbmMetadata and CPU_LgbmPickerFixture fixtures. Running pre-commit run --all-files locally should clean up most of it.

A few design questions, separate from CI

Since this changes the default immediate-mode selection path on supported archs, I would like to understand a few things before it lands:

  • The 6 derived workload features are fed as NaN. Can you share the validation behind NaN reproducing more reference picks than a textbook estimate?
  • The picker takes a pure argmax over the full solver vocab and only checks IsApplicable afterward in the hook. How often does the top pick get rejected and fall through to TunaNet, and is that cost acceptable on the hot path?
  • The real GPU smoke validation is still marked pending. I would want to see that result before approving.

On the arch coverage point above, thanks for clarifying that the gating is intentional and temporary. It might be worth a short note in the code or PR description saying the gfx_id allowlist will expand as more architectures get validated, just so it does not read as a hard limitation later.

jdcampbe and others added 6 commits June 16, 2026 13:31
Refresh the rank model from model_rank_v17_0616.txt. Same 41-feature
HIP-only schema as v16 (no embedded GPU data; inputs are the six
hipDeviceProp_t fields + gfx_id), so no C++ changes are needed -- only
the regenerated Treelite tree TUs, model_meta.json, and the validation
fixture.

The retrain pulls in additional data (notably more gfx90a and gfx1100
coverage) and grows the solver vocabulary 70 -> 79 (new Winograd
multipass WrW variants and a direct 11x11 solver); the picker reads the
vocab dynamically so this needs no code change. Oracle val geomean
1.0142, top-1 94.7%.

Validated: full-vocab argmax reproduces the refreshed test_vectors 50/50.
Regenerated rank C compile-checked under the HIPNOGPU Docker image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
… output)

Promote the TunaNet+Align model: 61 features = the 41 HIP-only base
features + 20 engineered derived features the C++ caller computes.

Derived features (no new embedded data; all from conv dims + cu_count):
  - 13 tn_* GEMM-geometry features (indices 28-40) are computed by reusing
    MIOpen's production miopen::ai::common::EngineeredConvFeatures verbatim
    -- the same single-source-of-truth the TunaNet/candidate-selection
    encoders use, so the C++ and the Python training pipeline cannot drift.
    H_out/W_out are taken from the output descriptor (exact). 2D geometry is
    used for all convs, matching how the model was trained; 3D extent is
    still carried by the base depth/spatial features.
  - 7 al_* tile-alignment features (indices 41-47): integer divisibility /
    last-64-tile under-fill of channels/output_channels/batch.

Ranked-list output, lazy applicability downstream:
  - The shipped model requires candidate masking (full-vocab argmax matches
    the reference only 7/60; masked candidate scoring matches 60/60). Rather
    than mask inside the picker, PickSolverRanked now scores the full vocab,
    sorts by predicted speed, and returns the ranked solver-id list. The
    existing GetSolutionsFallback caller walks that list applying
    IsApplicable lazily (the TunaNet contract) -- typically 1-3 checks, no
    spec_id, no embedded GPU data. Replaces the prior single-solver
    PickSolver + post-hoc applicability check.

Regenerated rank Treelite C from model_rank_median_tn_align_t600.txt;
refreshed model_meta.json + the test fixture (new format: per-vector
candidate_solvers + feature_matrix + expected_scores). The gtest now scores
each vector's candidate matrix and asserts argmax == argmax_solver.

Validated: candidate-matrix argmax reproduces the fixture 60/60 with
max|score-expected| = 0.0. Per-TU compile-checked (incl. -Wswitch-enum)
under the HIPNOGPU Docker image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…sion

The LGBM ranker over-selected ConvDirectNaiveConv* on out-of-distribution
shapes (~6x more than develop's ND TunaNet), causing ~4-20x slowdowns
(results correct, perf only). Root cause: the ConvDirectNaiveConv*
fallbacks are ALWAYS applicable, and the model frequently ranks one #1 on
unseen shapes; MIOpen's fallback walk keeps applicable solvers in rank
order and picks the best-ranked, so a #1 naive solver wins over a far
faster real kernel that ranked lower.

Fix (ranking only; no model retrain, no .so change): in PickSolverRanked,
demote every naive fallback below all non-naive solvers (relative score
order preserved within each group). A naive solver is now reached only
when no non-naive solver applies -- matching develop's ND TunaNet, which
ranks naive last. The naive set is read from
model_meta.json -> rank.naive_fallback_solvers (not hardcoded);
ConvDepthwiseFwd2D is a real optimized kernel, is not in that list, and is
not demoted. The walk stays lazy (the caller still applies IsApplicable
only to solvers it reaches).

model_meta.json refreshed to the same v18 model with the added
naive_fallback_solvers key (feature_order/solvers/vocab and test_vectors
byte-identical). The gtest test seam does raw argmax and is unaffected.

Offline gold-val for this policy: naive pick 51.6% -> 0.09%, coverage
0.51 -> 1.00, geomean on non-naive-best shapes unchanged (1.0136 ->
1.0132). Per-TU compile-checked (incl. -Wswitch-enum) under HIPNOGPU.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous fix demoted ConvDirectNaiveConv* unconditionally, which
over-corrects on grouped convs: at high group counts naive is genuinely
the fastest solver a large fraction of the time (~40% at groups>=512), so
forcing a non-naive pick swaps in the slow grouped-XDLOPS solver and
regresses hard (offline gold-val geomean 1.3649 on groups>=64).

Per deploy/README_CPP_DERIVED.md, gate the demotion on group count: demote
naive only when GetGroupCount() < rank.naive_guard_max_groups (default 64,
read from model_meta.json); at or above the threshold, leave the raw score
order so naive can win on merit. Low-group OOD shapes still get the naive
fix (the ~6x over-selection / ~4-20x slowdowns this addresses).

model_meta.json refreshed to add naive_guard_max_groups (same v18 model;
test_vectors byte-identical). Offline gold-val for the group-aware policy:
naive 5.4%, geomean(all) 1.0204, geomean(groups>=64) 1.0308 (vs 1.3649 for
the unconditional guard), geomean(groups<64) 1.0201.

Per-TU compile-checked (incl. -Wswitch-enum) under the HIPNOGPU Docker
image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Refresh the rank model from model_rank_expanded_tnalign_t600.txt
(expanded-data retrain: 866k gold rows, 13 specs). Same 61-feature
TunaNet+Align schema, same gfx_id vocab, same naive-guard fields
(naive_fallback_solvers + naive_guard_max_groups=64), so no C++ changes
-- the picker reads the solver list dynamically.

The retrain grows the solver vocabulary 70 -> 72, adding
ConvHipImplicitGemmFwdXdlops and ConvHipImplicitGemmBwdXdlops (the
grouped-XDLOPS family the ranker previously over-valued vs naive; giving
the model their non-grouped variants as first-class candidates is the
data-side complement to the group-aware naive guard). Reported oracle
gold-val geo 1.0124 / top1 93.2%.

Regenerated rank Treelite C; refreshed model_meta.json and the test
fixture (65 vectors). Candidate-matrix argmax reproduces the fixture
65/65 with max|score-expected| = 0.0. Per-TU compile-checked (incl.
-Wswitch-enum) under the HIPNOGPU Docker image.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@jdcampbe

Copy link
Copy Markdown
Contributor Author

This PR will be merged in with #9680

@jdcampbe jdcampbe closed this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants