Skip to content

Commit

Permalink
[init] Adding CUDA language/compiler and CodePrinter (#32)
Browse files Browse the repository at this point in the history
This PR aims to make the C code compilable using nvcc. The cuda language was added as well as a CudaCodePrinter.

Changes to stdlib:

Wrapped expressions using complex types in an `ifndef __NVCC__` to avoid processing them with the nvcc compiler

---------

Co-authored-by: Mouad Elalj, EmilyBourne
  • Loading branch information
bauom authored and EmilyBourne committed Apr 23, 2024
1 parent e011652 commit 908eb82
Show file tree
Hide file tree
Showing 18 changed files with 298 additions and 83 deletions.
1 change: 1 addition & 0 deletions .dict_custom.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Valgrind
variadic
subclasses
oneAPI
Cuda
getter
setter
bitwise
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/pytest_parallel/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ runs:
steps:
- name: Test with pytest
run: |
mpiexec -n 4 ${MPI_OPTS} python -m pytest epyccel/test_parallel_epyccel.py -v -m parallel -rXx
#mpiexec -n 4 ${MPI_OPTS} python -m pytest epyccel -v -m parallel -rXx
mpiexec -n 4 ${MPI_OPTS} python -m pytest epyccel/test_parallel_epyccel.py -v -m "parallel and not cuda" -rXx
#mpiexec -n 4 ${MPI_OPTS} python -m pytest epyccel -v -m "parallel and not cuda" -rXx
shell: ${{ inputs.shell_cmd }}
working-directory: ./tests

4 changes: 2 additions & 2 deletions .github/actions/pytest_run/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ runs:
working-directory: ./tests
id: pytest_3
- name: Test Fortran translations
run: python -m pytest -n auto -rX ${FLAGS} -m "not (parallel or xdist_incompatible) and not (c or python or ccuda) ${{ inputs.pytest_mark }}" --ignore=ndarrays 2>&1 | tee s4_outfile.out
run: python -m pytest -n auto -rX ${FLAGS} -m "not (parallel or xdist_incompatible) and not (c or python or cuda) ${{ inputs.pytest_mark }}" --ignore=ndarrays 2>&1 | tee s4_outfile.out
shell: ${{ inputs.shell_cmd }}
working-directory: ./tests
id: pytest_4
- name: Test multi-file Fortran translations
run: |
python -m pytest -rX ${FLAGS} -m "xdist_incompatible and not parallel and not (c or python or ccuda) ${{ inputs.pytest_mark }}" --ignore=ndarrays 2>&1 | tee s5_outfile.out
python -m pytest -rX ${FLAGS} -m "xdist_incompatible and not parallel and not (c or python or cuda) ${{ inputs.pytest_mark }}" --ignore=ndarrays 2>&1 | tee s5_outfile.out
pyccel-clean
shell: ${{ inputs.shell_cmd }}
working-directory: ./tests
Expand Down
11 changes: 9 additions & 2 deletions .github/actions/pytest_run_cuda/action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 'Pyccel pytest commands generating Ccuda'
name: 'Pyccel pytest commands generating Cuda'
inputs:
shell_cmd:
description: 'Specifies the shell command (different for anaconda)'
Expand All @@ -11,7 +11,14 @@ runs:
- name: Ccuda tests with pytest
run: |
# Catch exit 5 (no tests found)
sh -c 'python -m pytest -n auto -rx -m "not (parallel or xdist_incompatible) and ccuda" --ignore=symbolic --ignore=ndarrays; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret'
python -m pytest -rX ${FLAGS} -m "not (xdist_incompatible or parallel) and cuda ${{ inputs.pytest_mark }}" --ignore=symbolic --ignore=ndarrays 2>&1 | tee s1_outfile.out
pyccel-clean
shell: ${{ inputs.shell_cmd }}
working-directory: ./tests
- name: Final step
if: always()
id: status
run:
python ci_tools/json_pytest_output.py -t "Cuda Test Summary" --tests "Cuda tests:${{ steps.pytest_1.outcome }}:tests/s1_outfile.out"

shell: ${{ inputs.shell_cmd }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Change Log
All notable changes to this project will be documented in this file.

## \[Cuda - UNRELEASED\]

### Added

- #32 : add support for `nvcc` Compiler and `cuda` language as a possible option.

## \[UNRELEASED\]

### Added
Expand Down
8 changes: 5 additions & 3 deletions pyccel/codegen/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
from pyccel.codegen.printing.fcode import FCodePrinter
from pyccel.codegen.printing.ccode import CCodePrinter
from pyccel.codegen.printing.pycode import PythonCodePrinter
from pyccel.codegen.printing.cucode import CudaCodePrinter

from pyccel.ast.core import FunctionDef, Interface, ModuleHeader
from pyccel.utilities.stage import PyccelStage

_extension_registry = {'fortran': 'f90', 'c':'c', 'python':'py'}
_header_extension_registry = {'fortran': None, 'c':'h', 'python':None}
_extension_registry = {'fortran': 'f90', 'c':'c', 'python':'py', 'cuda':'cu'}
_header_extension_registry = {'fortran': None, 'c':'h', 'python':None, 'cuda':'h'}
printer_registry = {
'fortran':FCodePrinter,
'c':CCodePrinter,
'python':PythonCodePrinter
'python':PythonCodePrinter,
'cuda':CudaCodePrinter
}

pyccel_stage = PyccelStage()
Expand Down
5 changes: 4 additions & 1 deletion pyccel/codegen/compiling/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,10 @@ def compile_shared_library(self, compile_obj, output_folder, verbose = False, sh
# Collect compile information
exec_cmd, includes, libs_flags, libdirs_flags, m_code = \
self._get_compile_components(compile_obj, accelerators)
linker_libdirs_flags = ['-Wl,-rpath' if l == '-L' else l for l in libdirs_flags]
if self._info['exec'] == 'nvcc':
linker_libdirs_flags = ['-Xcompiler' if l == '-L' else f'"-Wl,-rpath,{l}"' for l in libdirs_flags]
else:
linker_libdirs_flags = ['-Wl,-rpath' if l == '-L' else l for l in libdirs_flags]

flags.insert(0,"-shared")

Expand Down
5 changes: 3 additions & 2 deletions pyccel/codegen/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,10 @@ def handle_error(stage):
if language is None:
language = 'fortran'

# Choose Fortran compiler
# Choose Default compiler
if compiler is None:
compiler = os.environ.get('PYCCEL_DEFAULT_COMPILER', 'GNU')
default_compiler_family = 'nvidia' if language == 'cuda' else 'GNU'
compiler = os.environ.get('PYCCEL_DEFAULT_COMPILER', default_compiler_family)

fflags = [] if fflags is None else fflags.split()
wrapper_flags = [] if wrapper_flags is None else wrapper_flags.split()
Expand Down
74 changes: 74 additions & 0 deletions pyccel/codegen/printing/cucode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# coding: utf-8
#------------------------------------------------------------------------------------------#
# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
# go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details. #
#------------------------------------------------------------------------------------------#
"""
Provide tools for generating and handling CUDA code.
This module is designed to interface Pyccel's Abstract Syntax Tree (AST) with CUDA,
enabling the direct translation of high-level Pyccel expressions into CUDA code.
"""

from pyccel.codegen.printing.ccode import CCodePrinter, c_library_headers

from pyccel.ast.core import Import, Module

from pyccel.errors.errors import Errors


errors = Errors()

__all__ = ["CudaCodePrinter"]

class CudaCodePrinter(CCodePrinter):
"""
Print code in CUDA format.
This printer converts Pyccel's Abstract Syntax Tree (AST) into strings of CUDA code.
Navigation through this file utilizes _print_X functions,
as is common with all printers.
Parameters
----------
filename : str
The name of the file being pyccelised.
prefix_module : str
A prefix to be added to the name of the module.
"""
language = "cuda"

def __init__(self, filename, prefix_module = None):

errors.set_target(filename, 'file')

super().__init__(filename)

def _print_Module(self, expr):
self.set_scope(expr.scope)
self._current_module = expr.name
body = ''.join(self._print(i) for i in expr.body)

global_variables = ''.join(self._print(d) for d in expr.declarations)

# Print imports last to be sure that all additional_imports have been collected
imports = [Import(expr.name, Module(expr.name,(),())), *self._additional_imports.values()]
c_headers_imports = ''
local_imports = ''

for imp in imports:
if imp.source in c_library_headers:
c_headers_imports += self._print(imp)
else:
local_imports += self._print(imp)

imports = f'{c_headers_imports}\
extern "C"{{\n\
{local_imports}\
}}'

code = f'{imports}\n\
{global_variables}\n\
{body}\n'

self.exit_scope()
return code
2 changes: 1 addition & 1 deletion pyccel/commands/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def pyccel(files=None, mpi=None, openmp=None, openacc=None, output_dir=None, com
# ... backend compiler options
group = parser.add_argument_group('Backend compiler options')

group.add_argument('--language', choices=('fortran', 'c', 'python'), help='Generated language')
group.add_argument('--language', choices=('fortran', 'c', 'python', 'cuda'), help='Generated language')

group.add_argument('--compiler', help='Compiler family or json file containing a compiler description {GNU,intel,PGI}')

Expand Down
13 changes: 12 additions & 1 deletion pyccel/compilers/default_compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@
},
'family': 'nvidia',
}
#------------------------------------------------------------
nvcc_info = {'exec' : 'nvcc',
'language' : 'cuda',
'debug_flags' : ("-g",),
'release_flags': ("-O3",),
'general_flags': ('--compiler-options', '-fPIC',),
'family' : 'nvidia'
}


#------------------------------------------------------------
def change_to_lib_flag(lib):
Expand Down Expand Up @@ -288,6 +297,7 @@ def change_to_lib_flag(lib):
pgfortran_info.update(python_info)
nvc_info.update(python_info)
nvfort_info.update(python_info)
nvcc_info.update(python_info)

available_compilers = {('GNU', 'c') : gcc_info,
('GNU', 'fortran') : gfort_info,
Expand All @@ -296,6 +306,7 @@ def change_to_lib_flag(lib):
('PGI', 'c') : pgcc_info,
('PGI', 'fortran') : pgfortran_info,
('nvidia', 'c') : nvc_info,
('nvidia', 'fortran') : nvfort_info}
('nvidia', 'fortran') : nvfort_info,
('nvidia', 'cuda'): nvcc_info}

vendors = ('GNU','intel','PGI','nvidia')
4 changes: 3 additions & 1 deletion pyccel/naming/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from .fortrannameclashchecker import FortranNameClashChecker
from .cnameclashchecker import CNameClashChecker
from .pythonnameclashchecker import PythonNameClashChecker
from .cudanameclashchecker import CudaNameClashChecker

name_clash_checkers = {'fortran':FortranNameClashChecker(),
'c':CNameClashChecker(),
'python':PythonNameClashChecker()}
'python':PythonNameClashChecker(),
'cuda':CudaNameClashChecker()}
92 changes: 92 additions & 0 deletions pyccel/naming/cudanameclashchecker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# coding: utf-8
#------------------------------------------------------------------------------------------#
# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
# go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details. #
#------------------------------------------------------------------------------------------#
"""
Handles name clash problems in Cuda
"""
from .languagenameclashchecker import LanguageNameClashChecker

