Skip to content

Commit

Permalink
Adds Python bindings to Caliper with pybind11 (#573)
Browse files Browse the repository at this point in the history
* Implements Python bindings for Caliper

* Improves the exported type enums

* Adds examples of the Python API

* Fixes default parameter for cali_attr_properties

* Removes default arugments for Attribute and Annotation in place of multiple constructors

* Adds unit tests for Python bindings

* Adds docstrings to Python bindings

* Moves Python bindings to src/interface and adds logic to help with unit testing

* Updates GH Actions runner to install Pybind11 correctly

* Removes Variant from Python bindings

* Removes redundant install of Pybind11 with apt

* Enables bitwise arithmetic in bindings for cali_attr_properties
  • Loading branch information
ilumsden authored Sep 19, 2024
1 parent d1f280e commit 4029342
Show file tree
Hide file tree
Showing 28 changed files with 1,233 additions and 5 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ jobs:

steps:
- uses: actions/checkout@v2

- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'

- name: Install dependencies
run: sudo apt-get install libdw-dev libunwind-dev gfortran
run: |
sudo apt-get install libdw-dev libunwind-dev gfortran
python3 -m pip install pybind11
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -C ${{github.workspace}}/cmake/hostconfig/github-actions.cmake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -Dpybind11_DIR=$(pybind11-config --cmakedir) -C ${{github.workspace}}/cmake/hostconfig/github-actions.cmake

- name: Build
# Build your program with the given configuration
Expand Down
16 changes: 13 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ option(BUILD_SHARED_LIBS "Build shared libraries" TRUE)
option(CMAKE_INSTALL_RPATH_USE_LINK_PATH "Add rpath for all dependencies" TRUE)

# Optional Fortran
add_caliper_option(WITH_FORTRAN "Install Fortran interface" FALSE)
add_caliper_option(WITH_TOOLS "Build Caliper tools" TRUE)
add_caliper_option(WITH_FORTRAN "Install Fortran interface" FALSE)
add_caliper_option(WITH_PYTHON_BINDINGS "Install Python bindings" FALSE)
add_caliper_option(WITH_TOOLS "Build Caliper tools" TRUE)

add_caliper_option(WITH_NVTX "Enable NVidia nvtx bindings for NVprof and NSight (requires CUDA)" FALSE)
add_caliper_option(WITH_CUPTI "Enable CUPTI service (CUDA performance analysis)" FALSE)
Expand Down Expand Up @@ -421,7 +422,12 @@ if(WITH_TAU)
endif()

# Find Python
find_package(Python COMPONENTS Interpreter REQUIRED)
set(FIND_PYTHON_COMPONENTS "Interpreter")
if (WITH_PYTHON_BINDINGS)
set(FIND_PYTHON_COMPONENTS "Development" ${FIND_PYTHON_COMPONENTS})
endif ()

find_package(Python COMPONENTS ${FIND_PYTHON_COMPONENTS} REQUIRED)
set(CALI_PYTHON_EXECUTABLE Python::Interpreter)

if (WITH_SAMPLER)
Expand Down Expand Up @@ -495,6 +501,10 @@ configure_file(
include_directories(${PROJECT_BINARY_DIR}/include)
include_directories(include)

if (WITH_PYTHON_BINDINGS AND BUILD_TESTING)
set(PYPATH_TESTING "" CACHE INTERNAL "")
endif()

add_subdirectory(ext)
add_subdirectory(src)

Expand Down
14 changes: 14 additions & 0 deletions cmake/get_python_install_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sys
import sysconfig

if len(sys.argv) != 3 or sys.argv[1] not in ("purelib", "platlib"):
raise RuntimeError(
"Usage: python get_python_install_paths.py <purelib | platlib> <sysconfig_scheme>"
)

install_dir = sysconfig.get_path(sys.argv[1], sys.argv[2], {"userbase": "", "base": ""})

if install_dir.startswith("/"):
install_dir = install_dir[1:]

print(install_dir, end="")
1 change: 1 addition & 0 deletions cmake/hostconfig/github-actions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(WITH_NVPROF Off CACHE BOOL "")
set(WITH_PAPI Off CACHE BOOL "")
set(WITH_SAMPLER On CACHE BOOL "")
set(WITH_VTUNE Off CACHE BOOL "")
set(WITH_PYTHON_BINDINGS On CACHE BOOL "")

set(WITH_DOCS Off CACHE BOOL "")
set(BUILD_TESTING On CACHE BOOL "")
48 changes: 48 additions & 0 deletions examples/apps/cali-perfproblem-branch-mispred.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) 2024, Lawrence Livermore National Security, LLC.
# See top-level LICENSE file for details.

from pycaliper.high_level import annotate_function
from pycaliper.annotation import Annotation

import numpy as np

@annotate_function()
def init(arraySize: int, sort: bool) -> np.array:
data = np.random.randint(256, size=arraySize)
if sort:
data = np.sort(data)
return data


@annotate_function()
def work(data: np.array):
data_sum = 0
for _ in range(100):
for val in np.nditer(data):
if val >= 128:
data_sum += val
print("sum =", data_sum)


@annotate_function()
def benchmark(arraySize: int, sort: bool):
sorted_ann = Annotation("sorted")
sorted_ann.set(sort)
print("Intializing benchmark data with sort =", sort)
data = init(arraySize, sort)
print("Calculating sum of values >= 128")
work(data)
print("Done!")
sorted_ann.end()


@annotate_function()
def main():
arraySize = 32768
benchmark(arraySize, True)
benchmark(arraySize, False)


if __name__ == "__main__":
main()

76 changes: 76 additions & 0 deletions examples/apps/py-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) 2024, Lawrence Livermore National Security, LLC.
# See top-level LICENSE file for details.

