Repository navigation
feat(hipblaslt): add adaptive sm_count_target mode - #13263
Conversation
❌ PR Check — Action Required
📖 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. |
Codecov Report❌ Patch coverage is ❌ Your project check has failed because the head coverage (65.71%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## users/csalvado/adaptive-sm-estimator #13263 +/- ##
========================================================================
- Coverage 76.12% 76.08% -0.04%
========================================================================
Files 2887 2889 +2
Lines 484232 484520 +288
Branches 73039 73093 +54
========================================================================
+ Hits 368591 368627 +36
- Misses 90401 90642 +241
- Partials 25240 25251 +11
*This pull request uses carry forward flags. Click here to find out more.
🚀 New features to boost your workflow:
|
16ba5d4 to
0e6462f
Compare
0e6462f to
4186f5a
Compare
9629960 to
df88170
Compare
2cf9244 to
f75f2fb
Compare
f75f2fb to
b367f23
Compare
50ada6d to
6a74b20
Compare
6a74b20 to
824bd49
Compare
824bd49 to
3b45434
Compare
3b45434 to
eee0c5e
Compare
Alex-Vasile
left a comment
There was a problem hiding this comment.
Approving CMakeLists changes
eee0c5e to
91a50a1
Compare
91a50a1 to
e874d92
Compare
e874d92 to
026f1f9
Compare
With HIPBLASLT_ADAPTIVE_SM_COUNT set, a co-tenant that takes whole CUs otherwise leaves SK5 grids sized for the full device. Every few C API launches on a stream now carry the occupancy probe; its reading feeds the stream's estimator, whose hint becomes the implicit sm_count_target for the heuristic query and for launches. The top query result reports enough workspace for a regrid under a later hint. A per-handle controller owns the probe buffer, the per-stream estimators and the published hints. Each launch holds its adaptive state in an RAII object whose end records the probe's completion event, and TensileLite marks the kernel invocations that carry the probe. The probe events are created with the controller; if creating one fails, the handle runs without adaptive mode and logs why.
A varying adaptive hint can change the timed kernel mid-run, so the timing clients exit unless HIPBLASLT_ADAPTIVE_SM_COUNT_FORCE fixes it.
026f1f9 to
da2a1c1
Compare
…rnels (#13692) JIRA ID : N/A ## Motivation hipBLASLt can steer kernel choice with an `sm_count_target` hint when part of the GPU is busy, but nothing measures how many CUs are actually free. This PR adds the measuring part: an opt-in probe in StreamK kernels with hybrid work assignment that records which CUs a launch ran on. It is the first PR of a stack: #13892 (estimator), #13263 (adaptive launches) and #13893 (re-selection) use it to set the hint automatically. ## Technical Details - New codegen option `--emit-occupancy-probe` (`TENSILELITE_EMIT_OCCUPANCY_PROBE=ON`, `invoke build --emit-occupancy-probe`). Off by default. - With it on, StreamK hybrid-assignment kernels on gfx950 get three extra kernel arguments at the end (`ProbeAddr`, `ProbeEpoch`, `ProbeGrid`) and a few instructions at the start that write the launch's epoch into the slot of the CU they run on. - The host can pad a launch with extra "probe-only" workgroups so it sees the whole GPU; those exit right after marking. - Nothing calls this yet: without a caller every probe address is null and kernels behave as before. - CHANGELOG entry for the build option. ## Test Plan - TensileLite unit tests: gate, probe emission, argument layout, end-to-end codegen (metadata offsets match the probe loads; nothing else stores before the probe-only exit), library metadata. - C++ `tensilelite-tests`: argument packing and padding (batched, 2-D grid, grouped GEMM), serialization. - Flag-off build compared against develop. - MI355X, with the rest of the stack as the caller: probe readings with and without `hipblaslt-cotenant`; `-v` correctness, including strided and general batched GEMM. ## Test Result - All tests pass. - With the flag off, generated kernels and library files are byte-identical to develop. - With the flag on, register usage of every kernel is unchanged. - Probe readings match the number of free CUs; results are correct with the probe on every launch. ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests. ## Risk level Low. Opt-in at build time; off by default with identical output, and inert without a host caller. --- ## Appendix ### What the probe writes ``` host-visible buffer (512 × u32 per stream) slot = XCC << 6 | SE << 4 | CU (unique per CU on gfx950) 512 slots = 8 XCC × 4 SE × 16 CU ids; MI355X uses 256 (CU ids 0–8 per SE) launch with epoch 1042: buffer after the launch WG on XCC0 SE0 CU3 ──► slot 3 = 1042 WG on XCC0 SE1 CU0 ──► slot 16 = 1042 WG on XCC5 SE2 CU7 ──► slot 359 = 1042 CU busy with another app → slot untouched (older epoch) ``` Count of slots holding the epoch = CUs this launch could use. ### Kernel prologue (every workgroup; thread 0 stores) ``` load ProbeAddr, ProbeEpoch, ProbeGrid read CU id (HW_ID, XCC_ID) → slot (while the loads are in flight) wait for the loads ProbeAddr == 0 ? ──yes──► continue as normal kernel │ no thread 0 stores epoch to ProbeAddr[slot] raw workgroup id ≥ ProbeGrid ? ──yes──► exit (probe-only workgroup) │ no continue as normal kernel ``` ### Probe-only workgroups (padding) ``` real grid = 224 WGs (< 256 CUs) dispatch = 256 WGs ┌──────────── 224 real WGs ───────────┐┌─ 32 probe-only ─┐ do the GEMM mark CU, exit ``` - Only the dispatch size grows; every grid-derived kernel argument (`numWorkGroups`, `SKGrid`, workgroup mapping) keeps the real grid. - Probe-only workgroups exit before touching memory, flags or the work queue, using the raw (pre-remap) workgroup id. ### Notes - Gated to StreamK hybrid-assignment kernels on gfx950, non-grouped, non-custom; gfx942 is off until its CU layout is verified on hardware. - CUs whose id falls outside the 512-slot layout skip the mark instead of aliasing. - A probe-built library needs a runtime that passes the probe arguments (this PR's host code does). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JIRA ID : N/A **Stack:** #13692 (kernel probe) → **this PR** (estimator) → #13263 (adaptive launches) → #13893 (re-selection). Review in that order. ## Motivation The probe from #13692 tells the host which CUs a launch ran on. This PR adds the policy that turns those readings into a per-stream CU hint: when to probe, how to read a probe, and when the hint changes. It is pure logic (no HIP calls), so it can be reviewed and tested on its own; #13263 wires it into launches. ## Technical Details - New header `rocblaslt_adaptive_sm_count.hpp` (HIP-free), namespace `rocblaslt::adaptive_sm`: - `Config`: the `HIPBLASLT_ADAPTIVE_SM_COUNT*` environment variables, read once, invalid values warn and keep the default. It also answers the two questions `_FORCE` raises for callers: `hintVaries()` (can timing see the hint change) and `tracksLaunches()` (do launches update the estimator: not under `_FORCE` unless `_FORCE_PROBE`). - `Estimator`: one stream's state over its probe slots. `beforeLaunch` reads a finished probe and returns the epoch to probe with (or none); `afterProbe` reports whether that probe was recorded. It does not know about `_FORCE`; the caller in #13263 substitutes the forced hint. - `applies` (does the adaptive hint apply to a matmul) and `tinyProblem` (too small to probe). - `countFrom`: CUs a probe marked; `ProbeEpoch`: probe epochs and the periodic slot clear. - `ReadingWindow` and `candidate`: the last readings, and the hint they give. - `normalizeEnv` moves to `rocblaslt_secure_env.hpp` so the header and `handle.cpp` share it. - Nothing calls the estimator yet; behaviour is unchanged. ## Test Plan - `hipblaslt-test-hostunit` (`HostUnitAdaptiveSmCount*`): the estimator driven launch by launch over a hand-filled slot array (probe ticks, misses and reset, a due probe waiting for the previous one, failed events, readings to hint, a full reading clearing the hint at once), epoch wrap and clear, environment parsing and the `Config` FORCE rules. ## Test Result - All host unit tests pass (runs on every CI tier, no GPU needed). ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests. ## Risk level None. New header and tests only; nothing calls it yet. --- ## Appendix ### Per-launch decisions (one stream) ``` every launch : pending probe finished? → reading → maybe new hint start, tick : a probe is due from the first launch, and again every PROBE_EVERY-th launch (a tick) due probe : taken by the first launch that can take it not tiny (< TINY_MFLOPS) and no probe pending a pending probe defers it (not a miss) missed : a tick that finds the probe still due (not taken since the previous tick) and no probe pending; 2 misses in a row → hint 0. A recorded probe restarts the count ``` At most one probe per stream is pending, so a newer probe never overwrites an unread one: each reading is exactly one probe. ### Reading a probe - Each probe has an epoch; its workgroups store it in the slot of the CU they run on. - Reading = slots holding that epoch or newer (signed 32-bit compare, so the wrap is safe); 0 means never written. - Every 2³⁰ epochs the stream's slots are zeroed before the next probe, so no stale mark can ever compare as newer. ### Readings → hint (window = last `PROBE_WINDOW` readings, default 3) | Window | Hint | |---|---| | a hint is set and one reading is at the full CU count | 0 at once; the window restarts from that reading | | all at the full CU count | 0 (no hint) | | all below the current hint | the smallest reading (exact) | | all above the current hint | the smallest reading (exact), 0 if it is the full count | | otherwise | unchanged | A reading of 0 is ignored. Requiring the whole window on one side filters one-off readings (e.g. a cotenant workgroup briefly preempted). ### Knobs (prefix `HIPBLASLT_ADAPTIVE_SM_COUNT`) | Var | Default | Meaning | |---|---|---| | (unsuffixed) | off | enable | | `_PROBE_EVERY` | 8 | launches between probes | | `_TINY_MFLOPS` | 256 | GEMMs below this never probe | | `_PROBE_WINDOW` | 3 | readings that must agree to change the hint | | `_LOG` | 0 | 1: config, hint changes, failed probe events, re-selections (#13893); 2: also readings. Timestamped lines to `HIPBLASLT_LOG_FILE` or stderr | | `_FORCE` | – | fixed hint (timing) | | `_FORCE_PROBE` | off | with `_FORCE`: probe but ignore readings | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
TheRock Submodule Bump ActivityNewest first
|
…anges (#13893) JIRA ID : AIHPBLAS-5323 **Stack:** #13692 (kernel probe) → #13892 (estimator) → #13263 (adaptive launches) → **this PR** (re-selection). ## Motivation With #13263 the adaptive hint ranks solutions at query time and sets the StreamK grid at launch. Frameworks usually query once and cache the algo, so when contention changes later the cached solution keeps running, only regridded. This PR lets the launch switch to the solution the heuristic would pick at the current hint. ## Technical Details - **Tag at query** (`rocblaslt_auxiliary.cpp`): the top heuristic result is tagged with the hint it was ranked under (`rocblaslt_matmul_algo::data[4..7]`; `tagAlgo` / `algoTag` in `rocblaslt_adaptive_sm_count.hpp`). - **Re-select at launch** (`tensile_host.cpp`, `reselectForAdaptiveHint`, called in `runContractionProblem` right after `updateTensileProblem`): when the hint of the launch's `adaptive_sm::Launch` (#13263) differs from the tag, the heuristic's pick at that hint runs if it fits the caller's workspace; otherwise the query's solution runs. Launches without a `Launch` (mode off, ext API, grouped GEMM) never re-select. - **Graph capture**: a launch on a capturing stream is not adaptive (#13263), so it has no `Launch` and is never re-selected: it runs the tagged solution as given. - **Memo** (`AdaptiveReselectionMemo` on `TensileDataGemm`, i.e. per matmul descriptor): the last 4 choices, keyed by the launch problem (shape, types, launch hint, caller workspace), the tagged solution, its query hint and the query's workspace limit. A repeat launch skips the ranking. - **Never replaced**: algos obtained by index, lower-ranked results and tuning-file or tuning-cache (`HIPBLASLT_TUNING_MODE=cache|tune`) entries (they are not tagged). - **Tune mode**: re-selection runs before a first-matmul benchmark; the query-hint fallback is re-applied after the benchmark rewrites the problem. The adaptive hint is not part of the tuning key. - **Uniform summation order**: re-selection runs too; the pick must also keep that order at the launch hint (`uniformSummationOrderSupported`, the check heuristic selection uses). - Docs: re-selection notes in the Stream-K how-to; `_LOG=1` also logs re-selections; CHANGELOG entry. ## Test Plan - `hipblaslt-test-hostunit`: tag round trip and version check. - `hipblaslt-test` GPU tests (FORCE-only cases skip otherwise): - a result tagged with another hint re-selects once, and a repeat launch hits the memo; - a result ranked at the full device is swapped for the solution picked at the forced hint, and that one launches; - a launch on a capturing stream runs such a result as given, and the next live launch re-selects it; - under uniform summation order it re-selects too and the launch passes the order's check; - with no workspace at all the launch still re-selects to the StreamK pick (it needs less workspace when given less); - StreamK OFF results are never tagged; - a tuning-file entry is never tagged, so never re-selected. - Manually: the adaptive, SK, tuning and uniform-summation suites with `HIPBLASLT_ADAPTIVE_SM_COUNT=1`, `_FORCE=128` and `_FORCE=128 _FORCE_PROBE=1`. - Launch overhead and ranking cost measured on MI355X (Test Result). ## Test Result - All suites pass; with the mode off behaviour is unchanged. - Under contention, launches from a cached algo run the solution chosen at the measured hint. - Once memoised, a re-selecting launch costs ~0.7 µs more host time than a plain one. - Capturing 16 GEMMs takes 211 µs of host time (172 µs with the mode off), down from 2643 µs when captured launches were re-selected. --- ## Appendix ### From query to launch ``` hipblasLtMatmulAlgoGetHeuristic (rocblaslt_auxiliary.cpp) rank under the query hint (#13263) → top result A adaptive, not from a tuning file → tagAlgo(A, query hint); workspace ≥ 128 MiB │ caller caches A ▼ hipblasLtMatmul(A) (rocblaslt_mat.cpp, #13263) Controller::begin → Launch { hint = the stream's current hint, probe } ▼ runContractionProblem (tensile_host.cpp) updateTensileProblem: data->problem holds the launch hint and workspace reselectForAdaptiveHint(A) → A, or a copy of A with another solution index look up the solution, build kernels, launch ``` The caller's algo is never modified: a swapped solution runs through a local copy of it. ### Launch-time decision ``` launch with algo A │ ├─ no Launch, or A not tagged (by index, lower rank, tuning file/cache) ──► run A │ ├─ launch hint == A's tag ────────────────────────────────────────► run A │ └─ hint differs memo hit? ──yes──► run the remembered choice │ no heuristic's pick P at the launch hint (ranked with the query's workspace limit) ├─ P fits the caller's workspace ──► run P └─ otherwise ──► run A, at the launch hint if A fits there, else at A's tag (the query hint) remember the choice ``` "Fits" means `requiredWorkspaceSize` at the caller's workspace is within it and, under uniform summation order, the order holds at the launch hint. StreamK solutions need less workspace when given less, so on gfx950 the pick fit in every case tried, down to zero workspace; the fallback covers solutions whose workspace is fixed. ### What is remembered Per matmul descriptor, the last 4 re-selections, the oldest replaced first: | Key | Value | |---|---| | launch problem: shape, types, launch hint, caller workspace | solution to run | | tagged solution | whether it runs at the query hint instead of the launch hint | | query hint | | | query's workspace limit | | Example: a framework creates descriptor D for bf16 512×8192×8192, queries once at startup (hint 0, solution A tagged 0) and caches A. | # | Situation | Stream hint | What happens | Memo for D | |---|---|---|---|---| | 1 | GPU idle | 0 | tag 0 = hint 0 → A | – | | 2 | a cotenant takes 64 CUs | 192 | miss → rank at 192 → B fits → B; logs `reselect … hint=0->192 solution=A->B` | {@192 → B} | | 3 | same contention | 192 | hit → B | {@192 → B} | | 4 | the cotenant leaves | 0 | tag 0 = hint 0 → A; memo not consulted | {@192 → B} | | 5 | it returns with 128 CUs | 128 | miss → rank at 128 → C | {@192 → B, @128 → C} | | 6 | back to 64 CUs | 192 | hit → B | unchanged | | 7 | same D, 1024×8192×8192 | 192 | new launch problem → miss → own entry | 3 entries | | 8 | a smaller workspace is passed | 192 | the workspace is in the key → miss → fit re-checked | 4 entries | ### Tag `data[0..3]` keeps the solution index. `data[4]` bit 0 marks the result as re-selectable, `data[5]` is the tag version (an unknown version reads as untagged), `data[6..7]` the hint at query. Untagged algos (all of them with the mode off) behave as before. ### Workspace The tagged result already reports `min(MAX_WORKSPACE_BYTES, max(required, 128 MiB))` (#13263). A re-selected solution must fit the workspace actually passed to `hipblasLtMatmul`; otherwise the query's solution runs. ### Limits - The memo is per descriptor: callers that create a descriptor per call start empty each time. They still hit TensileLite's ranking cache. - The tag holds one query hint: a query made while the hint is 192 tags its result 192, and its launches re-select only once the hint moves away from 192. - Graph capture: captured launches are not adaptive, so they are never re-selected. A replay runs the queried solution at hint 0 (#13263). - The query hint is the latest hint published on any stream (#13263). With per-call queries and different hints on two streams, launches on one of them keep mismatching and re-select through the memo or the ranking cache.
JIRA ID : N/A
Stack: #13692 (kernel probe) → #13892 (estimator) → this PR (adaptive launches) → #13893 (re-selection). Review in that order.
Motivation
sm_count_targethelps when part of the GPU is busy with another workload, but users rarely know what value to pass, and the right value changes over time. This PR measures how many CUs GEMMs can actually use (the probe from #13692, the estimator from #13892) and applies that as the hint automatically.Technical Details
HIPBLASLT_ADAPTIVE_SM_COUNT=1, with StreamK tile scheduling AUTO or ON and no explicitsm_count_target. The mode applies whatever the device or library; the hint only moves when the launched kernels carry the probe (gfx950, probe-built library).sm_count_target: the heuristic query ranks under it, and each launch uses it for the StreamK hybrid-assignment grid and static/dynamic mode. Swapping the solution at launch is feat(hipblaslt): re-select the top solution when the adaptive hint changes #13893.adaptive_sm::Controller(one per handle, null with the mode off) owns the probe slots in host-mapped memory and, per stream, a lock, a hipEvent, the feat(hipblaslt): add adaptive sm_count_target estimator #13892 estimator and the published hint._FORCEis applied there only.LaunchfromController::begin()(hint, probe target). Its destructor records the probe's event, so no return path can leave a probe reserved but unrecorded.KernelInvocation::occupancyProbe), so the host learns from TensileLite whether a launch probed rather than re-deriving its padding rules._FORCE) and leaves the stream's state alone, since a graph replays the captured kernel arguments.hipblaslt-bench,-sequence,-api-overhead) refuse unforced adaptive mode;_FORCE/_FORCE_PROBEgive fixed, measurable behaviour.Test Plan
hipblaslt-test-hostunit: the benchmark timing guard (estimator tests are in feat(hipblaslt): add adaptive sm_count_target estimator #13892).tensilelite-tests(PersistentArgumentLayout.*): the probe flag on kernel invocations: set exactly when the probe address is packed; unset for grouped GEMM and for a multi-dimensional grid too small to pad.hipblaslt-testGPU tests: query workspace, forced mode (AUTO and ON), a probe producing a reading, a cotenant holding n CUs (the reading is at most CUs − n), graph capture (a captured launch leaves the stream's state alone), the stream lock held through a probe launch and released after a non-probe launch. Mode-specific tests skip when their mode is not set, so CI runs them with the mode off.HIPBLASLT_ADAPTIVE_SM_COUNT=1,_FORCE=128and_FORCE=128 _FORCE_PROBE=1.HIPBLASLT_TEST_COTENANT_PROBE=1, needs an otherwise idle GPU since other processes taking CUs change the count): the reading beside a cotenant equals exactly CUs − n.hipblaslt-cotenant: correctness (-v, including batched GEMM) with and without contention, a driver where the cotenant appears and leaves, forced-mode bench runs.Test Result
sm_count_target.Submission Checklist
Risk level
Low. Opt-in at runtime (and needs a probe-built library); with the mode off, behaviour is unchanged.
Appendix
Block flow (one
hipblasLtMatmulcall)Timeline (one stream, a cotenant holds 64 of 256 CUs)
The host enqueues launches long before the GPU runs them, so a probe's result is read many launches after it was launched.
PROBE_EVERY = 8,PROBE_WINDOW = 3.PROBE_WINDOWreadings on the same side of the hint, so one odd reading does not move it. The exception is a reading of the full CU count, which clears the hint at once.Workspace
min(MAX_WORKSPACE_BYTES, max(required, 128 MiB)). 128 MiB matches origami's own partial-tile limit for StreamK grids; a launch that needs more falls back to a slower path, never fails.MAX_WORKSPACE_BYTESto your budget.What adapts
sm_count_target(desc / handle)Knobs
See #13892 (prefix
HIPBLASLT_ADAPTIVE_SM_COUNT:_PROBE_EVERY,_TINY_MFLOPS,_PROBE_WINDOW,_LOG,_FORCE,_FORCE_PROBE).Timing rule
A timed solution must never change mid-measurement, so benchmark clients refuse unforced adaptive mode.
_FORCE=<cus>times the solution adaptive mode picks at that hint; adding_FORCE_PROBE=1includes the probe cost.🤖 Generated with Claude Code