class CudaNameClashChecker(LanguageNameClashChecker):
"""
Class containing functions to help avoid problematic names in Cuda.
A class which provides functionalities to check or propose variable names and
verify that they do not cause name clashes. Name clashes may be due to
new variables, or due to the use of reserved keywords.
"""
# Keywords as mentioned on https://en.cppreference.com/w/c/keyword
keywords = set(['isign', 'fsign', 'csign', 'auto', 'break', 'case', 'char', 'const',
'continue', 'default', 'do', 'double', 'else', 'enum',
'extern', 'float', 'for', 'goto', 'if', 'inline', 'int',
'long', 'register', 'restrict', 'return', 'short', 'signed',
'sizeof', 'static', 'struct', 'switch', 'typedef', 'union',
'unsigned', 'void', 'volatile', 'whie', '_Alignas',
'_Alignof', '_Atomic', '_Bool', '_Complex', 'Decimal128',
'_Decimal32', '_Decimal64', '_Generic', '_Imaginary',
'_Noreturn', '_Static_assert', '_Thread_local', 't_ndarray',
'array_create', 'new_slice', 'array_slicing', 'alias_assign',
'transpose_alias_assign', 'array_fill', 't_slice',
'GET_INDEX_EXP1', 'GET_INDEX_EXP2', 'GET_INDEX_EXP2',
'GET_INDEX_EXP3', 'GET_INDEX_EXP4', 'GET_INDEX_EXP5',
'GET_INDEX_EXP6', 'GET_INDEX_EXP7', 'GET_INDEX_EXP8',
'GET_INDEX_EXP9', 'GET_INDEX_EXP10', 'GET_INDEX_EXP11',
'GET_INDEX_EXP12', 'GET_INDEX_EXP13', 'GET_INDEX_EXP14',
'GET_INDEX_EXP15', 'NUM_ARGS_H1', 'NUM_ARGS',
'GET_INDEX_FUNC_H2', 'GET_INDEX_FUNC', 'GET_INDEX',
'INDEX', 'GET_ELEMENT', 'free_array', 'free_pointer',
'get_index', 'numpy_to_ndarray_strides',
'numpy_to_ndarray_shape', 'get_size', 'order_f', 'order_c', 'array_copy_data'])

