Repository navigation
refactor(miopen): unify slow-solver handling behind a single speed class, fixing conv Find watchdog resets (TDR) - #11160
BradPepersAMD wants to merge 4 commits into
Conversation
✅ All Checks Passed — Ready for Review
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🎉 All checks passed! This PR is ready for review. |
|
This pull request has been inactive for 25 days and will be marked as stale. If you would like to keep this PR open, please:
This PR will be automatically closed in 5 days if no further activity occurs. |
|
This pull request has been automatically closed due to inactivity (30 days with no updates). If you'd like to continue working on this, feel free to reopen the PR or create a new one. |
…oid TDR MIOpen's Find step times every applicable solver by launching it. The naive convolution kernel is un-tiled, so on a large problem a single launch runs for seconds and merely benchmarking it trips the OS GPU watchdog (a TDR / driver reset) even when a fast solver also applies and would win. Skip *running* naive in Find's mini-benchmark when both a non-naive solver is applicable (computed from the full applicable list before workspace filtering, closing the leak where the alternative was workspace-filtered and naive ran anyway) and the problem's total MAC work exceeds a threshold (ConvDirectNaiveConvExceedsWorkLimit, ~16 GMAC, overridable via MIOPEN_DEBUG_CONV_DIRECT_NAIVE_MAX_WORK). Naive stays applicable at every size, so it still runs when it is the only option, and small problems where it is fastest (including all real depthwise convs) keep competing. Consolidates the naive-skip policy into one ShouldSkipNaiveBenchmark predicate, drops the reorder/out-parameter threading in EvaluateInvokers, and removes the superseded output-size-based MIOPEN_CONV_DIRECT_MAX_SIZE gate. Adds a CPU-only gtest for the work metric (no Handle, no kernel launch). Relates to #3509, #1860. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The naive-conv work gate skips *benchmarking* the un-tiled naive solver
during Find when a non-naive solver is applicable and the problem exceeds
the work limit. Applicability is not success, however: every applicable
non-naive candidate can still be rejected at evaluation (e.g. insufficient
workspace). Previously that left FindCore with an empty result and the
convolution failed outright ("No suitable algorithm"), even though naive
could have served it -- weakening the intended universal-fallback guarantee.
FindCore now re-evaluates with the skip disabled when the result would
otherwise be empty, so naive remains the true universal fallback. This only
triggers in the rare empty-result case; the already-rejected non-naive
candidates are re-rejected cheaply (workspace-filtered before any launch),
and the slow naive fallback is left non-optimal so it is not persisted to
find-db. The TDR win is preserved: naive is still skipped whenever any
solver actually succeeds.
Also:
- Reword the retention log from the misleading "Unable to Skip Naive Solver"
to a neutral "Retaining Naive Solver (<reason>)".
- Extract IsNaiveSolverId to dedupe the naive-id string test.
- CHANGELOG: record removal of MIOPEN_CONV_DIRECT_MAX_SIZE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64be73c to
7f7a941
Compare
MIOpen had four partly-overlapping mechanisms for "this solver is applicable but is a catastrophically bad choice here": MIOPEN_CONV_DIRECT_MAX_SIZE, MIOPEN_NAIVE_TIMEOUT, the MIOPEN_SEARCH_CUTOFF slow branch, and GetWti. Each acted too late, too bluntly, or without the cross-solver view the decision needs. Replace all four with one declaration point and one consumption rule. Declare: SolverInterface::GetSpeedClass(ctx, problem) returns Normal, Slow, or ExceedsLaunchBudget. Consume: Find's mini-benchmark and immediate mode's fallback both benchmark everything in the lowest class with an applicable member and defer the rest. Slow and ExceedsLaunchBudget are separate classes rather than one bool. With a single flag, "all candidates are slow" means nothing is skipped and naive is launched anyway -- and all-slow is most likely exactly when the problem is huge, i.e. precisely the regime the gate exists for. This is a pre-launch gate. Bounding how long Find *waits* cannot avert a TDR; the dispatch has already happened. A sole-candidate ExceedsLaunchBudget solver is still benchmarked, so no problem loses coverage. The naive threshold is derived from device capability (CUs, wavefront width, clock) through a measured MAC-per-lane-cycle rate, calibrated over 109 timed shape/direction points on gfx1151 (RDNA3.5, wave32) and gfx950 (CDNA4, wave64). Across the 39 pairs measured on both, gfx950 ran a median 5.04x faster against a 5.30x lane-cycle ratio, so the scaling holds to within ~5%. Removed: MIOPEN_CONV_DIRECT_MAX_SIZE; MIOPEN_NAIVE_TIMEOUT and its separate-stream machinery (stream_tracker, naive_timeout); the SEARCH_CUTOFF slow branch; a dead non_slow_succeeded out-parameter. Escape hatch: MIOPEN_DEBUG_DEFER_SLOW_SOLVERS=0 restores prior behaviour. Relates to #3509, #1860, #9513. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clang-tidy's bugprone-implicit-widening-of-multiplication-result flagged 60 * 1000 being evaluated in int before widening to the size_t constant. Matches the static_cast idiom already used by NAIVE_CONV_FALLBACK_MAX_WORK immediately below. No behavioural change; the product fits in int either way.
|
This pull request has been inactive for 25 days and will be marked as stale. If you would like to keep this PR open, please:
This PR will be automatically closed in 5 days if no further activity occurs. |
|
This pull request has been automatically closed due to inactivity (30 days with no updates). If you'd like to continue working on this, feel free to reopen the PR or create a new one. |
Summary
MIOpen has accumulated four independent, partly-overlapping mechanisms for the same
underlying problem: a solver can be applicable to a problem while being a catastrophically
bad choice for it. This PR replaces all four with a single declaration point and a single
consumption rule, and in doing so fixes a GPU driver watchdog reset (TDR) that could kill
the calling process during
miopenFindConvolution*.Net: +1081 / −1068 across 25 files.
The problem
Some solvers are deliberately applicable far outside the region where they are a sensible
choice, and this is by design:
problem size, or some convolutions become unsupported. Its kernel tiles within a grid,
but a problem is still covered by a single launch, so on a large shape (VAE-decode, 3D
convolution) that one launch runs for many seconds.
calls make it wildly uncompetitive.
Find measures every applicable solver by actually executing it. For naive this means the
OS GPU watchdog fires and resets the driver — even when a fast solver also applied and would
have won the benchmark. The solver does not have to be selected to hang the machine; being
benchmarked is enough. This is most visible on Windows, where the default TDR delay is 2 s
and the reset takes down the calling process, but the same mechanism exists on Linux.
Why the existing mechanisms are insufficient
MIOPEN_CONV_DIRECT_MAX_SIZEMIOPEN_NAIVE_TIMEOUT/_FACTORMIOPEN_SEARCH_CUTOFFslow branchGetWtiEach either acts too late (timeout), too bluntly (
MAX_SIZE,SEARCH_CUTOFF), or withoutthe cross-solver view the decision actually requires (
GetWti).The design
One declaration, one consumption rule.
Declare —
SolverInterface::GetSpeedClass(ctx, problem)returns:NormalSlowExceedsLaunchBudgetThis keeps the criterion with the solver that understands it: device-derived MAC work for
naive, arch-aware shape heuristics for explicit GEMM.
Consume — one comparison, applied identically by Find's mini-benchmark and by immediate
mode's fallback: benchmark everything in the best (lowest) class that has an applicable
member; defer everything worse.
Consequences, which are the whole policy:
Normalcandidate exists → every deferred solver is skipped and behaviour isbyte-identical to today.
Normal, severalSlow→ all of them are benchmarked and the least slow wins onmeasurement. Deferral ranks candidates; it does not discard a tier in favour of an
arbitrary survivor.
ExceedsLaunchBudgetremains → it is benchmarked. The universal-fallbackguarantee holds: no problem loses coverage. A huge sole-naive shape may still TDR — an
honest "extend coverage here" signal we deliberately do not mask.
SlowandExceedsLaunchBudgetare separate classes, not onebool. This is theload-bearing detail. With a single flag, "all candidates are slow" ⇒ nothing is skipped ⇒
naive is launched anyway — and all-slow is most likely exactly when the problem is huge,
i.e. precisely the regime the gate exists for. Separating them means a merely-wasteful
solver outranks a watchdog risk.
(
group == C == K⇒c_per_group == 1⇒ ~C× less work) never approach the limit, sonaive still wins where it is genuinely fastest.
This is deliberately a pre-launch gate. Nothing that bounds waiting can avert a TDR.
The naive threshold
Rather than a magic constant, the limit is derived from the device: compute units,
wavefront width, and clock, converted through a measured MAC-per-lane-cycle rate into a work
budget for a target launch time (default 1000 ms, clamped at 60 s). It therefore scales
across the CDNA/RDNA range instead of being tuned for one part. The re-measurement recipe is
documented in-tree above the constant in
conv_direct_naive_conv.cpp.On gfx1151 this yields a 22.27 GMAC limit at the default budget; on gfx950, 117.96 GMAC.
The constant was calibrated by forcing naive and timing it over 96 shape/direction points
on gfx1151 (RDNA3.5, wave32) and gfx950 (CDNA4, wave64). The result that matters is not the
constant but the scaling: across the 39 pairs measured on both parts, gfx950 ran a median
5.04× faster on shapes ≥ 5 GMAC against a 5.30× lane-cycle ratio, so deriving the limit from
device capability holds to within ~5% over a 5× spread of part size and across both
wavefront widths.
The known gap is that MAC count is not occupancy-aware: naive's grid is
group * n * k_per_groupblocks wide, so a shape whose(n, k_per_group)product is belowthe part's CU count under-fills it and runs under the model. Every measured point that fell
short is of that form — 3D convs at n=1, k=64 on a 256-CU part. These are under-gated:
safe for coverage, unsafe for TDR. Recorded in-tree rather than patched, because the fix is
a different metric, not a different constant.
The constant's role is a floor, and it was checked as one: forcing naive on the six
gfx1151 shapes the calibration sweep had refused to run — up to 154.6 GMAC, 6.9× the gate
limit — put the slowest of 18 points at an implied 2.97 against the modelled 3 (see
Validation). So the limit is not optimistic anywhere measured. It is pessimistic nearly
everywhere: the other 17 points run 1.5–3.9× faster than
predicted, and the binding term is direction, which the model does not carry — forward
sustains 7.9–11.6, backward-data 6.8–8.7, weights 3.0–7.1. The constant is in effect fitted
to weights-on-3D, the one case that touches it.
Escape hatch
MIOPEN_DEBUG_DEFER_SLOW_SOLVERS=0disables the mechanism entirely — every applicablesolver is benchmarked and offered. Intended for working around a
mis-classification (a solver deferred on a shape where it was in fact right) without a
rebuild. It re-exposes the TDR.
Verified live: with the gate on, a 128×512×512 / k=128 / 3×3 forward conv logs
Skipping last-resort (slow) Solver: miopenConvolutionFwdAlgoDirect:ConvDirectNaiveConvFwd;with the switch off, that solver is benchmarked.
Removed
MIOPEN_CONV_DIRECT_MAX_SIZEMIOPEN_NAIVE_TIMEOUT,MIOPEN_NAIVE_TIMEOUT_FACTOR, and the separate-stream evaluationmachinery behind them (
stream_tracker.hpp,handlehip.cppstream plumbing,naive_timeout.cpp,stream_tracker.cpp)MIOPEN_SEARCH_CUTOFF(its measured tuning/sampling bounds areretained unchanged)
non_slow_succeededout-parameter that was written but never readKnown limitations
speed class. This is accepted: a previously-recorded entry implies the benchmark already
completed on that machine, and every alternative considered (invalidating entries,
re-checking on load) was judged worse than the window it leaves open.
loaded CPU can drop a solver whose GPU time was fine. Pre-existing, deliberately not
addressed here — it is a separate concern and a separate fix.
Gemm*speed-class heuristics mis-fire, and this PR is what makes that reachable.Their thresholds are carried over unchanged, but on
developthey are consumed onlybehind
env::enabled(MIOPEN_SEARCH_CUTOFF)(find_solution.hpp:332,391); here they areconsumed unconditionally. A gfx1151 sweep found two shapes where the deferred
Gemm*solver was in fact the faster one (see Validation). The severity is sharply asymmetric:
when another performance solver survives the deferral the cost is ≤1.28×, but when the
only surviving
Normalcandidate is naive it is 7.07×, because naive is a coveragefallback rather than a competitor. The heuristics were evidently fitted on 2D data —
s = GetOutHeight() * GetOutWidth()carries no depth term, so a 3D conv's spatial work isundercounted by a factor of D — and
GemmFwdRest's own comment quotes FPR = 11–25%. Afalse-positive rate in that range is a different proposition as a default than as an
opt-in. Remedy is deliberately left open pending the CDNA sweep; the two candidates are
scoping these heuristics to
GetSpatialDimension() == 2, or refusing to defer when thesurviving
Normalset is naive-only.Validation
Completed (gfx1151, Linux/WSL2)
test_unit_conv_solver_naive_work_gateConvDirectNaiveConvFwdConvDirectNaiveConvBwdConvDirectNaiveConvWrwGemmFwdRest/GemmBwdRest/GemmWrwUniversalPlus:
MIOPEN_DEBUG_DEFER_SLOW_SOLVERSon/off behaviour confirmed against driver logs;clang-formatclean (18.1.4, matching the pinned revision).Gate on/off selection sweep, 13 shapes × 3 directions = 39 points, comparing the solver
Find selects and its benchmarked time against
MIOPEN_DEBUG_DEFER_SLOW_SOLVERS=0:Of the 14 points where the gate fired, 7 deferred naive (0.94–1.06×, the intended
behaviour) and 7 deferred a
Gemm*solver. Six of those left a Winograd solvercompeting and cost 0.85–1.28×. The seventh left naive as the only candidate:
conv3d_smallGemmBwdRestGemmBwdRest)iso_batchedGemmWrwUniversalThe 7.07× is 24× the worst inert-row excursion. The 1.28× sits inside that excursion, so it
was re-measured at 9 reps and reproduced (23.5 vs 18.3 ms) with a genuine solver change.
Both are analysed under Known limitations.
Above-the-limit check. The calibration sweep skips shapes it predicts will exceed the
budget, which leaves the threshold validated only from below. Forcing naive on all six
skipped gfx1151 shapes, smallest-first:
ramp_448dilate2conv3d_largeramp_768sdxl_vae_decodedw_dense_refThree results.
The gate is firing on real hazards, not hypothetical ones. Five of these eighteen
launches meet or exceed the stock Windows 2 s TDR delay —
dw_dense_refwrw at 4526 ms is2.26× over it, and a sixth point sits at 96% of it. These runs therefore are the failure
this PR exists to prevent, reproduced deliberately. They did not reset this host because
Linux/WSL2 has no 2 s watchdog; that is a property of the test box, not evidence of safety,
and it is why item 3 below is still required.
No point undershoots the constant over a 5× range of work above the limit, so the floor
holds. The single point that reaches it is the 3D weights conv (2629.7 ms measured against
2603.4 predicted, implied 2.97) — the documented low-occupancy case, still on the boundary
on a build that includes #7529's intra-grid spatial tiling.
The false-positive region is real: 8 of 18 points finish inside the 1000 ms budget
despite being gated,
ramp_448by 2.4–3.9×. Scored against the budget the gate is right on10/18; scored against the thing that actually breaks, 5/18. The difference is the deliberate
~2× headroom between a 1000 ms budget and a 2 s watchdog, which the directional spread above
justifies keeping. The cost of the over-fire is a benchmark naive would lose anyway at these
sizes, so this is not an argument for raising the limit.
ramp_768andsdxl_vae_decodeare the same shape, so their six points double as anunplanned replicate: 14% / 19% / 0.5% spread fwd / bwd / wrw.
Required before merge
gfx942 sweep would confirm the scaling holds on a part neither datapoint anchors.
Separately, the gfx950 sweep ran on a driver predating Optimize naive conv solver: file split, float acc, spatial tiling, bfloat16 #7529's intra-grid spatial tiling,
so its low-occupancy 3D outliers still need re-timing on a current build. The
above-the-limit check weakens the hope that they are a stale-build artefact: on gfx1151
with Optimize naive conv solver: file split, float acc, spatial tiling, bfloat16 #7529, the 3D weights case lands at an implied 2.97 — on the constant, not above
it. Re-time it on gfx950 before treating the margin there as recovered.
highest-risk item and is not a TDR concern. It is no longer hypothetical: the gfx1151
sweep above demonstrates it, so what remains is scope and fix, not existence.
Gemm*heuristics to 2D, or refuse to defer when thesurviving
Normalset is naive-only, or re-fit the thresholds).gfx11/gfx12 branch and are therefore not covered by the gfx1151 result. Compare
selected solver and achieved TFLOP/s, gate on vs
MIOPEN_DEBUG_DEFER_SLOW_SOLVERS=0.Windows host with the stock 2 s TDR delay.