-
Notifications
You must be signed in to change notification settings - Fork 33
Array index analysis using SMT #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Also use z3-solver instead of pysmt as it exposes additional useful features, such as predicates for checking bit vector overflow.
|
This feels like one for @hiker to look at as he did DependencyTools - without looking at the code my feeling is that it would make sense for extra sophistication to go in there rather than into a specific PSyIR node. EDIT: my mistake - I see you say you've added a new class to encapsulate this functionality. I'd still like to see whether it makes sense to merge it/include it with DependencyTools though. |
|
Thanks @arporter. I could call the new analysis from |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3213 +/- ##
==========================================
+ Coverage 99.90% 99.91% +0.01%
==========================================
Files 376 376
Lines 53156 53952 +796
==========================================
+ Hits 53104 53907 +803
+ Misses 52 45 -7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Thanks a lot for this contribution. TBH, ever since I used an SAT (elsewhere), I was thinking to try using an SMT for this - so thanks a lot. And indeed (besides others), the current implementation of the dependency analysis does not use the loop indices (which was on my todo list ... but given that this code is hardly ever seen in reality, I never even started :) ), and I wanted to improve its handling cause by tiling loops :) Now, this PR is rather 'difficult'. Let me try to put it into context:
And I agree with Andy that it should go into the dependency tools (making it somehow configurable which version to use. Two additional concerns/questions I have:
Now the bad news: I am pretty busy with updating my PSyclone training (I need to give it in 3 weeks time, and there are quite a few changes in PSyclone), and we are planning a release soon. You might also be affected by Sergi's changes (that will soon go onto master) that replaced the component indices (I think you are probably not affected by that), so it will be a bit before I can really dig into that. Till I get back to that, could you check if your Z3 solver would be able to handle all other cases we support atm (i.e. could replace the existing code)? And some indication of the timing would be useful (atm you are using a timeout of 5 seconds. What is the typical runtime for a 'normal' case? E.g. if it's 2 seconds, then imho we would be affected too much by load of the machine it's running on and the performance of that machine. If it's 0.1, then it's probably all good). @arporter , @sergisiso , Z3 takes up around 45MB on my laptop, so less than sympy (66MB - plus other dependencies sympy has). And strangely, it contains the same library twice, so could be reduced to 23MB. I also did a bit of search if there is a SMT solver that integrates better with SymPy (i.e. if we could use sympy expressions to feed them into the solver, instead of converting psyir to z3), but that appears to be not the case. |
|
@hiker thank you very much for these comments and suggestions, and your time. Below, I respond to specific points and then summarise next steps.
I envisaged that we'd want both. The new analysis is only useful when combined with the existing one. It only considers array index conflicts and assumes the existing analysis has already established that there are no scalar conflicts, for example. (I currently combine the two in In addition, the solving time of the SMT-based analysis is quite unpredictable. In simple cases, it's probably overkill and might make PSyclone noticably slower when processing large codebases. I think it's best used as a fall-back option when the default (more efficient) analysis fails and you want PSyclone to try harder in specific files or routines.
Yes, this should be possible. I will have a look.
Thanks, I'll take a TODO to handle these. At the moment, the analysis only translates logical variables and integer variables of unspecified kind to SMT. If this is insufficient to prove absence of conflicts, then the analysis will conservatively report that a conflict exists.
Good point! This crossed my mind but I forgot about it. I'll take another TODO for this (to handle both scalar members and array members).
That's correct. Unfortunately, I don't know of a good solution. I think it is just a drawback of using powerful, general-purpose solvers.
Yes, I'll change the default settings so that integer overflow is ignored. That still leaves the choice between using the integer solver and the bit vector solver. A simple solution here would be to try one then the other (or both in parallel).
I appreciate that, which makes me all the more thankful for the feedback you've already given. I'm also unsure how much time I will have for this work, with the NG-ARCH project coming to an end in a few weeks (I hope I will get time for it, and other PSyclone ideas, but that's unclear at the moment). In any case, I hope the work serves as an interesting proof-of-concept, even if it doesn't get merged in. Here is a summary of things I'll aim for in the short term:
|
|
I've made a number of updates:
|
|
Below are some results for the execution time of the analysis. I ran
on all loops in the UKCA codebase (308 files). Here is a histogram of the times taken by each method to complete on each loop:
As expected, All loops taking longer than 15s to analyse by The Using Using |
|
A few more updates from me:
|
|
@mn416 I am sorry for the delay, but am getting finally some time to get started on this (though I did do some Z3 work in the background and wrote a Kenken solver with it - that was neat :) ). Now, you did mention that you are not sure how much time you will have for this, given that ng-arch is (has?) ended. Could you quickly let me know? I see two scenarios:
Let me know how you are doing. And thanks for this work here, it looks certainly a lot easier than doing this explicitly using sympy. |
|
Thanks for the update @hiker. Very pleased to hear that you've been having fun with Z3 as one of my aims with this work was to try and get PSyclone folk more interested in it! I think there's an interesting synergy there. As it turns out, I should have more time to work on this, at least for small/medium sized changes. So, I'd be happy to receive requests for improvements, and also happy for you to provide any improvements yourself, if you wish. Since my last commit, I've noticed a small issue. |
hiker
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have enough time today to finish the review, so I have to abort somehow in the middle.
Two 'general' questions:
- Is there any advantage of supporting bit vectors? It feels like the transformation of expressions to Z3 can be cut in half if we would only support integers (and since we kind of agree to ignore overflows). Or of course the reverse ;)
- I feel it might be useful to have a command line option to generically enable your analysis and set the main configurations (timeout and #threads?). E.g. the Fab build system should be able to support to directory-specific flags, so the build system could enable your analysis for all files under
../ukcaor so, and not affecting anything else?
That might be useful if the same transmutation script is used across different source directories, but only a subset will benefit from Z3.
I will continue hopefully tomorrow.
| (if there are any) are recursively gathered. The condition for | ||
| the final 'else' in the chain (if there is one) is 'None'. | ||
| ''' | ||
| if self.else_body is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this harder to understand than necessary (probably because adding 'this' block's data at the end with an insert). As far as I can tell, the following code produces the same result:
branches = [(self.condition, self.if_body)]
if self.else_body:
if (isinstance(self.else_body, Schedule) and
len(self.else_body.children) == 1 and
isinstance(self.else_body.children[0], IfBlock)):
branches.extend(self.else_body.children[0].flat())
else:
branches.append((None, self.else_body))
return branches
(if not, we need a test for that, the code passes the existing tests).
| ifblock.addchild(else_body) | ||
| assert ("Item 'Schedule' can't be child 3 of 'If'. The valid format is: " | ||
| "'DataNode, Schedule [, Schedule]'." in str(excinfo.value)) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a test with no else branch? ATM that line in if_block.py is not covered. My suggested re-writing avoids this issue (since there is no code to execute if there is no else block :) ), but still we should have a separate test that covers the no-else case.
| conflicts. An ArrayIndexAnalysisOptions value can also be given, | ||
| instead of a bool, in which case the analysis will be invoked | ||
| with the given options. | ||
| :type use_smt_array_index_analysis: Union[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have switched to use 'proper' Python typing (instead of :type:), and are fixing files as we touch them. Could you update this to use:
loop_types_to_parallelise: Bool = None,
use_smt_array_index_analysis: Union[...] = False):
and remove the existing :type: comments? Thanks
| var_info) | ||
| # If using the SMT-based array index analysis then do | ||
| # nothing for now. This analysis is run after the loop. | ||
| if self._use_smt_array_index_analysis: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to first run the existing array analysis? And only run the the Z3 solver if the result is false (and long term, maybe the existing analysis could return three values: yes, no, unknown.
I might try to do some additional performance tests (I noticed the results you have posted ;) ), this obviously only makes sense if the existing one is significantly faster.
| instead of a bool, in which case the analysis will be invoked | ||
| with the given options. | ||
| :type use_smt_array_index_analysis: Union[ | ||
| bool, ArrayIndexAnalysisOptions] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit I don't like this kind of Union here. Yes, it actually works fine, but I would guess that a linter will have problems with that (and yes, atm we don't do linting for typing, but that might come at some stage :) ).
Could we instead just use use_smt_array_index_analysis: Optional[...] = None? Then add a enabled option to the ArrayIndexAnalysisOptions class. So, if no ArrayIndexAnalysisOptions is specified, you can set self._smt_array_analysis_option = ArrayIndexAnalysisOptions() and set the enabled option to false. This way, the dependency analysis member will have a single type only, removing the need for isinstance tests later.
I think my main issue is that it does not 'read' nice: the variable is use_..., but it's sometimes not a boolean.
|
|
||
| def _init_analysis(self): | ||
| '''Intialise the analysis by setting all the internal state | ||
| varibles accordingly.''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/varibles/variables/
| return z3.substitute(expr, *subst_pairs) | ||
|
|
||
| def _translate_integer_expr_with_subst(self, expr: Node): | ||
| '''Translate the given integer expresison to SMT, and apply the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/expresison/expression/
| ) -> (z3.ExprRef, z3.BoolRef): | ||
| '''Translate a scalar integer Fortran expression to SMT. In addition, | ||
| return a constraint that prohibits/ignores bit-vector overflow in the | ||
| expression.''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a :returns:... line
| constraints = [] | ||
|
|
||
| def trans(expr: Node) -> z3.ExprRef: | ||
| # Check that type is a scalar integer of unspecified precision |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add :param ... and :returns: documentation.
|
|
||
| def trans(expr: Node) -> z3.ExprRef: | ||
| # Check that type is a scalar integer of unspecified precision | ||
| type_ok = _is_scalar_integer(expr.datatype) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand the type_ok handling. It is only tested for the first two tests, after that is ignored, so a (bad-style) assignment (int_var = 1.23) would not set int_var to 1?
|
Oh, some additional comments I forgot:
The User's Guide can be quite simple and just adding the command line options (I suggested) to the getting going section (i.e just the text that explains this option in argparse). For the developers guide I would suggest to just convert the great documentation you have in the class into a docstring for the class, and then after a paragraph introducing (very high level) Z3, then just add the doc string (which is already done widely in our docs)
Edited: Also bring this PR up to current master, and solve the conflict - sorry, my fault because of the delay in reviewing :( |
|
...
Do you have the file names for the slow ones? I would be interested to look at them in more detail (for both approaches). ...
I assume you are looking at the code 'around' a loop to know additional constraints on variables that might be used in the loop? Is this actually required (i. e. leads to more parallelisable loops)? I still need to fully understand you code ;) Might an option to restrict the Z3 analysis to just the loops be useful??
Would that imply that you get hundreds of duplicated constraints (which could slow things down)? Maybe this could be filtered??
Sounds promising.
Again, I would be interested in a list :) If you don't have the data ready, don't worry, I should be able to reproduce them: which seems to match the number of files you mentioned :) |
|
I measured some results (unfortunately using a slightly different ukca version), all results where either the existing or z3 tests took more than 5 seconds (might be slightly biased, since DependencyTools include scalar analysys, but I'd guess that's very small) |
|
I just realised that I am running on a slightly different ukca version, but here outliers (runtime > 5 seconds) I see: (from 256 files, after which psyclone appears to hang, which I will look at separately): |
|
I will stop for now, and hand it back to you. I started to get into the actual analysis, but I thinks splitting this into two files (one to convert expressions (or Fortran statement), one to do the actual analysis) will be a good start. Note that I also did #3311, which should fix some (all?) of the timing outliers with the dependency tools ;) Thanks for your performance results. |
This PR introduces an
ArrayIndexAnalysisclass to determine whether or not distinct iterations of a given loop can generate conflicting array accesses (if not, the loop can potentially be parallelised). It formulates the problem as a set of SMT constraints over array indices which are then are passed to the third-party Z3 solver.The PR also introduces a new option
use_smt_array_index_analysistoDependencyTools,ParallelLoopTrans, andOMPLoopTranswhich enables use of the new analysis in place ofDependencyTools's current array conflict analysis.To install the Z3 solver:
This dependency has been added to
setup.py.The analysis is broadly explained in source code comments.
There is also a set of examples showing what the analysis is capable of, available here. None of these examples are solved by PSyclone's existing loop analysis, and all can be solved by the SMT-based analysis.
The comments below include results from applying the analysis to the full UKCA codebase (execution time of the analysis, and number of loops parallelised).