├── CHANGELOG.md ├── README.md ├── .gitignore ├── LICENSE └── acc2omp_converter.py /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to ACC2OMP are documented in this file 4 | 5 | ## [0.2.3] - 2021-05-18 6 | 7 | Minor enhancements: 8 | - Support additional common OpenACC directives: atomic, serial, declare, update host/device 9 | - Improve internal code documentation 10 | 11 | Bug fix: 12 | - keepOpenACC codepath was not working 13 | - Issue with line continue symbols for multi-line directives 14 | 15 | ## [0.2.2] - 2020-03-10 16 | 17 | Minor enhancements: 18 | - Support OpenACC present directive 19 | - More robust handling of directives that translates into nothing 20 | 21 | Bug fix: 22 | - Arguements to directives were being forced into lowercase 23 | 24 | ## [0.2.1] - 2020-01-23 25 | 26 | Minor enhancements: 27 | - Original OpenACC directives can be maintained in output. 28 | - Prettify formatting when commas are present. 29 | 30 | ## [0.2] - 2020-01-10 31 | 32 | Production Release 33 | 34 | Conversion tool hardened on at least one real science code. 35 | Previously, lines with non-supported directives where completely 36 | deleted from source-2-source translation. Instead we now detect 37 | lines that are only partially translated and retain the original 38 | OpenACC. 39 | 40 | ## [0.1] - 2019-08-01 41 | 42 | Initial Release 43 | 44 | # Notes for File Format 45 | 46 | ## [Release Number] - YYYY-MM-DD 47 | 48 | Document major changes 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACC2OMP 2 | 3 | # Description 4 | A Python script to facilitate source-to-source translation from OpenACC to OpenMP. 5 | 6 | This tool was written to lower the barrier for Fortran software projects that have been using the OpenACC programming model 7 | and want to migrate to the OpenMP 4.5 programming model. For the most part, there is a 1-to-1 mapping between OpenACC directives 8 | and OpenMP directives with some exceptions. 9 | 10 | Please note that is very much a work-in-progress (WIP) and has several known limitations documented below: 11 | 12 | # Usage 13 | On the command line, type: 14 | ``./acc2omp_converter.py filename.F90`` 15 | 16 | The original file will be backed up to ``filename.F90.bak`` 17 | 18 | A number of diagnostic output is written to the standard out. It can be surpressed by setting the `debug = 0`. 19 | 20 | # Known limitations 21 | - OpenACC -> OpenMP directive mapping must be explicitly available in a dictionary 22 | - OpenACC async is not handled properly because of fundamental differences between OpenACC and OpenMP. 23 | - Only handles Fortran 24 | - No unit tests 25 | - No enforcement of PEP8 formating on source code via CI 26 | 27 | # Funding 28 | This research was supported by the Exascale Computing Project (17-SC-20-SC), a joint project of the U.S. Department of Energy’s 29 | Office of Science and National Nuclear Security Administration, responsible for delivering a capable exascale ecosystem, including 30 | software, applications, and hardware technology, to support the nation’s exascale computing imperative. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, UChicago Argonne, LLC 2 | 3 | All Rights Reserved 4 | 5 | Software Name: ACC2OMP 6 | 7 | By: Argonne National Laboratory 8 | 9 | OPEN SOURCE LICENSE 10 | 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | 1. Redistributions of source code must retain the above copyright notice, this 16 | list of conditions and the following disclaimer. 17 | 18 | 2. Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimer in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | 3. Neither the name of the copyright holder nor the names of its 23 | contributors may be used to endorse or promote products derived from 24 | this software without specific prior written permission. 25 | 26 | ****************************************************************************** 27 | 28 | DISCLAIMER 29 | 30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 31 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 32 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 33 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 34 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 35 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 36 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 37 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 38 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | 41 | ****************************************************************************** -------------------------------------------------------------------------------- /acc2omp_converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Author: Nichols A. Romero 3 | # e-mail: naromer@anl.gov 4 | # Argonne National Laboratory 5 | 6 | # Python imports 7 | import fileinput 8 | import re 9 | from shutil import copyfile 10 | 11 | # Most common user configurable parameters 12 | # Set to True for debugging and development purposes 13 | debug = True 14 | # Set to True to retain OpenACC directives in output 15 | keepOpenACC = True 16 | 17 | # Lists, dicts, and strings to aid in translation of OpenACC to OpenMP 18 | # Note that the other way would be more difficult since OpenMP tends to 19 | # be more verbose than OpenACC. 20 | 21 | # In the variable names in the program, Dir stands for directive 22 | # not directory. 23 | 24 | ompDir = '!$omp' 25 | accDir = '!$acc' 26 | ompDirContinue = '!$omp&' 27 | accDirContinue = '!$acc&' 28 | nextLineContinue = '&' 29 | 30 | emptyString = '' 31 | singleSpaceString = ' ' 32 | transitionArrow = ' -> ' 33 | backupExtString = '.bak' 34 | 35 | # directives without arguements 36 | singleDirDict = { 37 | 'loop': 'parallel do', 38 | 'gang': emptyString, 39 | 'independent': emptyString, 40 | 'parallel': 'target teams distribute', 41 | 'vector': 'simd', 42 | 'routine': 'declare target', 43 | 'seq': emptyString, 44 | 'data': 'data', 45 | 'end': 'end', 46 | 'enter': 'target enter', 47 | 'exit': 'target exit', 48 | 'atomic': 'atomic', 49 | 'serial': 'target', 50 | 'declare': 'declare target', 51 | } 52 | 53 | dualDirDict = {} 54 | 55 | # directives with arguements 56 | singleDirwargsDict = { 57 | 'attach': 'map(to:', 58 | 'detach': 'map(from:', 59 | 'copy': 'map(tofrom:', 60 | 'copyin': 'map(to:', 61 | 'copyout': 'map(from:', 62 | 'create': 'map(alloc:', 63 | 'delete': 'map(release:', 64 | 'async': 'depend(out:', 65 | 'wait': 'task depend(in:', 66 | 'collapse': 'collapse(', 67 | 'private': 'private(', 68 | 'vector_length': 'simd simdlen(', 69 | 'num_gangs': 'num_teams(', 70 | 'present': emptyString, 71 | } 72 | 73 | dualDirwargsDict = { 74 | 'update host': 'target update from(', 75 | 'update device': 'target update to(', 76 | } 77 | 78 | 79 | def remove_extra_spaces(origString): 80 | """ 81 | Converter needs extra spaces before and after commas and parenthesis 82 | removed in order work properly. 83 | """ 84 | # Space before and after a comma 85 | newString = re.sub(' *, *', ',', origString) 86 | 87 | # Space before and after left parenthesis 88 | newString = re.sub(' *\( *', '(', newString) 89 | 90 | # Space before and after right parenthesis 91 | newString = re.sub(' *\) *', ')', newString) 92 | 93 | # Add space back in for continuation symbol 94 | newString = re.sub('\)&', ') &', newString) 95 | 96 | # Add space back when newString is adjacent to another variable or 97 | # directive 98 | # \w means any single letter, digit or underscore 99 | newString = re.sub('(\))(\w)', r'\1 \2', newString) 100 | 101 | return newString 102 | 103 | 104 | def add_space_after_commas(origString): 105 | """ 106 | Directives with arguements need spaces insert after commas. 107 | """ 108 | # space after a comma 109 | newString = re.sub(',', ', ', origString) 110 | 111 | return newString 112 | 113 | 114 | if __name__ == "__main__": 115 | # This list will contain the output buffer in a line-by-line breakup 116 | entries = [] 117 | 118 | # Translate source file one line at a time 119 | lines = fileinput.input() 120 | 121 | for line in lines: 122 | # Remove extraneous spaces, but we only use 123 | # parsedLine for lines that actually contain directives 124 | origLine = line 125 | parsedLine = remove_extra_spaces(line) 126 | line = parsedLine 127 | 128 | if debug: 129 | print "extra spaces extracted below:" 130 | print line 131 | 132 | # Four cases to consider when parsing a line: 133 | # 1. Carriage return only 134 | # 2. White space only 135 | # 3. No OpenACC directive 136 | # 4. Line containing an OpenACC directive 137 | 138 | # First case is a line with only a CR 139 | if len(line) == 0: 140 | if debug: 141 | print 'Carriage return only' 142 | entries.append(origLine) 143 | continue 144 | 145 | # As long as the line is not empty (case #1), it can be 146 | # parsed. We need an iterable object and enumerate object 147 | # to aid in search for directives. We keep track of the 148 | # length of the line as well as its left justification, 149 | # but only use this when we are actually translating 150 | # directives (case #4) 151 | lenLine = len(line) 152 | numLeftSpaces = lenLine - len(line.lstrip(singleSpaceString)) 153 | dirs = line.split() 154 | lenDirs = len(dirs) 155 | enumDirs = enumerate(dirs) 156 | 157 | # Second case is a line with only a whitespace 158 | if lenDirs == 0: 159 | if debug: 160 | print 'Blank line' 161 | entries.append(origLine) 162 | continue 163 | 164 | # Third case is a line that contains no directive 165 | # Use Booleans to keep track of when an OpenACC directive 166 | # has been found, by default we assume there is no 167 | # ACC directive present. Also allow for the possibility 168 | # that uppercase is used for the OpenACC directive, though 169 | # most people will use lowercase. 170 | accDirFound = False 171 | accDirContinueFound = False 172 | if ((dirs[0].lower() != accDir) and 173 | (dirs[0].lower() != accDirContinue)): 174 | if debug: 175 | print 'No OpenACC directive on this line' 176 | entries.append(origLine) 177 | continue 178 | 179 | # Fourth case contains some OpenACC directive 180 | # From this point forward, we assume that a directive has 181 | # been found and we try to do a translation. 182 | # We will either find an OpenACC directive or a continuation 183 | # of an OpenACC directive. Check for both, but only one 184 | # must be found. 185 | if dirs[0].lower() == accDir: accDirFound = True 186 | if dirs[0].lower() == accDirContinue: accDirContinueFound = True 187 | 188 | # Detect whether they are using upper or lower case for the OpenACC 189 | # directive. Depending on the capitalization of the first instance 190 | # of an OpenACC pragma on that line will determine the 191 | # capitalization of the rest of the line. Mixed capitalization will 192 | # throw off this detection. 193 | accDirUpperCase = dirs[0].isupper() 194 | accDirLowerCase = dirs[0].islower() 195 | 196 | if debug: 197 | print "accDirUpperCase = ", accDirUpperCase 198 | print "accDirLowerCase = ", accDirLowerCase 199 | 200 | # Booleans cannot be both True or both False 201 | assert (accDirFound != accDirContinueFound) 202 | assert (accDirUpperCase != accDirLowerCase) 203 | 204 | if debug: 205 | print 'OpenACC directive present. Translating.' 206 | print dirs 207 | 208 | # These are the cases we consider 209 | # 1. Directive pairs. These are pairs of directives that only have 210 | # meaning in combinations. Thus, they must be translated in pairs. 211 | # 2. Directive pairs with arguements. 212 | # 3. Directive single with no arguements. 213 | # 4. Directive single with scalar arguments. 214 | # 5. Directive single with multi-dimensional array arguements. 215 | 216 | # First find directive pairs, this is kludgy way to search through 217 | # directives but we do pairs first because there is overlap between 218 | # keywords among the different directive categories. 219 | 220 | # Counters which are only reset at each iteration of outer loop 221 | # NOTE: If present, totalDirsFound will count nextLineContinue symbol 222 | dualDir = None 223 | totalDirsFound = 0 224 | 225 | # Booleans to keep track of what directive type has been found 226 | # Need to be reset at each iteration of inner loop 227 | singleDirFound = False 228 | singleDirwargsFound = False 229 | dualDirFound = False 230 | dualDirwargsFound = False 231 | dirwargsFound = False 232 | for i, dir in enumDirs: 233 | # first iteration just put the OMP directive or continuation 234 | # version of it into a string and go to the next iteration 235 | if i == 0: 236 | newLine = singleSpaceString * numLeftSpaces 237 | if accDirUpperCase: 238 | ompDir = ompDir.upper() 239 | ompDirContinue = ompDirContinue.upper() 240 | else: # accDirLowerCase is True 241 | ompDir = ompDir.lower() 242 | ompDirContinue = ompDirContinue.lower() 243 | if accDirFound: 244 | newLine = newLine + ompDir 245 | else: # accDirContinueFound is True 246 | newLine = newLine + ompDirContinue 247 | continue 248 | 249 | # second iteration store the first pragma in the pair 250 | if i == 1: 251 | prevdir = dir 252 | 253 | # Special detection needed for line continuation 254 | if dir == nextLineContinue: 255 | totalDirsFound = totalDirsFound + 1 256 | newLine = newLine + singleSpaceString + nextLineContinue 257 | 258 | # Additional logic would be necessary if examining 259 | # triplets of directives 260 | # take adjacent directives and create new key 261 | # store previous two directives for next iteration 262 | # if i > 1: 263 | # dualDir = prevdir + singleSpaceString + currentDir 264 | # prevdrevdir = prevdir 265 | # prevdir = dir 266 | 267 | # Some directives will have arguements, so we need to identify 268 | # those. The maxsplit arguement to the split method in dirwards 269 | # is needed to identify arrays properly. We split *only* on the 270 | # first parenthesis from the left hand side. 271 | # 272 | # Note that currentDir and dualDir must be in lowercase for pattern 273 | # matching purposes. 274 | dirwargs = dir.split('(', 1) 275 | lenDirwargs = len(dirwargs) 276 | currentDir = dirwargs[0].lower() 277 | dualDir = prevdir.lower() + singleSpaceString + currentDir 278 | 279 | if lenDirwargs > 1: dirwargsFound = True # Boolean unused for now 280 | if debug: 281 | print 'dirwargs = ', dirwargs 282 | print 'dirwargs[0] = currentDir = ', currentDir 283 | print 'lenDirswargs = ', lenDirwargs 284 | print 'dualDir =', dualDir 285 | 286 | # identify which case we are in, only one can be true at any time 287 | # Need the check on dualDir equal None, because it will not exist 288 | # on iteration = 1. 289 | if dualDir is not None: 290 | if dualDir in dualDirDict: 291 | print 'OpenACC Directive Dual with no argument found' 292 | dualDirFound = True 293 | if dualDir in dualDirwargsDict: 294 | print 'OpenACC Directive Dual with argument found' 295 | dualDirwargsFound = True 296 | 297 | if currentDir in singleDirDict: 298 | print 'OpenACC Directive Single with no argument found' 299 | singleDirFound = True 300 | if currentDir in singleDirwargsDict: 301 | print 'OpenACC Directive Single with argument found' 302 | singleDirwargsFound = True 303 | 304 | # Tests that only one is true with XOR, if not, skip this iteration 305 | # and look for a pair, otherwise, Continue 306 | if not (singleDirFound ^ singleDirwargsFound ^ 307 | dualDirFound ^ dualDirwargsFound): 308 | if debug: 309 | print "Next Iteration will check for Dual Directives Found" 310 | continue 311 | else: 312 | assert(singleDirFound ^ singleDirwargsFound ^ 313 | dualDirFound ^ dualDirwargsFound) 314 | 315 | # Code below generates the new directives depending on the value 316 | # of the boolean, probably need a function instead. 317 | 318 | # (single) directive with no arguements 319 | if singleDirFound: 320 | totalDirsFound = totalDirsFound + 1 321 | if debug: 322 | print 'OpenACC Directive Single with no argument found' 323 | newDir = singleDirDict[currentDir] 324 | if newDir == emptyString: continue 325 | if accDirUpperCase: newDir = newDir.upper() 326 | if debug: print currentDir + transitionArrow + newDir 327 | newLine = newLine + singleSpaceString + newDir 328 | 329 | # (single) directive with an arguement 330 | if (lenDirwargs > 1) and singleDirwargsFound: 331 | totalDirsFound = totalDirsFound + 1 332 | if debug: print 'OpenACC Directive Single with argument found' 333 | newDir = singleDirwargsDict[currentDir] 334 | if newDir == emptyString: continue 335 | if accDirUpperCase: newDir = newDir.upper() 336 | newLine = newLine + singleSpaceString + newDir 337 | # for-loop handles the arguement component 338 | for j in range(1, lenDirwargs): 339 | newDir = dirwargs[j] 340 | if debug: print currentDir + transitionArrow + newDir 341 | newLine = newLine + newDir 342 | 343 | # (pair) directive with no arguement 344 | if dualDirFound: 345 | totalDirsFound = totalDirsFound + 2 346 | if debug: 347 | print 'OpenACC Directive Dual with no arguement found' 348 | newDir = dualDirDict[dualDir] 349 | if newDir == emptyString: continue 350 | if accDirUpperCase: newDir = newDir.upper() 351 | if debug: 352 | print dualDir + transitionArrow + newDir 353 | newLine = newLine + singleSpaceString + newDir 354 | 355 | # (pair) directive with an arguement 356 | if (lenDirwargs > 1) and dualDirwargsFound: 357 | totalDirsFound = totalDirsFound + 2 358 | if debug: print 'OpenACC Directive Dual with an argument' 359 | newDir = dualDirwargsDict[dualDir] 360 | if newDir == emptyString: continue 361 | if accDirUpperCase: newDir = newDir.upper() 362 | newLine = newLine + singleSpaceString + newDir 363 | # for-loop handles the arguement component 364 | for j in range(1, lenDirwargs): 365 | newDir = dirwargs[j] 366 | if debug: print dualDir + transitionArrow + newDir 367 | newLine = newLine + newDir 368 | 369 | # reset booleans for next iteration 370 | singleDirFound = False 371 | singleDirwargsFound = False 372 | dualDirFound = False 373 | dualDirwargsFound = False 374 | dirwargsFound = False 375 | 376 | # End of inner loop on `i` 377 | 378 | # On last Loop iteration, check that you were able to translate 379 | # all directives. The minus one in the first conditional takes into 380 | # account the initial `!$acc` or `!$acc&` which is not counted. 381 | # If the directive cannot be translated, keep line AS IS and 382 | # output original line containing OpenACC. 383 | if (totalDirsFound < (lenDirs - 1)): 384 | if debug: 385 | print 'lenDirs=', lenDirs 386 | print 'totalDirsFound=', totalDirsFound 387 | print 'OpenACC directive could not be translated.' 388 | newLine = origLine 389 | else: 390 | if keepOpenACC: # append original line into the buffer 391 | entries.append(origLine) 392 | newLine = add_space_after_commas(newLine) + '\n' 393 | 394 | # Finally we add the new line into the buffer 395 | entries.append(newLine) 396 | 397 | # End of outer loop on `line` 398 | 399 | # We intentionally wait until the entire file is read because 400 | # fileinput module will return None only after the entire file 401 | # has been read. 402 | # First we backup the file, we have to resort to using the sys 403 | # module to get the filename because the filename() method 404 | # returns None unless the entire file has been read. 405 | currentFilename = lines.filename() 406 | backupFilename = currentFilename + backupExtString 407 | copyfile(currentFilename, backupFilename) 408 | 409 | if debug: 410 | print 'Current Filename: ', currentFilename 411 | print 'Backup Filename: ', backupFilename 412 | 413 | # Close the current open file 414 | lines.close() 415 | 416 | # Open a new file to write to that has the same source filename. 417 | # Looks like the file is modified in-place, but this is not the 418 | # case. Write the translated file to disk with the original filename 419 | with open(currentFilename, 'w') as theFile: 420 | theFile.write(''.join(entries)) 421 | --------------------------------------------------------------------------------