Skip to content

Commit e0b1640

Browse files
authored
Merge pull request #1034 from FlorianDeconinck/feature/fdeconinck/MKIAU_fortran_python_bridge
[MKIAU] Initial prototype of a CFFI-based fortran/python bridge
2 parents 0828820 + ccc6ce5 commit e0b1640

File tree

13 files changed

+734
-4
lines changed

13 files changed

+734
-4
lines changed

GEOSmkiau_GridComp/CMakeLists.txt

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
esma_set_this()
22

3+
option(BUILD_PYMKIAU_INTERFACE "Build pyMKIAU interface" OFF)
4+
35
set (srcs
46
IAU_GridCompMod.F90
57
GEOS_mkiauGridComp.F90
@@ -8,6 +10,90 @@ set (srcs
810
DynVec_GridComp.F90
911
)
1012

11-
set(dependencies MAPL_cfio_r4 NCEP_sp_r4i4 GEOS_Shared GMAO_mpeu MAPL FVdycoreCubed_GridComp ESMF::ESMF NetCDF::NetCDF_Fortran)
12-
esma_add_library (${this} SRCS ${srcs} DEPENDENCIES ${dependencies})
13+
if (BUILD_PYMKIAU_INTERFACE)
14+
list (APPEND srcs
15+
pyMKIAU/interface/interface.f90
16+
pyMKIAU/interface/interface.c)
17+
18+
message(STATUS "Building pyMKIAU interface")
19+
20+
add_definitions(-DPYMKIAU_INTEGRATION)
21+
22+
# The Python library creation requires mpiexec/mpirun to run on a
23+
# compute node. Probably a weird SLURM thing?
24+
find_package(Python3 COMPONENTS Interpreter REQUIRED)
25+
26+
# Set up some variables in case names change
27+
set(PYMKIAU_INTERFACE_LIBRARY ${CMAKE_CURRENT_BINARY_DIR}/libpyMKIAU_interface_py.so)
28+
set(PYMKIAU_INTERFACE_HEADER_FILE ${CMAKE_CURRENT_BINARY_DIR}/pyMKIAU_interface_py.h)
29+
set(PYMKIAU_INTERFACE_FLAG_HEADER_FILE ${CMAKE_CURRENT_SOURCE_DIR}/pyMKIAU/interface/interface.h)
30+
set(PYMKIAU_INTERFACE_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/pyMKIAU/interface/interface.py)
31+
32+
# This command creates the shared object library from Python
33+
add_custom_command(
34+
OUTPUT ${PYMKIAU_INTERFACE_LIBRARY}
35+
# Note below is essentially:
36+
# mpirun -np 1 python file
37+
# but we use the CMake options as much as we can for flexibility
38+
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PYMKIAU_INTERFACE_FLAG_HEADER_FILE} ${CMAKE_CURRENT_BINARY_DIR}
39+
COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 1 ${Python3_EXECUTABLE} ${PYMKIAU_INTERFACE_SRCS}
40+
BYPRODUCTS ${PYMKIAU_INTERFACE_HEADER_FILE}
41+
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
42+
MAIN_DEPENDENCY ${PYMKIAU_INTERFACE_SRCS}
43+
COMMENT "Building pyMKIAU interface library with Python"
44+
VERBATIM
45+
)
46+
47+
# This creates a target we can use for dependencies and post build
48+
add_custom_target(generate_pyMKIAU_interface_library DEPENDS ${PYMKIAU_INTERFACE_LIBRARY})
1349