def has_clash(self, name, symbols):
"""
Indicate whether the proposed name causes any clashes.
Checks if a suggested name conflicts with predefined
keywords or specified symbols,returning true for a clash.
This method is crucial for maintaining namespace integrity and
preventing naming conflicts in code generation processes.
Parameters
----------
name : str
The suggested name.
symbols : set
Symbols which should be considered as collisions.
Returns
-------
bool
True if the name is a collision.
False if the name is collision free.
"""
return any(name == k for k in self.keywords) or \
any(name == s for s in symbols)

def get_collisionless_name(self, name, symbols):
"""
Get a valid name which doesn't collision with symbols or Cuda keywords.
Find a new name based on the suggested name which will not cause
conflicts with Cuda keywords, does not appear in the provided symbols,
and is a valid name in Cuda code.
Parameters
----------
name : str
The suggested name.
symbols : set
Symbols which should be considered as collisions.
Returns
-------
str
A new name which is collision free.
"""
if len(name)>4 and all(name[i] == '_' for i in (0,1,-1,-2)):
# Ignore magic methods
return name
if name[0] == '_':
name = 'private'+name
return self._get_collisionless_name(name, symbols)
2 changes: 2 additions & 0 deletions pyccel/stdlib/numpy/numpy_c.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ double fsign(double x)
return SIGN(x);
}

#ifndef __NVCC__
/* numpy.sign for complex */
double complex csign(double complex x)
{
return x ? ((!creal(x) && cimag(x) < 0) || (creal(x) < 0) ? -1 : 1) : 0;
}
#endif
2 changes: 2 additions & 0 deletions pyccel/stdlib/numpy/numpy_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

long long int isign(long long int x);
double fsign(double x);
#ifndef __NVCC__
double complex csign(double complex x);
#endif

#endif
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ markers =
python: test to generate python code
xdist_incompatible: test which compiles a file also compiled by another test
external: test using an external dll (problematic with conda on Windows)
cuda: test to generate cuda code
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
def language(request):
return request.param

@pytest.fixture( params=[
pytest.param("fortran", marks = pytest.mark.fortran),
pytest.param("c", marks = pytest.mark.c),
pytest.param("python", marks = pytest.mark.python),
pytest.param("cuda", marks = pytest.mark.cuda)
],
scope = "session"
)
def language_with_cuda(request):
return request.param

def move_coverage(path_dir):
for root, _, files in os.walk(path_dir):
for name in files:
Expand Down
Loading

0 comments on commit 908eb82

Please sign in to comment.