[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[TIR] Avoid all-pairs comparison in subexpr elimination - #11423

Closed
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim
Closed

[TIR] Avoid all-pairs comparison in subexpr elimination#11423
tkonolige wants to merge 2 commits into
apache:mainfrom
tkonolige:faster_subexpr_elim

Conversation

@tkonolige

Copy link
Copy Markdown
Contributor

Subexpression elimination used a comparison of each subexpression to every other subexpression to determine which to eliminate. This resulted in an O(N^2) algorithm that was slow when there were many LetStmts. Instead we now hash each subexpression and compare the hashes. We reuse structual hashing without remapping free variables to uniquely
determine each subexpression.

@masahi@FranckQC

Subexpression elimination used a comparison of each subexpression to
every other subexpression to determine which to eliminate. This resulted
in an O(N^2) algorithm that was slow when there were many LetStmts.
Instead we now hash each subexpression and compare the hashes. We reuse
structual hashing without remapping free variables to uniquely
determine each subexpression.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, I have noticed this pass is a little slow when conducting some synthetic tests with lots of subexpressions.

Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
Comment threadsrc/tir/transforms/common_subexpr_elim_tools.cc Outdated
@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@AndrewZhaoLuo comments addressed

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

Thank you for your interest in improving the pass!
I haven't worked on the CSE pass for a while, so I am probably a little bit rusty on it, but I think there are quite a few problems with this PR.

1)
The role of this function SyntacticToSemanticComputations() is to transform a ComputationTable (that is to say, a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>, ie a hashtable mapping PrimExpr to size_t, using StructuralHash for the hashing, and ExprDeepEqual for the equality test) into a vector of std::pair<PrimExpr, size_t>where semantically equivalent elements have been merged.

The whole need for this function is due to having collected syntactical entities (using an efficient structure for that, the ComputationTable, where it's fast to retrieve an element (in constant time), as it's a hashtable), which latter of course needs to be transformed into a collection (a vector), where equivalent terms (like x+y and y+x) are merged together (and their counters added), which I then call semantical entities.

This notion of being semantically equivalent could be anything, like identifying terms modulo associativity [(x+y)+z with x+(y+z)], modulo commutatitvity [x+y with y+x], or anything else, with any equivalence relation. It's completely customizable by just changing the EquivalentTerms() function.

Sure, at the moment, this function EquivalentTerms() just calls the syntactical equality EqualTerms(), but the whole pass has been written with the idea that we could latter-on replace it with anything else, for making it even more powerful. You can see that the function SyntacticToSemanticComputations() that you have changed used to call std::find_if with the predicate being precisely this very customizable EquivalentTerms(). This is lost with these changes, and it no longer identifies equivalent terms.

2)
There is something else which I don't quite get about your changes. Why do you even bother to transform the ComputationTable (which is a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>) into a std::unordered_map<PrimExpr, size_t, ExprDeepHashStruct, ExprDeepEqual>?
The lines 753 to 763 (https://github.com/apache/tvm/pull/11423/files#diff-f83c46530e92c628fd309499ceffa32cb2fb0505633ac0e754e2bdef4d518962R753-R762) are basically transforming a hashtable (table) into the exact same hashtable (equiv_computations)). This code is doing nothing.

If the pass is really taking too long for many people, I could try to help to improve it, if that's needed. There will necessary be a limit in what we can gain at compile time, as improving the runtime speed (or creating opportunities for it, which CSE is doing) often has a price to pay at compile time. But we can see what we can do, for sure.

What do you think?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks for helping to clarify things @FranckQC! I think I'm a little confused still.

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Even if we switch SyntacticToSemanticComputations to a no-op, I worry that we are going to hit this O(N^2) problem in the future. Is there a way to get around the all-pairs comparison?

Regarding 2), I didn't realize ComputationTable was already a std::unordered_map<PrimExpr, size_t, StructuralHash, ExprDeepEqual>. Whoops :).

@AndrewZhaoLuo

AndrewZhaoLuo commented May 26, 2022

Copy link
Copy Markdown
Contributor

@FranckQC thanks for elucidating the intention of the original design. I see why for an arbitrary semantic computation functions you may need to have N^2 comparisons.

Is it possible to relax the number of comparisons by using a canonical form. E.g. (x + y) + z and z + (y + x) would always reduce to (x + y) + z or is this too limiting/impossible to catch some optimizations? If we could do things this way we would have O(N) computations now.