50+
# Because of the weird hacking of INTERFACE libraries below, we cannot
51+
# use the "usual" CMake calls to install() the .so. I think it's because
52+
# INTERFACE libraries don't actually produce any artifacts as far as
53+
# CMake is concerned. So we add a POST_BUILD custom command to "install"
54+
# the library into install/lib
55+
add_custom_command(TARGET generate_pyMKIAU_interface_library
56+
POST_BUILD
57+
# We first need to make a lib dir if it doesn't exist. If not, then
58+
# the next command can copy the script into a *file* called lib because
59+
# of a race condition (if install/lib/ isn't mkdir'd first)
60+
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_INSTALL_PREFIX}/lib
61+
# Now we copy the file (if different...though not sure if this is useful)
62+
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${PYMKIAU_INTERFACE_LIBRARY}" ${CMAKE_INSTALL_PREFIX}/lib
63+
)
64+
65+
# We use INTERFACE libraries to create a sort of "fake" target library we can use
66+
# to make libFVdycoreCubed_GridComp.a depend on. It seems to work!
67+
add_library(pyMKIAU_interface_py INTERFACE)
68+
69+
# The target_include_directories bits were essentially stolen from the esma_add_library
70+
# code...
71+
target_include_directories(pyMKIAU_interface_py INTERFACE
72+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
73+
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> # stubs
74+
# modules and copied *.h, *.inc
75+
$<BUILD_INTERFACE:${esma_include}/${this}>
76+
$<INSTALL_INTERFACE:include/${this}>
77+
)
78+
target_link_libraries(pyMKIAU_interface_py INTERFACE ${PYMKIAU_INTERFACE_LIBRARY})
79+
80+
# This makes sure the library is built first
81+
add_dependencies(pyMKIAU_interface_py generate_pyMKIAU_interface_library)
82+
83+
# This bit is to resolve an issue and Google told me to do this. I'm not
84+
# sure that the LIBRARY DESTINATION bit actually does anything since
85+
# this is using INTERFACE
86+
install(TARGETS pyMKIAU_interface_py
87+
EXPORT ${PROJECT_NAME}-targets
88+
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
89+
)
90+
91+
endif ()
92+
93+
if (BUILD_PYMKIAU_INTERFACE)
94+
set(dependencies pyMKIAU_interface_py MAPL_cfio_r4 NCEP_sp_r4i4 GEOS_Shared GMAO_mpeu MAPL FVdycoreCubed_GridComp ESMF::ESMF NetCDF::NetCDF_Fortran)
95+
else ()
96+
set(dependencies MAPL_cfio_r4 NCEP_sp_r4i4 GEOS_Shared GMAO_mpeu MAPL FVdycoreCubed_GridComp ESMF::ESMF NetCDF::NetCDF_Fortran)
97+
endif ()
98+
99+
esma_add_library (${this} SRCS ${srcs} DEPENDENCIES ${dependencies})

GEOSmkiau_GridComp/GEOS_mkiauGridComp.F90

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ module GEOS_mkiauGridCompMod
1919
use GEOS_UtilsMod
2020
! use GEOS_RemapMod, only: myremap => remap
2121
use m_set_eta, only: set_eta
22+
#ifdef PYMKIAU_INTEGRATION
23+
use pyMKIAU_interface_mod
24+
use ieee_exceptions, only: ieee_get_halting_mode, ieee_set_halting_mode, ieee_all
25+
#endif
2226
implicit none
2327
private
2428

@@ -91,8 +95,15 @@ subroutine SetServices ( GC, RC )
9195
type (ESMF_Config) :: CF
9296

9397
logical :: BLEND_AT_PBL
94-
95-
!=============================================================================
98+
#ifdef PYMKIAU_INTEGRATION
99+
! IEEE trapping see below
100+
logical :: halting_mode(5)
101+
! BOGUS DATA TO SHOW USAGE
102+
type(a_pod_struct_type) :: options
103+
real, allocatable, dimension(:,:,:) :: in_buffer
104+
real, allocatable, dimension(:,:,:) :: out_buffer
105+
#endif
106+
!=============================================================================
96107

97108
! Begin...
98109

@@ -459,6 +470,25 @@ subroutine SetServices ( GC, RC )
459470
call MAPL_GenericSetServices ( gc, RC=STATUS)
460471
VERIFY_(STATUS)
461472

