Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3621 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 403 403
Lines 56802 56833 +31
=========================================
+ Hits 56802 56833 +31 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Reasonable small change to add a The two 'odd; ones:
I did update some files to start using pathlib, which makes some changes look much bigger than they actually are. I have confirmed that this feature works for writing out GPU markup information (separate PR incoming for that :) ) IT passed, ready for review |
arporter
left a comment
There was a problem hiding this comment.
Thanks very much Joerg. I like the basic functionality and think it all makes sense.
However, I'm not keen on then using that functionality to set the name of the file which the PSyIR will be written to. That information does not belong in the PSyIR as the writing of code to file lives outside. How does this play into your work towards supporting GPU markup?
|
|
||
| A `FileContainer` is always created at the root of the PSyIR tree when | ||
| parsing Fortran code, as a Fortran file can contain one or more | ||
| program units (captured as `Containers` and/or `Routines`). PSyIR |
There was a problem hiding this comment.
Not yours but, somewhere along the line, this paragraph has become garbled. Please could you delete the sentence beginning "PSyIR tree when...".
| ) | ||
| self._psyir_node = processor.generate_psyir(fparse_tree) | ||
| self._psyir_node.name = filename | ||
| file_path = Path(self.filename) |
There was a problem hiding this comment.
Comment please: Store information on the originating file within the root FileContainer node (or something)
| tree = self._processor.generate_parse_tree_from_file(path) | ||
| file_container = self._processor.generate_psyir(tree) | ||
| # Since we are parsing a file, we have a FileContainer: | ||
| file_container = cast(FileContainer, file_container) |
There was a problem hiding this comment.
Is this needed? Could we have:
file_container: FileContainer = self._processor..... or does mypy then complain that the generate_pysir returns Node?
There was a problem hiding this comment.
Before I forget this: it does complain:
psyir/frontend/fortran.py:198: error: Incompatible types in assignment (expression has type "Node", variable has type "FileContainer") [assignment]
But since we haven't agreed on what tool to use, I am happy to remove this (though it might be worth to make a decision what we want to use, so we can over time improve it ... or let the AI run wild :) )
| @file_path.setter | ||
| def file_path(self, value: Path): | ||
| '''Set the path of the source file represented by this node.''' | ||
| self._file_path = value |
| file_container = self._processor.generate_psyir(tree) | ||
| # Since we are parsing a file, we have a FileContainer: | ||
| file_container = cast(FileContainer, file_container) | ||
| file_container.file_path = path |
There was a problem hiding this comment.
Comment please: Store the path of the originating file in the FileContainer.
| content = "module andy\n\nend module" | ||
| with open(fname, "w", encoding="utf-8") as fout: | ||
| fout.write(content) | ||
| fname.write_text(content) |
There was a problem hiding this comment.
Wow, Path keeps on giving doesn't it?
| assert file_container.file_path == file_path | ||
| new_file_path = Path("other.f90") | ||
| file_container.file_path = new_file_path | ||
| assert file_container.file_path == new_file_path |
There was a problem hiding this comment.
This will need extending once you've added the type check to the setter.
| error_import = script_factory(tmp_path, """ | ||
| import non_existent | ||
| """) | ||
| error_import = script_factory(tmp_path, dedent(""" |
There was a problem hiding this comment.
Ooh, yet another thing I didn't know about.
| logger.error(err, exc_info=True) | ||
| sys.exit(1) | ||
|
|
||
| if output_file: |
There was a problem hiding this comment.
At first I thought this was OK but now I'm not so sure. Conceptually, I think it makes more sense to have file_path hold the path to the file from which the PSyIR was generated. In PSyKAl mode, this would mean it would be None for a generated PSy layer as that has been created from scratch. It's up to something outside of PSyIR to decided where to output any code - it shouldn't be set ahead of time inside the tree.
How does this fit with the routine-markup workflow that we're aiming for?
There was a problem hiding this comment.
At first I thought this was OK but now I'm not so sure. Conceptually, I think it makes more sense to have
file_pathhold the path to the file from which the PSyIR was generated. In PSyKAl mode, this would mean it would beNonefor a generated PSy layer as that has been created from scratch. It's up to something outside of PSyIR to decided where to output any code - it shouldn't be set ahead of time inside the tree.How does this fit with the routine-markup workflow that we're aiming for?
One additional thing that I just thought about: what if the writing of the output files is changed to use the FilePath.file_path (instead of args.opsy etc)? Would that make you feel more comfortable? I think that is actually something that should be done if we add the attribute. Then it is guarateed that even if the file does not exist yet, this is the filename it will be written to.
There was a problem hiding this comment.
Yes, I was wondering about that too. That would be slightly better (modulo the bigger conversation below).
There was a problem hiding this comment.
That would enable some nice changes to generator.py, and I think that file could urgently do with some refactoring :)
ATM, dsl and transmute are handled entirely different: generate (for DSL) returns the strings for psy and alg layer, and the caller (main) writes both. For transmute, the code_transformation_mode does everything and returns nothing. If they would both return FileContainer(s), that would be easier to understand (we can even add a write or save method to the `FileContainer).
Oops, actually, generate is incorrectly types, it returns [ast, str] for lfric, and [str,str] for gocean :) Anyway, refactoring would be nice :)
There is the old-style creation in alg_gen that is annoying, which is what brings in the ast, and explains why main does:
psy_str = str(psy)
alg_str = str(alg)
Though only the last one seems to be actually necessary.
grin and I am not suggestion to convert the fparser AST to psyir till we can remove alg_gen - though, shouldn't that work? :)
Anyway, we can discuss in the Telco ;) TBH, adding a write method to FileContainer feels very nice :)
OK, the way to planned mark up works is that a PSy-layer file So, the problem is where is X :) Options:
IMHO, a FileContainer having a file_path would match its functionality, and explain the difference between Container and FileContainer (atm there is none). Also, the I do get the issue that for an existing file If we agree to not have the psy-layer name in the FileContainer, I would then actually consider not adding this attribute at all. If a user script can't rely on this attribute in all cases (i.e. it can if the PSyIR comes from an existing file, e.g. using the module manager, but not for alg- or psy-layer), it would either remained unused, or causes if-statements and some kind of code to handle if file_path is None in user transformation scripts. Might be more straight-forward to then just use a keyword argument. The final argument for this attribute: in the future, additional applications might need this? Maybe ... a script want to cache (based on a hash of the file content) DUC information to speed up processing of files when nothing has changed? That cache could use the same file-path information. That's admittedly the only thing I could currently think of (any other cache information like Martin's suggested cached type information would be handled by PSyclone internally, so it can access the file name). |
Is this a phase handled by the build system itself? I was hoping to do something agnostic to the build system, e.g. that it could also work with NEMO, current LFRic, whatever LFRic wants to do next. What I had it mind was two* calls to the build system:
We don't need the path in this case* because we expect all files to go through psyclone during the second build, at some point all the 'module' in the yaml file will go through it. (* technically you can have a top-level build target that does these 2 steps) |
Well, I suggested to include the psyclone control file (which can be used to specify several psyclone passes, and which I intend to extend to work with this setup) :) But I can't really see how this can be done without involving the build system. A suite might be running transmute and DSL in any order (transmute, dsl, transmute) - only at the very end we will have the complete information what needs to be marked up (at least in theory a transmute step could read in a psy-layer file?? - Unlikely admittedly, but who knows). I can't really see how PSyclone can know that it should run an additional step.
I wouldn't agree with that. Yes, timing-wise you are right, but given your definition of the information, it means the file has combined the individual dependencies using the module as key. A module can be used by two different files Also, if I am not mistaken, If we store the information per file in a central file, then we have overall This is imho huge overhead for no gain. ATM, handling this is trivial: you take a Individual files is definitely the KISS solution imho.
There can't be a 'too late' imho. If a file is updated adding a new dependency, the new dependency needs to be marked up. We should not rely on always enforcing a clean build. Though admittedly, I just realise that the markup step (which isn't fully done yet) should test if function/symbol isn't already marked up ;)
Hmm - I wonder if I misunderstood something: yes, the build system needs to trigger an additional phase (if upmarking is required, e.g. not if you are just doing CPU openmp). Maybe I misunderstood your statement above that this should be agnostic to the build system.
Agreed, at this stage we don't need the file_path attribute anymore
Same module name in two files is a nightmare for any build system :) Fab explicitly doesn't allow that. If there are two implementations (dl_esm_inf has two parallel_mod files or so, one with MPI, the other a dummy wrapper), you need to take care that the corresponding one is added to the list of files being used (and I believe that applies to amy build system. Symbol names are typically using the module name to distinguish them, so I would expect that this results in linking issues anyway) |
|
I don't think we need to worry about the same module name in two files - such a thing cannot be compiled due to the namespace collision. e.g. there are indeed two Let me just try re-stating what I think it is we're trying to solve:
First off, I'm not convinced that we'll ever get good GPU performance if a kernel calls other routines - I would have thought that's too much code and stack for an efficient GPU kernel (i.e. we'll get very poor occupancy). However, we have to crawl before we can walk and to do that we do need a solution. We could get PSyclone to chase down the targets of calls and mark them up. However, we know that having PSyclone modify files other than the one it has been given (or is generating) makes life very difficult and won't play nicely with parallel builds. So, we need a mechanism to mark-up the targets of calls and for that we need to know which ones to mark up. Therefore, we need:
I'm worried about stage 2 - what happens when the "necessary markup" is more than "compile this for GPU" - i.e. if there's parallelism inside the routine that we want to exploit? Or is the assumption that we're at the bottom of the stack and there's no parallelism we want? Ideally, we'll have at least module-inlined as many routines as possible which will actually remove them as a dependency for a given call site. I guess that has to happen as part of stage 1? |
Agreed.
I can't comment om the performance, not enough experience. 'Pseudo'-dead-code elimination (based on known namelist file settings to e.g. cut down the call tree in jacobian or so) will help with that. I am also not entirely certain about no kernel using module variables - maybe? TBH, from a software engineering point of view using e.g. a module with global and common constants (planet radius) makes sense, and it feels very inconvenient to pass all potential variables through (I understand that this is the original design, but maybe not the reality). Hmm - pseudo-constant-replacement might also help with that (i.e. we read namelist file, and hard-code values :) ). But agree with crawling :)
Ah yes. I always assumed that development iterates, i.e. if we build repeats and we manually exclude files to be compiled for GPU (because or problems elsewhere). Advantage: we notice if something breaks. Disadvantage: manual work. Your proposal of course automates it all, which is quite nice, but we need to find a way of tracking that we not suddenly lose kernels that were on GPU before. Both approaches can probably be done, depending on developer's preference? But I like this idea.
Yes, but that might be third or fourth step :) There is at least one app that transmutes a kernel, so that would be transmute, then DSL, then transmute for marking up. I think we should stay flexible with the number of passes.
Actually, if we have file-specific dependency files (as opposed to a global file as discussed with Sergi), there is no need to do that again, we can just read the corresponding yaml file and have this information (we might even want to store the file path into the yaml file as well. The module manager should always find the files for us, but that's faster, and cheap storage wise).
I don't see a problem with that from a build point of view - as long as we run distinct phases. It will need some clever script design, but certainly feasible.
I don't know how reliable inlining is all-in-all ;) I assume we could just run validate to check if this is possible (and if so, don't issue the dependency). Or we could even create the full Fortran and write it out, and then in a later phase 'transmute' (I don't like that term ;) ) these files to add the directives. From a build-system point of view, I still think my concept gives us all the flexibility we need: It needs to be extended to allow specification of which files to run on, and keyword arguments, I'll get to this once I have the first steps finished. The code can even read several of these yaml files and combine them, which means this work can be reused This will allow us easily switch between 'try automatically as many files as possible, or 'check that it still works for all files based on manual exclusions ' or so. |
This makes me think that we are not understanding each other because I would have said the exact opposite :) Writing to a central file is O(n), each file adds its contribution. And the solution is much simpler. You don't have to find them involving the ModuleManager, you don't have to merge their contribution when transmuting, and the file just contains the same That said, I don't want to completely derail the conversation, the imortant bit is whats the workflow, not so much if the info is stored in a centralised or decentralised file. |
|
Maybe some pseudocode could show that I was thinking: Phase1: After phase 1 we have an annotions.yml with: Phase2: |
I think I got this (we really need to be in one room with a white board - and I am happy to let this rest till Monday). My issue is: what if you do a partial build (i.e. you still have the same, previous central file) , because (say) you removed a dependency We could add a kind of reference counting ( If you create this information from individual files, it will always be up-to-date. Basically, trade off of inodes vs code-maintainability :) Here my read-in code with one dependency file per file (for routines, I haven't touched symbols yet, but it will be the same): I can't judge the performance impact tbh - one process reading a few 100 or 1000 small yaml files (which might be cached on the node that builds), vs using locks to serialise one common file (which might be expensive on lustre), but in order to get this fixed quickly and easy to maintain, I certainly prefer one dep-file per source file. I think, this is actually just a tiny implementation detail. My suggestion: we start with something simple, and if it should turn out to be too slow (maybe serial reading of the around 2700 small files which we might have in lfric_atm in takes too long??), we can still change this, since both writing the file, and determining the list of files and functions/variables to mark up is in our control (well, I would prefer this, but it could of course be moved into lfric. Us doing that would have the advantage that we can find a solution that will work for both NEMO and LFRic, and can control fully it). I will put this on hold till the telco ;) |
Adds a
file_pathattribute to FileContainer.This is useful if e.g. a user-script wants to write file-specific information (e.g. for GPU markup - write all non-local dependencies), it can access the original filename, and create the file 'next' to it.