Support runtime GEMM dimensions for dynamic shapes - #11
Conversation
6edd3e3 to
21f938d
Compare
|
/runtest h100 |
|
|
|
/runtest mi300x |
|
|
|
|
|
/runtest w7900 |
|
|
|
|
sanjibansg
left a comment
There was a problem hiding this comment.
LGTM!
One last comment, can you add a bit of documentation mentioning details like how to initialize the cache, etc.
|
As requested, added a README section covering the dynamic-shape behavior and how to initialize the cache limit |
|
hipBLASLt was missing the same implementation that was done for cuBLASLt, so this PR includes that as well. The only difference is that hipBLASLt has no Tests pass on AMD, MI100 |
sanjibansg
left a comment
There was a problem hiding this comment.
Some more comments on more descriptive methods and algorithmic reasoning. Comments mostly addresses the cuBLASLt code, but stand similar for the hipBLASLt ones as well.
|
|
||
| ## Dynamic GEMM shapes and the algorithm cache | ||
|
|
||
| Both GPU backends behave the same way here. One instance serves GEMM calls at sizes that vary at runtime: matrix layouts are not tied to a shape, and each call stamps its dimensions into a shared per-role descriptor right before the multiply. |
There was a problem hiding this comment.
Using Both GPU Backends is ambiguous, better state clearly what it infers here.
|
|
||
| Both GPU backends behave the same way here. One instance serves GEMM calls at sizes that vary at runtime: matrix layouts are not tied to a shape, and each call stamps its dimensions into a shared per-role descriptor right before the multiply. | ||
|
|
||
| Algorithm selection is cached. `addLayoutConfig(m, n, k, lda, ldb, ldc, transa, transb)` declares the largest shape a call site will use (its envelope) and resolves the algorithm for it once, up front. Any later call at a size covered by an envelope reuses that entry, so sweeping sizes does not re-query the heuristic or grow the cache. A call with no covering envelope is resolved at its exact shape and cached per shape, and if an envelope's algorithm cannot run a particular size the call falls back to exact-shape resolution. |
There was a problem hiding this comment.
This paragraph is not very clear as to what it addresses. We need to state clearly what Algorithm selection here means, what envelope here is, etc. Code documentation are better descriptive of the algorithms, features, and declarations it entails
| if (!L) { | ||
| CHECK_CUBLAS(cublasLtMatrixLayoutCreate(&L, CUDA_R_32F, rows, cols, ld)); | ||
| } else { | ||
| CHECK_CUBLAS(cublasLtMatrixLayoutSetAttribute( |
There was a problem hiding this comment.
what if there is only a change in rows, but the other fields remain unaffected, do we still need to update all the attributes?
There was a problem hiding this comment.
Right now all three attributes are rewritten on every call even when nothing changed. I'm planning to keep track of the dimensions each descriptor currently holds and update only what differs i.e. a change in rows updates rows and ld together (ld=rows), a change in cols updates only cols and a repeated shape writes nothing.
However the above approach will only avoid rewrites when the same size repeats in succession, so a better approach could be that we give each call site declared through addLayoutConfig its own three descriptors instead of sharing one per matrix role. Sites then stop overwriting each other, so when sizes repeat across events nothing is written at all and the ordering constraint between the validity check and the final stamp mostly goes away; so I think we can try this instead
| const ShapeEnvelope * | ||
| findEnvelope(const std::pair<std::size_t, std::size_t> &kA, | ||
| const std::pair<std::size_t, std::size_t> &kB, | ||
| const std::pair<std::size_t, std::size_t> &kC) const { |
There was a problem hiding this comment.
explain what this method is exactly doing
|
|
||
| if (env && !algoUsable(desc, h.algo, kA, kB, kC)) { | ||
| ++stats.envelopeRejects; | ||
| h = *getOrComputeAlgo(transA, transB, epilogue, kA, kB, kC); |
There was a problem hiding this comment.
why do we need to compute the algo twice here?
There was a problem hiding this comment.
it's not computed twice in the normal path. The second resolution happens only when cuBLASLt rejects the declared shape's algorithm at this call's exact size (returns NOT_SUPPORTED, m=1 was found to do this for cuBLASLt) and then it falls back to resolving at the exact shape, which also gets cached. Added a comment explaining the same
| std::unordered_map<std::pair<std::size_t, std::size_t>, | ||
| hipblasLtMatrixLayout_t, PairHash, PairEq> | ||
| layoutStore; | ||
| enum LayoutRole { ROLE_A = 0, ROLE_B = 1, ROLE_C = 2 }; |
There was a problem hiding this comment.
add a comment as to what LayoutRole means and what those individual roles mean here.
|
@sanjibansg
All the existing tests present in dev are also passing Nvidia: H100 |
|
/runtest h100-47gb |
|
|
|
/runtest mi300x |
|
|
|
/runtest w7900 |
|
|
|
/runtest l40s |
|
|
|
|
|
/runtest h100-47gb |
|
|
sanjibansg
left a comment
There was a problem hiding this comment.
Very close, couple of more comments and the PR is ready to be merged! thanks for the efforts!
| // The cuBLASLt spellings of everything the shared BlasLt implementation in | ||
| // backends/gpu/detail uses: types, enum values and functions. |
There was a problem hiding this comment.
| // The cuBLASLt spellings of everything the shared BlasLt implementation in | |
| // backends/gpu/detail uses: types, enum values and functions. | |
| // The cuBLASLt forwarding of the shared BlasLt implementation in backends/gpu/detail |
| static_cast<std::size_t>(k.epilogue); | ||
| return h ^ (h >> 16); | ||
| } | ||
| // The hipBLASLt spellings of everything the shared BlasLt implementation in |
There was a problem hiding this comment.
| // The hipBLASLt spellings of everything the shared BlasLt implementation in | |
| // The hipBLASLt forwarding of the shared BlasLt implementation in |
| ``` | ||
|
|
||
| The GPU backends (`BlasCuda`, `BlasHip`) additionally expose `gemmrelu`/`gemmgelu` (fused bias + activation via cuBLASLt/hipBLASLt epilogues), `gemmStridedBatched`, and `addLayoutConfig` (used to pre-register cuBLASLt/hipBLASLt matrix layouts for a given shape before the first `matmul`/`gemm` call on that shape). | ||
| The GPU backends (`BlasCuda`, `BlasHip`) additionally expose `gemmrelu`/`gemmgelu` (fused bias + activation via cuBLASLt/hipBLASLt epilogues), `gemmStridedBatched`, and `addOperationConfig` (creates the matrix layouts and resolves the multiply algorithm for a call site's shape ahead of its first call, see below). |
There was a problem hiding this comment.
list these out in bullets to make them easier to understand
| The GPU backends (`BlasCuda`, `BlasHip`) additionally expose `gemmrelu`/`gemmgelu` (fused bias + activation via cuBLASLt/hipBLASLt epilogues), `gemmStridedBatched`, and `addOperationConfig` (creates the matrix layouts and resolves the multiply algorithm for a call site's shape ahead of its first call, see below). | |
| The GPU backends (`BlasCuda`, `BlasHip`) additionally expose | |
| - `gemmrelu`/`gemmgelu` (fused bias + activation via cuBLASLt/hipBLASLt epilogues) | |
| - `gemmStridedBatched` for batched gemm operations through strides | |
| - `addOperationConfig` that creates the matrix layouts and resolves the multiply algorithm for a call site's shape ahead of its first call, see below). |
| ### Warming the cache with addOperationConfig | ||
|
|
||
| `addOperationConfig(m, n, k, lda, ldb, ldc, transa, transb, epilogue)` fills the cache for one call site ahead of its first call: it creates the three layouts and resolves the algorithm for the given dimensions. The `epilogue` argument is the `Epilogue` enum from `sofieBLAS/core.hpp` and names which call the site will make, because the fused epilogue is part of the selected kernel: |
There was a problem hiding this comment.
I would not mention warmup step here, since this is more inference workflow related, there could be situations where a warmup is not possible in its traditional sense, sofieBLAS works even in that case by just caching in the information in its first call.
| | `Epilogue::ReluBias` | `gemmrelu` (bias, then ReLU) | | ||
| | `Epilogue::GeluBias` | `gemmgelu` (bias, then GELU) | | ||
|
|
||
| A generated Session constructor calls it once per GEMM call site with the construction-time dimensions, so the first inference pays no heuristic queries at those sizes. |
There was a problem hiding this comment.
this is SOFIE-related text, I would not mention this here. sofieBLAS should work independent of SOFIE, therefore could be utilized in other projects as well
| blas.matmul('N', 'N', 37, 3, 5, 1.0f, dA, dB, 0.0f, dC); // new size: created on first use | ||
| ``` | ||
|
|
||
| ### One implementation for both GPU backends |
There was a problem hiding this comment.
this comment is confusing and could hint that we need to instantiate blas methods differently for CPU and GPU backends, which is not correct. The BLAS methods are instantiated in identical ways with only difference being the template dispatch variable.
|
|
|
@sanjibansg thanks for the review, i've addressed the comments and made changes |




Implements #10
Edit:
The PR has been brought up to date with the dev branch and the approach has grown from the older description, described below.
The layout part is unchanged: one
cublasLtMatrixLayout_tper role, runtime dims stamped in before each multiply operation. The change is on the algorithm caching. Re-querying the heuristic per new size meant a dynamic model pays one heuristic query for every distinct size it meets and the cache grows with the sweep. Now addLayoutConfig is no longer a no-op: it records the call site's construct-time shape as its envelope and resolves the algorithm for it once, up front. At infer time any size covered by an envelope reuses that entry; so a size sweep does zero heuristic queries and adds zero cache entries. In any case if cuBLASLt cannot run the envelope's algorithm at the actual size the call falls back to resolving that exact shape.LayoutStats and algoCacheSize()expose the counts that the tests assert through them.The cache is unbounded by default. Passing a limit to the constructor enables LRU eviction, for the case where many above-envelope shapes get resolved individually.
Benchmark (Nvidia H100) through exisiting, bench_cuda:

Original description:
sofieBLAS registers cuBLASLt matrix layouts at construct size (addLayoutConfig) and the matmul path looks them up by the runtime (rows, cols). With dynamic shapes the two disagree i.e. a Session constructed at one size and inferred at another misses the layout and throws std::out_of_range, so one Session only works at a single size.
This PR resolves each matrix's layout at matmul time instead: keep one cublasLtMatrixLayout_t per role (A/B/C) and stamp the runtime dims into it via
cublasLtMatrixLayoutSetAttributebefore each multiply. The descriptor is host-side metadata consumed by cublasLtMatmul at the call, so reusing one object across shapes is safe.