[AIMIGRAPHX-926] Support commonly used shape methods for symbolic shapes#4760
[AIMIGRAPHX-926] Support commonly used shape methods for symbolic shapes#4760shivadbhavsar wants to merge 21 commits intosym_dim_integrationfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds interval-aware symbolic expressions and extends shape/permutation utilities so commonly used shape methods work with symbolic dynamic shapes.
Changes:
- Add bounded symbols (
min/max) tosym::expr, pluseval_min()/eval_max()and interval-based semantic comparisons. - Add symbolic dynamic strides to
shape, enablingpacked/standard/transposed/broadcasted,to_static(symbol_map),from_permutation/with_lens, and permutation logic to operate on symbolic shapes. - Expand unit tests to cover symbolic equality/hashing, min/max evaluation, comparisons, serialization, and shape/permutation behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/sym_test.cpp | Adds coverage for bounded symbols, interval evaluation, comparisons, and serialization. |
| test/shape_test.cpp | Adds extensive symbolic-shape tests for layout flags, permutations, compatibility, and dynamic_dimension arithmetic. |
| test/serialize_program.cpp | Adds msgpack roundtrip test for programs containing symbolic shapes. |
| src/targets/gpu/hip_gemm_impl.cpp | Minor batch-shape construction refactor for GEMM validation. |
| src/targets/gpu/gemm_impl.cpp | Minor batch-shape construction refactor for GEMM validation. |
| src/sym.cpp | Implements bounded symbols, interval eval, and semantic comparison; updates hashing/ordering/serialization. |
| src/shape.cpp | Introduces symbolic dyn_strides and updates shape methods/serialization to support symbolic shapes. |
| src/permutation.cpp | Adds permutation logic for symbolic shapes using max-interval evaluation. |
| src/include/migraphx/sym.hpp | Extends public API for bounded vars, min/max eval, and comparisons. |
| src/include/migraphx/shape.hpp | Extends dynamic_dimension and shape APIs for symbolic expressions and dyn_strides. |
Comments suppressed due to low confidence (3)
src/include/migraphx/shape.hpp:139
dynamic_dimension::normalize_sym()is called in the constructors, butfrom_value()for reflectable types assigns fields directly and will not callnormalize_sym()after deserialization. Older serialized shapes without the newsymfield will deserialize fixed dimensions withsym_expr == nullopt, breaking the new invariant that fixed dims become symbolic constants. Consider adding a customfrom_value/migraphx_from_valuefordynamic_dimension(or a post-deserialize hook) that callsnormalize_sym()after populating fields.
template <class Self, class F>
static auto reflect(Self& self, F f)
{
return pack(f(self.min, "min"),
f(self.max, "max"),
f(self.optimals, "optimals"),
f(self.sym_expr, "sym"));
}
bool is_fixed() const;
bool is_symbolic() const { return sym_expr.has_value(); }
void normalize_sym()
{
if(is_fixed() and not is_symbolic())
sym_expr = sym::lit(min);
}
src/shape.cpp:1083
dynamic_dimension::operator+=saturatesmaxon overflow but updatesminwith uncheckedmin + x.min. Ifminoverflows (wraps) whilemaxsaturates, the resulting range can become invalid and propagate incorrect bounds. Consider applying the same overflow handling tomin(and similarly foroperator*=whereminmultiplies without overflow checks).
shape::dynamic_dimension& shape::dynamic_dimension::operator+=(const shape::dynamic_dimension& x)
{
auto lhs_sym = sym_expr;
auto rhs_sym = x.sym_expr;
min = min + x.min;
max = (max > std::numeric_limits<std::size_t>::max() - x.max)
? std::numeric_limits<std::size_t>::max()
: max + x.max;
src/shape.cpp:1139
dynamic_dimension::operator*=uses a checked/saturating multiply formaxbut multipliesminwithout overflow checks (min = min * x.min). This can overflow and produce an incorrect (wrapped) lower bound. Consider using the samesafe_mullogic formin(or a dedicated safe multiply for lower bounds).
shape::dynamic_dimension& shape::dynamic_dimension::operator*=(const shape::dynamic_dimension& x)
{
auto lhs_sym = sym_expr;
auto rhs_sym = x.sym_expr;
min = min * x.min;
auto safe_mul = [](std::size_t a, std::size_t b) -> std::size_t {
if(b == 0)
return 0;
if(a > std::numeric_limits<std::size_t>::max() / b)
return std::numeric_limits<std::size_t>::max();
return a * b;
};
max = safe_mul(max, x.max);
if(x.is_fixed())
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… optimals consistent with symbols
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## sym_dim_integration #4760 +/- ##
=======================================================
+ Coverage 92.49% 92.52% +0.02%
=======================================================
Files 583 583
Lines 29670 29888 +218
=======================================================
+ Hits 27443 27651 +208
- Misses 2227 2237 +10
🚀 New features to boost your workflow:
|
| // expected. | ||
| // 3. The number of unique symbols per expression is small (typically | ||
| // 1-3), making the 2^k evaluation cost negligible. | ||
| std::pair<int64_t, int64_t> expr::compute_bounds() const |
There was a problem hiding this comment.
I dont think this is a good way to do this. #4782 already can do this with interval math so it works for more operators(like subtraction) and doesnt require 2^k evaluations.
There was a problem hiding this comment.
I agree, I use this as a quick and easy approach for now till the updated symbolic expr library goes in with better interval arithmetic
| { | ||
| auto bounds = sym_expr->compute_bounds_uint(); | ||
| min = bounds.first; | ||
| max = bounds.second; |
There was a problem hiding this comment.
Computing min and max at every step might be slow. We should first refactor the codebase to use get_interval() and get_optimals():
struct dynamic_dimension
{
struct interval
{
std::size_t min;
std::size_t max;
};
interval get_interval() const;
std::set<std::size_t> get_optimals() const;
};We can probably use cursor/claude to do the refactor. At first it can just store the member variables like what we are doing now and then later we can change it to compute this from the symbolic expression.
There was a problem hiding this comment.
I am looking into doing this and I think it is a good idea. Only thing is, it will make #4702 kind of pointless (which is also ok). Would you @pfultz2 and @CharlieL7 be ok to review this mega PR with all the changes then? and i would discard 4702
There was a problem hiding this comment.
Can you first just do the refactor to get_interval and get_optimals with no functional changes? I can review that PR.
There was a problem hiding this comment.
So given that struct, can we settle on how we would lazily compute the intervals and optimals? My thinking atm is to make the range and optimals fields optional (they would always be defied for current range-based shapes) that for symbolic shapes would only populate values when some compute_intervals method is called (throw if some codepath calls min() or max() before explicitly making this call). Does that sound reasonable or did you have something different in mind?
There was a problem hiding this comment.
I am thinking we store the interval as std::optional<interval> for when we are using the old intervals. Then if we are using symbolic expression we would compute it on the fly:
interval get_interval() const {
if(range)
return *range;
sym::interval v = e.eval_interval();
return {.max = to<std::size_t>(v.max), .min = to<std::size_t>(v.max); }
}Later when we remove all the interval code we would get rid of the field and it just do e.eval_interval().
|
Abandoning PR, and adding updates after requested refactor straight to #4702 |
Motivation
Commonly used methods in shape.cpp and permutation.cpp can now be used for dynamic shapes with symbols.
Technical Details
Changelog Category
Add a
CHANGELOG.mdentry for any option other thanNot Applicable