from pycaliper.high_level import annotate_function
from pycaliper.config_manager import ConfigManager
from pycaliper.instrumentation import (
set_global_byname,
begin_region,
end_region,
)
from pycaliper.loop import Loop

import argparse
import sys
import time


def get_available_specs_doc(mgr: ConfigManager):
doc = ""
for cfg in mgr.available_config_specs():
doc += mgr.get_documentation_for_spec(cfg)
doc += "\n"
return doc


@annotate_function()
def foo(i: int) -> float:
nsecs = max(i * 500, 100000)
secs = nsecs / 10**9
time.sleep(secs)
return 0.5 * i


def main():
mgr = ConfigManager()

parser = argparse.ArgumentParser()
parser.add_argument("--caliper_config", "-P", type=str, default="",
help="Configuration for Caliper\n{}".format(get_available_specs_doc(mgr)))
parser.add_argument("iterations", type=int, nargs="?", default=4,
help="Number of iterations")
args = parser.parse_args()

mgr.add(args.caliper_config)

if mgr.error():
print("Caliper config error:", mgr, file=sys.stderr)

mgr.start()

set_global_byname("iterations", args.iterations)
set_global_byname("caliper.config", args.caliper_config)

begin_region("main")

begin_region("init")
t = 0
end_region("init")

loop_ann = Loop("mainloop")

for i in range(args.iterations):
loop_ann.start_iteration(i)
t *= foo(i)
loop_ann.end_iteration()

loop_ann.end()

end_region("main")

mgr.flush()


if __name__ == "__main__":
main()

4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ if (WITH_TOOLS)
add_subdirectory(tools)
endif()

if (WITH_PYTHON_BINDINGS)
add_subdirectory(interface/python)
endif()

install(
TARGETS
caliper
Expand Down
64 changes: 64 additions & 0 deletions src/interface/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
set(PYCALIPER_BINDING_SOURCES
annotation.cpp
config_manager.cpp
instrumentation.cpp
loop.cpp
mod.cpp
)

set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)

find_package(pybind11 CONFIG REQUIRED)

set(PYCALIPER_SYSCONFIG_SCHEME "posix_user" CACHE STRING "Scheme used for searching for pycaliper's install path. Valid options can be determined with 'sysconfig.get_scheme_names()'")

execute_process(COMMAND ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/cmake/get_python_install_paths.py purelib ${PYCALIPER_SYSCONFIG_SCHEME} OUTPUT_VARIABLE PYCALIPER_SITELIB)
execute_process(COMMAND ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/cmake/get_python_install_paths.py platlib ${PYCALIPER_SYSCONFIG_SCHEME} OUTPUT_VARIABLE PYCALIPER_SITEARCH)

message(STATUS "Pycaliper sitelib: ${PYCALIPER_SITELIB}")
message(STATUS "Pycaliper sitearch: ${PYCALIPER_SITEARCH}")

set(PYCALIPER_SITELIB "${PYCALIPER_SITELIB}/pycaliper")
set(PYCALIPER_SITEARCH "${PYCALIPER_SITEARCH}/pycaliper")

pybind11_add_module(__pycaliper_impl ${PYCALIPER_BINDING_SOURCES})
target_link_libraries(__pycaliper_impl PUBLIC caliper)
target_compile_features(__pycaliper_impl PUBLIC cxx_std_11)
target_include_directories(__pycaliper_impl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

add_custom_target(
pycaliper_test ALL # Always build pycaliper_test
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/pycaliper
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/pycaliper ${CMAKE_CURRENT_BINARY_DIR}/pycaliper
COMMENT "Copying pycaliper Python source to ${CMAKE_CURRENT_BINARY_DIR}/pycaliper"
)
add_dependencies(__pycaliper_impl pycaliper_test)

if (BUILD_TESTING)
set(PYPATH_TESTING ${CMAKE_CURRENT_BINARY_DIR} CACHE INTERNAL "")
add_custom_target(
pycaliper_symlink_lib_in_build ALL
COMMAND ${CMAKE_COMMAND} -E create_symlink
$<TARGET_FILE:__pycaliper_impl>
${CMAKE_CURRENT_BINARY_DIR}/pycaliper/$<TARGET_FILE_NAME:__pycaliper_impl>
COMMENT "Creating symlink between Python C module and build directory for testing"
DEPENDS __pycaliper_impl
)
message(STATUS "Will add ${PYPATH_TESTING} to PYTHONPATH during test")
endif()

install(
DIRECTORY
pycaliper/
DESTINATION
${PYCALIPER_SITELIB}
)

install(
TARGETS
__pycaliper_impl
ARCHIVE DESTINATION
${PYCALIPER_SITEARCH}
LIBRARY DESTINATION
${PYCALIPER_SITEARCH}
)
Loading

0 comments on commit 4029342

Please sign in to comment.