-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial prototype of a CFFI-based fortran/python bridge
Bogus data code to show usage Ships with an f_py memory converted and a Timer capable of GPU via `cupy`
- Loading branch information
1 parent
96a8a26
commit 461de4f
Showing
13 changed files
with
733 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
.pytest_cache | ||
*.egg-info/ | ||
test_data/ | ||
.gt_cache_* | ||
.translate-*/ | ||
.vscode | ||
test_data/ | ||
sandbox/ | ||
*.mod |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Fortran - Python bridge prototype | ||
|
||
Nomenclatura: we call the brige "fpy" and "c", "f" and "py" denotes functions in their respective language. | ||
|
||
Building: you have to pass `-DBUILD_PYMKIAU_INTERFACE=ON` to your `cmake` command to turn on the interface build and execution. | ||
|
||
## Pipeline | ||
|
||
Here's a quick rundown of how a buffer travels through the interface and back. | ||
|
||
- From Fortran in `GEOS_MKIAUGridComp:488` we call `pyMKIAU_interface_f_run` with the buffer passed as argument | ||
- This pings the interface, located at `pyMKIAU/interface/interface.f90`. This interface uses the `iso_c_binding` to marshall the parameters downward (careful about the user type, look at the code) | ||
- Fortran then call into C at `pyMKIAU/interface/interface.c`. Those functions now expect that a few `extern` hooks have been made available on the python side, they are define in `pyMKIAU/interface/interface.h` | ||
- At runtime, the hooks are found and code carries to the python thanks to cffi. The .so that exposes the hooks is in `pyMKIAU/interface/interface.py`. Within this code, we: expose extern functions via `ffi.extern`, build a shared library to link for runtime and pass the code down to the `pyMKIAU` python package which lives at `pyMKIAU/pyMKIAU` | ||
- In the package, the `serservices` or `run` function is called. | ||
|
||
## Fortran <--> C: iso_c_binding | ||
|
||
We leverage Fortan `iso_c_binding` extension to do conform Fortran and C calling structure. Which comes with a bunch of easy type casting and some pretty steep potholes. | ||
The two big ones are: | ||
|
||
- strings need to be send/received as a buffer plus a length, | ||
- pointers/buffers are _not_ able to be pushed into a user type. | ||
|
||
## C <->Python: CFFI based glue | ||
|
||
The interface is based on CFFI which is reponsible for the heavy lifting of | ||
|
||
- spinning a python interpreter | ||
- passing memory between C and Python without a copy | ||
|
||
## Running python | ||
|
||
The last trick is to make sure your package is callable by the `interface.py`. Basically your code has to be accessible by the interpreter, be via virtual env, conda env or PYTHONPATH. The easy way to know is that you need to be able to get into your environment and run in a python terminal | ||
|
||
```python | ||
from pyMKIAU.core import pyMKIAU_init | ||
pyMKIAU_init() | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
#include <stdio.h> | ||
#include <time.h> | ||
#include "interface.h" | ||
|
||
extern int pyMKIAU_interface_c_setservice() | ||
{ | ||
// Check magic number | ||
int return_code = pyMKIAU_interface_py_setservices(); | ||
|
||
if (return_code < 0) | ||
{ | ||
exit(return_code); | ||
} | ||
} | ||
|
||
extern int pyMKIAU_interface_c_run(a_pod_struct_t *options, const float *in_buffer, float *out_buffer) | ||
{ | ||
// Check magic number | ||
if (options->mn_123456789 != 123456789) | ||
{ | ||
printf("Magic number failed, pyMKIAU interface is broken on the C side\n"); | ||
exit(-1); | ||
} | ||
|
||
int return_code = pyMKIAU_interface_py_run(options, in_buffer, out_buffer); | ||
|
||
if (return_code < 0) | ||
{ | ||
exit(return_code); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
module pyMKIAU_interface_mod | ||
|
||
use iso_c_binding, only: c_int, c_float, c_double, c_bool, c_ptr | ||
|
||
implicit none | ||
|
||
private | ||
public :: pyMKIAU_interface_f_setservice, pyMKIAU_interface_f_run | ||
public :: a_pod_struct_type | ||
|
||
!----------------------------------------------------------------------- | ||
! See `interface.h` for explanation of the POD-strict struct | ||
!----------------------------------------------------------------------- | ||
type, bind(c) :: a_pod_struct_type | ||
integer(kind=c_int) :: npx | ||
integer(kind=c_int) :: npy | ||
integer(kind=c_int) :: npz | ||
! Magic number | ||
integer(kind=c_int) :: make_flags_C_interop = 123456789 | ||
end type | ||
|
||
|
||
interface | ||
|
||
subroutine pyMKIAU_interface_f_setservice() bind(c, name='pyMKIAU_interface_c_setservice') | ||
end subroutine pyMKIAU_interface_f_setservice | ||
|
||
subroutine pyMKIAU_interface_f_run(options, in_buffer, out_buffer) bind(c, name='pyMKIAU_interface_c_run') | ||
|
||
import c_float, a_pod_struct_type | ||
|
||
implicit none | ||
! This is an interface to a C function, the intent ARE NOT enforced | ||
! by the compiler. Consider them developer hints | ||
type(a_pod_struct_type), intent(in) :: options | ||
real(kind=c_float), dimension(*), intent(in) :: in_buffer | ||
real(kind=c_float), dimension(*), intent(out) :: out_buffer | ||
|
||
end subroutine pyMKIAU_interface_f_run | ||
|
||
end interface | ||
|
||
end module pyMKIAU_interface_mod |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
#pragma once | ||
|
||
/*** | ||
* C Header for the interface to python. | ||
* Define here any POD-strict structures and external functions | ||
* that will get exported by cffi from python (see interface.py) | ||
***/ | ||
|
||
#include <stdbool.h> | ||
#include <stdlib.h> | ||
|
||
// POD-strict structure to pack options and flags efficiently | ||
// Struct CANNOT hold pointers. The iso_c_binding does not allow for foolproof | ||
// pointer memory packing. | ||
// We use the low-embedded trick of the magic number to attempt to catch | ||
// any type mismatch betweeen Fortran and C. This is not a foolproof method | ||
// but it bring a modicum of check at the cost of a single integer. | ||
typedef struct | ||
{ | ||
int npx; | ||
int npy; | ||
int npz; | ||
// Magic number needs to be last item | ||
int mn_123456789; | ||
} a_pod_struct_t; | ||
|
||
// For complex type that can be exported with different | ||
// types (like the MPI communication object), you can rely on C `union` | ||
typedef union | ||
{ | ||
int comm_int; | ||
void *comm_ptr; | ||
} MPI_Comm_t; | ||
|
||
// Python hook functions: defined as external so that the .so can link out ot them | ||
// Though we define `in_buffer` as a `const float*` it is _not_ enforced | ||
// by the interface. Treat as a developer hint only. | ||
|
||
extern int pyMKIAU_interface_py_run(a_pod_struct_t *options, const float *in_buffer, float *out_buffer); | ||
extern int pyMKIAU_interface_py_setservices(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import cffi # type: ignore | ||
|
||
TMPFILEBASE = "pyMKIAU_interface_py" | ||
|
||
ffi = cffi.FFI() | ||
|
||
source = """ | ||
from {} import ffi | ||
from datetime import datetime | ||
from pyMKIAU.core import pyMKIAU_init, pyMKIAU_run #< User code starts here | ||
import traceback | ||
@ffi.def_extern() | ||
def pyMKIAU_interface_py_setservices() -> int: | ||
try: | ||
# Calling out off the bridge into the python | ||
pyMKIAU_init() | ||
except Exception as err: | ||
print("Error in Python:") | ||
print(traceback.format_exc()) | ||
return -1 | ||
return 0 | ||
@ffi.def_extern() | ||
def pyMKIAU_interface_py_run(options, in_buffer, out_buffer) -> int: | ||
try: | ||
# Calling out off the bridge into the python | ||
pyMKIAU_run(options, in_buffer, out_buffer) | ||
except Exception as err: | ||
print("Error in Python:") | ||
print(traceback.format_exc()) | ||
return -1 | ||
return 0 | ||
""".format(TMPFILEBASE) | ||
|
||
with open("interface.h") as f: | ||
data = "".join([line for line in f if not line.startswith("#")]) | ||
data = data.replace("CFFI_DLLEXPORT", "") | ||
ffi.embedding_api(data) | ||
|
||
ffi.set_source(TMPFILEBASE, '#include "interface.h"') | ||
|
||
ffi.embedding_init_code(source) | ||
ffi.compile(target="lib" + TMPFILEBASE + ".so", verbose=True) |
Empty file.
Oops, something went wrong.