Regardless, this has O(N^2) for little benefit (as we do equality only) at the moment so I propose to comment out the original impl. and replace it with a O(N) fast one / no op, and add a suitable comment.

@AndrewZhaoLuoAndrewZhaoLuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning off the merge approval until we hash this out.

@FranckQC

FranckQC commented May 26, 2022

Copy link
Copy Markdown
Contributor

Hi everyone,

@tkonolige :

Regarding 1), you are saying that right now SyntacticToSemanticComputations just happens to use ExprDeepEqual, but we intend to change it in the future? And so, right now (in main), SyntacticToSemanticComputations is essentially doing nothing? If so, could we just have SyntacticToSemanticComputations just be a no-op for now?

Hum, no, I wouldn't say it does nothing. It still transforms a hastable into a vector. How it does so depends on the EquivalentTerms() function.

SyntacticToSemanticComputations() transforms a table of syntactic entities (where x+y is not the same thing as y+x, even if you were to work modulo commutativity) into a vector of "semantical entities", where equivalent computations have been fusioned/merged together, according to the notion of "being equivalent". This function is building equivalence classes, if you want. And this notion of "being equivalent" is customizable, and implemented by the EquivalentTerms() function, which at the moment simply calls EqualTerms(). So at the moment, the only way to have x Equiv y is to have x and y being syntactically the same term. But that's not a requirement, and it could be any equivalence relation, because the pass has been written to work for any equivalence relation, in order to be able to produce even more commonings.

As a use case of this kind of thing:
For instance, in just a 3 lines of code, @yuanfz98 proposed to change the equivalence relation to now identify terms modulo associativity and distributivity (of * over +) in a PR. Sadly I didn't have enough time to discuss this PR at the time (which got quickly closed). Some people were afraid it would add too much computational complexity to the pass. The complexity of the pass itself wouldn't change (we already look at each pair!), but this time it took benefit of that. However, this new equivalence function was relying on a pseudo-normalisation function (arith::Analyzer::Simplify, see discussion here), which would necessary need some time to (try to) normalize the terms being manipulated. It should not take too long, as they did not implement a full decision procedure, just some quick attempts of applying some rewrite rules, which are some known patterns, that often lead to the most simplified term (but it's not guaranteed). (Said differently, arith::Analyzer::Simplify is correct but not complete.)
It could still take too long in the current form, I don't know, I didn't try it as it was. But I think there's a way to make that more efficient.

@AndrewZhaoLuo and @tkonolige

I think I should be able to address the issue without removing completely the possibility to have more interesting comparisons in the future.
I'll give it a go this week-end if you're happy with that.

Thanks.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC because this currently is causing issues, how about we remove the code for now. When you figure out a solution we can add the code back in.

@FranckQC

Copy link
Copy Markdown
Contributor

If that's solved by tomorrow, would that work for you?

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Sure, that is probably fine. Thank you for the quick turnaround.

@AndrewZhaoLuo

Copy link
Copy Markdown
Contributor

I think the main issue I have O(N^2) runtime on a default pass. We can do linear time if we plan things well, e.g. have syntactic canonical forms. Haven't dug too deeply, but if this normalization stuff does something like this I will be cool

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update on a solution?

@FranckQC

Copy link
Copy Markdown
Contributor

Just starting to work on it now. Was busy with other things earlier today.
A PR will come this evening/night.

@tkonolige

tkonolige commented May 27, 2022

Copy link
Copy Markdown
ContributorAuthor

I don't think @AndrewZhaoLuo or I will be back to review this until Tuesday so don't feel rushed.

@FranckQC

Copy link
Copy Markdown
Contributor

Just a small update to let you know that I have a patch almost ready for that. Just testing it now (both functionally and performance-wise) to make sure everything is ok.
I will update soon again.

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

@FranckQC any update?

@FranckQC

Copy link
Copy Markdown
Contributor

Sorry, it's currently being reviewed in our repo downstream. I hope to make the PR here this afternoon!

@FranckQC

Copy link
Copy Markdown
Contributor

Hi,

The PR is ready here : #11574
Apologies as it took me a little bit longer than I expected.
The only test failing seems to be from a flaky test as it's unrelated to the changes introduced.

Thanks!

@tkonolige

Copy link
Copy Markdown
ContributorAuthor

Thanks @FranckQC.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@tkonolige@FranckQC@AndrewZhaoLuo