473+
#ifdef PYMKIAU_INTEGRATION
474+
! Spin the interface - we have to deactivate the ieee error
475+
! to be able to load numpy, scipy and other numpy packages
476+
! that generate NaN as an init mechanism for numerical solving
477+
call ieee_get_halting_mode(ieee_all, halting_mode)
478+
call ieee_set_halting_mode(ieee_all, .false.)
479+
call pyMKIAU_interface_f_setservice()
480+
call ieee_set_halting_mode(ieee_all, halting_mode)
481+
482+
! BOGUS CODE TO SHOW USAGE
483+
options%npx = 10
484+
options%npy = 11
485+
options%npz = 12
486+
allocate (in_buffer(10,11,12), source = 42.42 )
487+
allocate (out_buffer(10,11,12), source = 0.0 )
488+
call pyMKIAU_interface_f_run(options, in_buffer, out_buffer)
489+
write(*,*) "[pyMKIAU] From fortran OUT[5,5,5] is ", out_buffer(5,5,5)
490+
#endif
491+
462492
RETURN_(ESMF_SUCCESS)
463493

464494
end subroutine SetServices
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
__pycache__/
2+
*.py[cod]
3+
*$py.class
4+
.pytest_cache
5+
*.egg-info/
6+
test_data/
7+
.gt_cache_*
8+
.translate-*/
9+
.vscode
10+
test_data/
11+
sandbox/
12+
*.mod
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Fortran - Python bridge prototype
2+
3+
Nomenclatura: we call the brige "fpy" and "c", "f" and "py" denotes functions in their respective language.
4+
5+
Building: you have to pass `-DBUILD_PYMKIAU_INTERFACE=ON` to your `cmake` command to turn on the interface build and execution.
6+
7+
## Pipeline
8+
9+
Here's a quick rundown of how a buffer travels through the interface and back.
10+
11+
- From Fortran in `GEOS_MKIAUGridComp:488` we call `pyMKIAU_interface_f_run` with the buffer passed as argument
12+
- 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)
13+
- 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`
14+
- 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`
15+
- In the package, the `serservices` or `run` function is called.
16+
17+
## Fortran <--> C: iso_c_binding
18+
19+
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.
20+
The two big ones are:
21+
22+
- strings need to be send/received as a buffer plus a length,
23+
- pointers/buffers are _not_ able to be pushed into a user type.
24+
25+
## C <->Python: CFFI based glue
26+
27+
The interface is based on CFFI which is reponsible for the heavy lifting of
28+
29+
- spinning a python interpreter
30+
- passing memory between C and Python without a copy
31+
32+
## Running python
33+
34+
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.
35+
The easy way to know is that you need to be able to get into your environment and run in a python terminal:
36+
37+
```python
38+
from pyMKIAU.core import pyMKIAU_init
39+
pyMKIAU_init()
40+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#include <stdio.h>
2+
#include <time.h>
3+
#include "interface.h"
4+
5+
extern int pyMKIAU_interface_c_setservice()
6+
{
7+
// Check magic number
8+
int return_code = pyMKIAU_interface_py_setservices();
9+
10+
if (return_code < 0)
11+
{
12+
exit(return_code);
13+
}
14+
}
15+
16+
extern int pyMKIAU_interface_c_run(a_pod_struct_t *options, const float *in_buffer, float *out_buffer)
17+
{
18+
// Check magic number
19+
if (options->mn_123456789 != 123456789)
20+
{
21+
printf("Magic number failed, pyMKIAU interface is broken on the C side\n");
22+
exit(-1);
23+
}
24+
25+
int return_code = pyMKIAU_interface_py_run(options, in_buffer, out_buffer);
26+
27+
if (return_code < 0)
28+
{
29+
exit(return_code);
30+
}
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module pyMKIAU_interface_mod
2+
3+
use iso_c_binding, only: c_int, c_float, c_double, c_bool, c_ptr
4+
5+
implicit none
6+
7+
private
8+
public :: pyMKIAU_interface_f_setservice, pyMKIAU_interface_f_run
9+
public :: a_pod_struct_type
10+
11+
!-----------------------------------------------------------------------
12+
! See `interface.h` for explanation of the POD-strict struct
13+
!-----------------------------------------------------------------------
14+
type, bind(c) :: a_pod_struct_type
15+
integer(kind=c_int) :: npx
16+
integer(kind=c_int) :: npy
17+
integer(kind=c_int) :: npz
18+
! Magic number
19+
integer(kind=c_int) :: make_flags_C_interop = 123456789
20+
end type
21+
22+
23+
interface
24+
25+
subroutine pyMKIAU_interface_f_setservice() bind(c, name='pyMKIAU_interface_c_setservice')
26+
end subroutine pyMKIAU_interface_f_setservice
27+
28+
subroutine pyMKIAU_interface_f_run(options, in_buffer, out_buffer) bind(c, name='pyMKIAU_interface_c_run')
29+
30+
import c_float, a_pod_struct_type
31+
32+
implicit none
33+
! This is an interface to a C function, the intent ARE NOT enforced
34+
! by the compiler. Consider them developer hints
35+
type(a_pod_struct_type), intent(in) :: options
36+
real(kind=c_float), dimension(*), intent(in) :: in_buffer
37+
real(kind=c_float), dimension(*), intent(out) :: out_buffer
38+
39+
end subroutine pyMKIAU_interface_f_run
40+
41+
end interface
42+
43+
end module pyMKIAU_interface_mod
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#pragma once
2+
3+
/***
4+
* C Header for the interface to python.
5+
* Define here any POD-strict structures and external functions
6+
* that will get exported by cffi from python (see interface.py)
7+
***/
8+
9+
#include <stdbool.h>
10+
#include <stdlib.h>
11+
12+
// POD-strict structure to pack options and flags efficiently
13+
// Struct CANNOT hold pointers. The iso_c_binding does not allow for foolproof
14+
// pointer memory packing.
15+
// We use the low-embedded trick of the magic number to attempt to catch
16+
// any type mismatch betweeen Fortran and C. This is not a foolproof method
17+
// but it bring a modicum of check at the cost of a single integer.
18+
typedef struct
19+
{
20+
int npx;
21+
int npy;
22+
int npz;
23+
// Magic number needs to be last item
24+
int mn_123456789;
25+
} a_pod_struct_t;
26+
27+
// For complex type that can be exported with different
28+
// types (like the MPI communication object), you can rely on C `union`
29+
typedef union
30+
{
31+
int comm_int;
32+
void *comm_ptr;
33+
} MPI_Comm_t;
34+
35+
// Python hook functions: defined as external so that the .so can link out ot them
36+
// Though we define `in_buffer` as a `const float*` it is _not_ enforced
37+
// by the interface. Treat as a developer hint only.
38+
39+
extern int pyMKIAU_interface_py_run(a_pod_struct_t *options, const float *in_buffer, float *out_buffer);
40+
extern int pyMKIAU_interface_py_setservices();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import cffi # type: ignore
2+
3+
TMPFILEBASE = "pyMKIAU_interface_py"
4+
5+
ffi = cffi.FFI()
6+
7+
source = """
8+
from {} import ffi
9+
from datetime import datetime
10+
from pyMKIAU.core import pyMKIAU_init, pyMKIAU_run #< User code starts here
11+
import traceback
12+
13+
@ffi.def_extern()
14+
def pyMKIAU_interface_py_setservices() -> int:
15+
16+
try:
17+
# Calling out off the bridge into the python
18+
pyMKIAU_init()
19+
except Exception as err:
20+
print("Error in Python:")
21+
print(traceback.format_exc())
22+
return -1
23+
return 0
24+
25+
@ffi.def_extern()
26+
def pyMKIAU_interface_py_run(options, in_buffer, out_buffer) -> int:
27+
28+
try:
29+
# Calling out off the bridge into the python
30+
pyMKIAU_run(options, in_buffer, out_buffer)
31+
except Exception as err:
32+
print("Error in Python:")
33+
print(traceback.format_exc())
34+
return -1
35+
return 0
36+
""".format(TMPFILEBASE)
37+
38+
with open("interface.h") as f:
39+
data = "".join([line for line in f if not line.startswith("#")])
40+
data = data.replace("CFFI_DLLEXPORT", "")
41+
ffi.embedding_api(data)
42+
43+
ffi.set_source(TMPFILEBASE, '#include "interface.h"')
44+
45+
ffi.embedding_init_code(source)
46+
ffi.compile(target="lib" + TMPFILEBASE + ".so", verbose=True)

GEOSmkiau_GridComp/pyMKIAU/pyMKIAU/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)