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.
| using Operation = hipblasOperation_t; | ||
| using Epilogue = hipblasLtEpilogue_t; | ||
|
|
||
| static constexpr auto OpN = HIPBLAS_OP_N; |
|
@sanjibansg
All the existing tests present in dev are also passing Nvidia: H100 |
|
/runtest h100-47gb |
|
|
|
/runtest mi300x |
|
|


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.