diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 1cd1d3bfa..d96ef6269 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7806d93d1..661a1f409 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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) @@ -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) diff --git a/cmake/get_python_install_paths.py b/cmake/get_python_install_paths.py new file mode 100644 index 000000000..eb4823207 --- /dev/null +++ b/cmake/get_python_install_paths.py @@ -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 " + ) + +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="") diff --git a/cmake/hostconfig/github-actions.cmake b/cmake/hostconfig/github-actions.cmake index 7220a4763..cc5c035b0 100644 --- a/cmake/hostconfig/github-actions.cmake +++ b/cmake/hostconfig/github-actions.cmake @@ -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 "") diff --git a/examples/apps/cali-perfproblem-branch-mispred.py b/examples/apps/cali-perfproblem-branch-mispred.py new file mode 100644 index 000000000..3d2626e69 --- /dev/null +++ b/examples/apps/cali-perfproblem-branch-mispred.py @@ -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() + diff --git a/examples/apps/py-example.py b/examples/apps/py-example.py new file mode 100644 index 000000000..690c7ca43 --- /dev/null +++ b/examples/apps/py-example.py @@ -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() + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac40c0dc4..1cff50221 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,6 +58,10 @@ if (WITH_TOOLS) add_subdirectory(tools) endif() +if (WITH_PYTHON_BINDINGS) + add_subdirectory(interface/python) +endif() + install( TARGETS caliper diff --git a/src/interface/python/CMakeLists.txt b/src/interface/python/CMakeLists.txt new file mode 100644 index 000000000..6549d5cb0 --- /dev/null +++ b/src/interface/python/CMakeLists.txt @@ -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 + $ + ${CMAKE_CURRENT_BINARY_DIR}/pycaliper/$ + 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} +) \ No newline at end of file diff --git a/src/interface/python/annotation.cpp b/src/interface/python/annotation.cpp new file mode 100644 index 000000000..04f620528 --- /dev/null +++ b/src/interface/python/annotation.cpp @@ -0,0 +1,117 @@ +#include "annotation.h" + +namespace cali { + +PythonAnnotation::PythonAnnotation(const char *name) + : cali::Annotation(name, CALI_ATTR_DEFAULT) {} + +PythonAnnotation::PythonAnnotation(const char *name, cali_attr_properties opt) + : cali::Annotation(name, opt) {} + +PythonAnnotation &PythonAnnotation::begin() { + cali::Annotation::begin(); + return *this; +} + +PythonAnnotation &PythonAnnotation::begin(int data) { + cali::Annotation::begin(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::begin(double data) { + cali::Annotation::begin(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::begin(const char *data) { + cali::Annotation::begin(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::begin(cali_attr_type type, + const std::string &data) { + cali::Annotation::begin(type, data.data(), data.size()); + return *this; +} + +PythonAnnotation &PythonAnnotation::set(int data) { + cali::Annotation::set(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::set(double data) { + cali::Annotation::set(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::set(const char *data) { + cali::Annotation::set(data); + return *this; +} + +PythonAnnotation &PythonAnnotation::set(cali_attr_type type, + const std::string &data) { + cali::Annotation::set(type, data.data(), data.size()); + return *this; +} + +void create_caliper_annotation_mod(py::module_ &caliper_annotation_mod) { + py::class_ annotation_type(caliper_annotation_mod, + "Annotation"); + annotation_type.def(py::init(), + "Creates an annotation object to manipulate the context " + "attribute with the given name.", + py::arg()); + annotation_type.def(py::init(), + "Creates an annotation object to manipulate the context " + "attribute with the given name", + py::arg(), py::arg("opt")); + annotation_type.def( + "end", &PythonAnnotation::end, + "Close the top-most open region for the associated context attribute."); + annotation_type.def("begin", + static_cast( + &PythonAnnotation::begin), + "Begin the region for the associated context attribute"); + annotation_type.def("begin", + static_cast( + &PythonAnnotation::begin), + "Begin the region for the associated context attribute " + "with an integer value"); + annotation_type.def( + "begin", + static_cast( + &PythonAnnotation::begin), + "Begin the region for the associated context attribute " + "with a str/bytes value"); + annotation_type.def( + "begin", + static_cast( + &PythonAnnotation::begin), + "Begin the region for the associated context attribute " + "with a str/bytes value"); + annotation_type.def("set", + static_cast( + &PythonAnnotation::set), + "Exports an entry for the associated context attribute " + "with an integer value. The top-most prior open value " + "for the attribute, if any, will be overwritten."); + annotation_type.def( + "set", + static_cast( + &PythonAnnotation::set), + "Exports an entry for the associated context attribute " + "with a str/bytes value. The top-most prior open value " + "for the attribute, if any, will be overwritten."); + annotation_type.def( + "set", + static_cast( + &PythonAnnotation::set), + "Exports an entry for the associated context attribute " + "with a str/bytes value. The top-most prior open value " + "for the attribute, if any, will be overwritten."); +} + +} // namespace cali \ No newline at end of file diff --git a/src/interface/python/annotation.h b/src/interface/python/annotation.h new file mode 100644 index 000000000..9e1e09557 --- /dev/null +++ b/src/interface/python/annotation.h @@ -0,0 +1,37 @@ +#ifndef CALI_INTERFACE_PYTHON_ANNOTATION_H +#define CALI_INTERFACE_PYTHON_ANNOTATION_H + +#include "common.h" + +namespace cali { + +class PythonAnnotation : public cali::Annotation { +public: + PythonAnnotation(const char *name); + + PythonAnnotation(const char *name, cali_attr_properties opt); + + PythonAnnotation &begin(); + + PythonAnnotation &begin(int data); + + PythonAnnotation &begin(double data); + + PythonAnnotation &begin(const char *data); + + PythonAnnotation &begin(cali_attr_type type, const std::string &data); + + PythonAnnotation &set(int data); + + PythonAnnotation &set(double data); + + PythonAnnotation &set(const char *data); + + PythonAnnotation &set(cali_attr_type type, const std::string &data); +}; + +void create_caliper_annotation_mod(py::module_ &caliper_annotation_mod); + +} // namespace cali + +#endif /* CALI_INTERFACE_PYTHON_ANNOTATION_H */ \ No newline at end of file diff --git a/src/interface/python/common.h b/src/interface/python/common.h new file mode 100644 index 000000000..dfcebd539 --- /dev/null +++ b/src/interface/python/common.h @@ -0,0 +1,12 @@ +#ifndef CALI_INTERFACE_PYTHON_COMMON_HPP +#define CALI_INTERFACE_PYTHON_COMMON_HPP + +#include + +#include + +#include + +namespace py = pybind11; + +#endif /* CALI_INTERFACE_PYTHON_COMMON_HPP */ \ No newline at end of file diff --git a/src/interface/python/config_manager.cpp b/src/interface/python/config_manager.cpp new file mode 100644 index 000000000..ad4f8a5c3 --- /dev/null +++ b/src/interface/python/config_manager.cpp @@ -0,0 +1,141 @@ +#include "config_manager.h" +#include + +namespace cali { + +PythonConfigManager::PythonConfigManager() : cali::ConfigManager() {} + +PythonConfigManager::PythonConfigManager(const char *config_str) + : cali::ConfigManager(config_str) {} + +void PythonConfigManager::add_config_spec(py::dict json) { + add_config_spec(py::str(json)); +} + +void PythonConfigManager::add_option_spec(py::dict json) { + add_option_spec(py::str(json)); +} + +void PythonConfigManager::py_add(const char *config_string) { + cali::ConfigManager::add(config_string); +} + +void PythonConfigManager::check(const char *config_str) { + std::string err_msg = cali::ConfigManager::check(config_str); + if (err_msg.size() != 0) { + throw std::runtime_error(err_msg); + } +} + +void create_caliper_config_manager_mod( + py::module_ &caliper_config_manager_mod) { + py::class_ config_mgr_type(caliper_config_manager_mod, + "ConfigManager"); + config_mgr_type.def(py::init<>(), "Create a ConfigManager."); + config_mgr_type.def( + py::init(), + "Create a ConfigManager with the provide configuration string."); + config_mgr_type.def( + "add_config_spec", + static_cast( + &cali::ConfigManager::add_config_spec), + "Add a custom config spec to this ConfigManager." + "" + "Adds a new Caliper configuration spec for this ConfigManager" + "using a custom ChannelController or option checking function."); + config_mgr_type.def( + "add_config_spec", + static_cast( + &PythonConfigManager::add_config_spec), + "Add a JSON config spec to this ConfigManager" + "" + "Adds a new Caliper configuration specification for this ConfigManager" + "using a basic ChannelController." + "" + "See the C++ docs for more details"); + config_mgr_type.def( + "add_option_spec", + static_cast( + &cali::ConfigManager::add_option_spec), + "Add a JSON option spec to this ConfigManager" + "" + "Allows one to define options for any config in a matching category." + "Option specifications must be added before querying or creating any" + "configurations to be effective." + "" + "See the C++ docs for more details"); + config_mgr_type.def( + "add_option_spec", + static_cast( + &PythonConfigManager::add_option_spec), + "Add a JSON option spec to this ConfigManager" + "" + "Allows one to define options for any config in a matching category." + "Option specifications must be added before querying or creating any" + "configurations to be effective." + "" + "See the C++ docs for more details"); + config_mgr_type.def("add", &PythonConfigManager::py_add, + "Parse the provided configuration string and create the " + "specified configuration channels."); + config_mgr_type.def( + "load", + static_cast( + &cali::ConfigManager::load), + "Load config and option specs from the provided filename."); + config_mgr_type.def( + "set_default_parameter", + static_cast( + &cali::ConfigManager::set_default_parameter), + "Pre-set a key-value pair for all configurations."); + config_mgr_type.def( + "set_default_parameter_for_config", + static_cast( + &cali::ConfigManager::set_default_parameter_for_config), + "Pre-set a key-value pair for the specified configuration."); + config_mgr_type.def( + "error", + static_cast( + &cali::ConfigManager::error), + "Returns true if there was an error while parsing configuration."); + config_mgr_type.def("error_msg", + static_cast( + &cali::ConfigManager::error_msg), + "Returns an error message if there was an error while " + "parsing configuration."); + config_mgr_type.def("__repr__", + static_cast( + &cali::ConfigManager::error_msg)); + config_mgr_type.def( + "start", + static_cast(&cali::ConfigManager::start), + "Start all configured measurement channels, or re-start paused ones."); + config_mgr_type.def( + "stop", + static_cast(&cali::ConfigManager::stop), + "Pause all configured measurement channels."); + config_mgr_type.def( + "flush", + static_cast(&cali::ConfigManager::flush), + "Flush all configured measurement channels."); + config_mgr_type.def("check", &PythonConfigManager::check, + "Check if the given config string is valid."); + config_mgr_type.def( + "available_config_specs", + static_cast (PythonConfigManager::*)() const>( + &cali::ConfigManager::available_config_specs), + "Return names of available config specs."); + config_mgr_type.def( + "get_documentation_for_spec", + static_cast( + &cali::ConfigManager::get_documentation_for_spec), + "Return short description for the given config spec."); + config_mgr_type.def_static( + "get_config_docstrings", + static_cast (*)()>( + &cali::ConfigManager::get_config_docstrings), + "Return descriptions for available global configs."); +} + +} // namespace cali \ No newline at end of file diff --git a/src/interface/python/config_manager.h b/src/interface/python/config_manager.h new file mode 100644 index 000000000..9f18ee7da --- /dev/null +++ b/src/interface/python/config_manager.h @@ -0,0 +1,56 @@ +#ifndef CALI_INTERFACE_PYTHON_CONFIG_MANAGER_H +#define CALI_INTERFACE_PYTHON_CONFIG_MANAGER_H + +#include "common.h" + +#include + +namespace cali { +class PythonConfigManager : public cali::ConfigManager { +public: + PythonConfigManager(); + + PythonConfigManager(const char *config_str); + + // void add_config_spec(const char *json); + + void add_config_spec(py::dict json); + + // void add_option_spec(const char *json); + + void add_option_spec(py::dict json); + + void py_add(const char *config_string); + + // void load(const char *filename); + + // void set_default_parameter(const char *key, const char *value); + + // void set_default_parameter_for_config(const char *config, const char *key, + // const char *value); + + // bool error() const; + + // Set to __repr__ + // bool error_msg() const; + + // void start(); + + // void stop(); + + // void flush(); + + void check(const char *config_str); + + // std::vector available_config_specs() const; + + // std::string get_documentation_for_spec(const char *name) const; + + // static std::vector get_config_docstrings(); +}; + +void create_caliper_config_manager_mod(py::module_ &caliper_config_manager_mod); + +} // namespace cali + +#endif /* CALI_INTERFACE_PYTHON_CONFIG_MANAGER_H */ \ No newline at end of file diff --git a/src/interface/python/instrumentation.cpp b/src/interface/python/instrumentation.cpp new file mode 100644 index 000000000..ecfdc64ad --- /dev/null +++ b/src/interface/python/instrumentation.cpp @@ -0,0 +1,197 @@ +#include "instrumentation.h" + +#include + +namespace cali { + +PythonAttribute::PythonAttribute(const char *name, cali_attr_type type) + : m_attr_id(cali_create_attribute(name, type, CALI_ATTR_DEFAULT)) { + if (m_attr_id == CALI_INV_ID) { + throw std::runtime_error("Failed to create attribute"); + } +} + +PythonAttribute::PythonAttribute(const char *name, cali_attr_type type, + cali_attr_properties opt) + : m_attr_id(cali_create_attribute(name, type, opt)) { + if (m_attr_id == CALI_INV_ID) { + throw std::runtime_error("Failed to create attribute"); + } +} + +PythonAttribute::PythonAttribute(cali_id_t id) { + if (id == CALI_INV_ID) { + throw std::runtime_error("Invalid attribute"); + } + m_attr_id = id; +} + +PythonAttribute PythonAttribute::find_attribute(const char *name) { + PythonAttribute found_attr{cali_find_attribute(name)}; + return found_attr; +} + +const char *PythonAttribute::name() const { + return cali_attribute_name(m_attr_id); +} + +cali_attr_type PythonAttribute::type() const { + return cali_attribute_type(m_attr_id); +} + +cali_attr_properties PythonAttribute::properties() const { + return static_cast( + cali_attribute_properties(m_attr_id)); +} + +void PythonAttribute::begin() { cali_begin(m_attr_id); } + +void PythonAttribute::begin(int val) { cali_begin_int(m_attr_id, val); } + +void PythonAttribute::begin(double val) { cali_begin_double(m_attr_id, val); } + +void PythonAttribute::begin(const char *val) { + cali_begin_string(m_attr_id, val); +} + +void PythonAttribute::set(int val) { cali_set_int(m_attr_id, val); } + +void PythonAttribute::set(double val) { cali_set_double(m_attr_id, val); } + +void PythonAttribute::set(const char *val) { cali_set_string(m_attr_id, val); } + +void PythonAttribute::end() { cali_end(m_attr_id); } + +void create_caliper_instrumentation_mod( + py::module_ &caliper_instrumentation_mod) { + // PythonAttribute bindings + py::class_ cali_attribute_type(caliper_instrumentation_mod, + "Attribute"); + cali_attribute_type.def(py::init(), + "Create Caliper Attribute with name and type.", + py::arg(), py::arg()); + cali_attribute_type.def( + py::init(), + "Create Caliper Attribute with name, type, and properties.", py::arg(), + py::arg(), py::arg("opt")); + cali_attribute_type.def_static("find_attribute", + &PythonAttribute::find_attribute, + "Get Caliper Attribute by name."); + cali_attribute_type.def_property_readonly("name", &PythonAttribute::name, + "Name of the Caliper Attribute."); + cali_attribute_type.def_property_readonly("type", &PythonAttribute::type, + "Type of the Caliper Attribute."); + cali_attribute_type.def_property_readonly( + "properties", &PythonAttribute::properties, + "Properties of the Caliper Attribute."); + cali_attribute_type.def( + "begin", + static_cast(&PythonAttribute::begin), + "Begin region where the value for the Attribute is 'true' on the " + "blackboard."); + cali_attribute_type.def( + "begin", + static_cast(&PythonAttribute::begin), + "Begin integer region for attribute on the blackboard."); + cali_attribute_type.def( + "begin", + static_cast(&PythonAttribute::begin), + "Begin float region for attribute on the blackboard."); + cali_attribute_type.def( + "begin", + static_cast( + &PythonAttribute::begin), + "Begin str/bytes region for attribute on the blackboard."); + cali_attribute_type.def( + "set", static_cast(&PythonAttribute::set), + "Set integer value for attribute on the blackboard."); + cali_attribute_type.def( + "set", + static_cast(&PythonAttribute::set), + "Set double value for attribute on the blackboard."); + cali_attribute_type.def( + "set", + static_cast( + &PythonAttribute::set), + "Set str/bytes value for attribute on the blackboard."); + cali_attribute_type.def( + "end", &PythonAttribute::end, + "End innermost open region for attribute on the blackboard."); + + // Bindings for region begin/end functions + caliper_instrumentation_mod.def( + "begin_region", &cali_begin_region, + "Begin nested region by name." + "" + "Begins nested region using the built-in annotation attribute."); + caliper_instrumentation_mod.def( + "end_region", &cali_end_region, + "End nested region by name." + "" + "Ends nested region using built-in annotation attribute." + "Prints an error if the name does not match the currently open region."); + caliper_instrumentation_mod.def( + "begin_phase", &cali_begin_phase, + "Begin phase region by name." + "" + "A phase marks high-level, long(er)-running code regions. While regular " + "regions" + "use the \"region\" attribute with annotation level 0, phase regions use " + "the" + "\"phase\" attribute with annotation level 4. Otherwise, phases behave" + "identical to regular Caliper regions."); + caliper_instrumentation_mod.def("end_phase", &cali_end_phase, + "End phase region by name."); + caliper_instrumentation_mod.def( + "begin_comm_region", &cali_begin_comm_region, + "Begin communication region by name." + "" + "A communication region can be used to mark communication operations" + "(e.g., MPI calls) that belong to a single communication pattern." + "They can be used to summarize communication pattern statistics." + "Otherwise, they behave identical to regular Caliper regions."); + caliper_instrumentation_mod.def("end_comm_region", &cali_end_comm_region, + "End communication region by name."); + + // Bindings for "_byname" functions + caliper_instrumentation_mod.def( + "begin_byname", &cali_begin_byname, + "Same as Annotation.begin, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "begin_byname", &cali_begin_double_byname, + "Same as Annotation.begin, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "begin_byname", &cali_begin_int_byname, + "Same as Annotation.begin, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "begin_byname", &cali_begin_string_byname, + "Same as Annotation.begin, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "set_byname", &cali_set_double_byname, + "Same as Annotation.set, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "set_byname", &cali_set_int_byname, + "Same as Annotation.set, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "set_byname", &cali_set_string_byname, + "Same as Annotation.set, but refers to annotation by name."); + caliper_instrumentation_mod.def( + "end_byname", &cali_end_byname, + "Same as Annotation.end, but refers to annotation by name."); + + // Bindings for global "_byname" functions + caliper_instrumentation_mod.def( + "set_global_byname", &cali_set_global_double_byname, + "Set a global attribute with a given name to the given value."); + caliper_instrumentation_mod.def( + "set_global_byname", &cali_set_global_int_byname, + "Set a global attribute with a given name to the given value."); + caliper_instrumentation_mod.def( + "set_global_byname", &cali_set_global_string_byname, + "Set a global attribute with a given name to the given value."); + caliper_instrumentation_mod.def( + "set_global_byname", &cali_set_global_uint_byname, + "Set a global attribute with a given name to the given value."); +} + +} // namespace cali \ No newline at end of file diff --git a/src/interface/python/instrumentation.h b/src/interface/python/instrumentation.h new file mode 100644 index 000000000..461f1cdbe --- /dev/null +++ b/src/interface/python/instrumentation.h @@ -0,0 +1,52 @@ +#ifndef CALI_INTERFACE_PYTHON_INSTRUMENTATION_H +#define CALI_INTERFACE_PYTHON_INSTRUMENTATION_H + +#include "common.h" + +namespace cali { + +class PythonAttribute { +public: + PythonAttribute(const char *name, cali_attr_type type); + + PythonAttribute(const char *name, cali_attr_type type, + cali_attr_properties opt); + + static PythonAttribute find_attribute(const char *name); + + const char *name() const; + + cali_attr_type type() const; + + cali_attr_properties properties() const; + + void begin(); + + void begin(int val); + + void begin(double val); + + void begin(const char *val); + + void set(int val); + + void set(double val); + + void set(const char *val); + + void end(); + +private: + PythonAttribute(cali_id_t id); + + cali_id_t m_attr_id; +}; + +// TODO add "byname" functions to module + +void create_caliper_instrumentation_mod( + py::module_ &caliper_instrumentation_mod); + +} // namespace cali + +#endif /* CALI_INTERFACE_PYTHON_INSTRUMENTATION_H */ \ No newline at end of file diff --git a/src/interface/python/loop.cpp b/src/interface/python/loop.cpp new file mode 100644 index 000000000..c86c74b34 --- /dev/null +++ b/src/interface/python/loop.cpp @@ -0,0 +1,29 @@ +#include "loop.h" + +namespace cali { + +PythonLoop::PythonLoop(const char *name) { + if (cali_loop_attr_id == CALI_INV_ID) { + cali_init(); + } + cali_begin_string(cali_loop_attr_id, name); + m_iter_attr = cali_make_loop_iteration_attribute(name); +} + +void PythonLoop::start_iteration(int i) { cali_begin_int(m_iter_attr, i); } + +void PythonLoop::end_iteration() { cali_end(m_iter_attr); } + +void PythonLoop::end() { cali_end(cali_loop_attr_id); } + +void create_caliper_loop_mod(py::module_ &caliper_loop_mod) { + py::class_ loop_type(caliper_loop_mod, "Loop"); + loop_type.def(py::init(), "Create a loop annotation."); + loop_type.def("start_iteration", &PythonLoop::start_iteration, + "Start a loop iteration."); + loop_type.def("end_iteration", &PythonLoop::end_iteration, + "End a loop iteration."); + loop_type.def("end", &PythonLoop::end, "End the loop annotation."); +} + +} // namespace cali \ No newline at end of file diff --git a/src/interface/python/loop.h b/src/interface/python/loop.h new file mode 100644 index 000000000..eb483c073 --- /dev/null +++ b/src/interface/python/loop.h @@ -0,0 +1,26 @@ +#ifndef CALI_INTERFACE_PYTHON_LOOP_H +#define CALI_INTERFACE_PYTHON_LOOP_H + +#include "common.h" + +namespace cali { + +class PythonLoop { +public: + PythonLoop(const char *name); + + void start_iteration(int i); + + void end_iteration(); + + void end(); + +private: + cali_id_t m_iter_attr; +}; + +void create_caliper_loop_mod(py::module_ &caliper_loop_mod); + +} // namespace cali + +#endif /* CALI_INTERFACE_PYTHON_LOOP_H */ \ No newline at end of file diff --git a/src/interface/python/mod.cpp b/src/interface/python/mod.cpp new file mode 100644 index 000000000..83f53e0f6 --- /dev/null +++ b/src/interface/python/mod.cpp @@ -0,0 +1,86 @@ +#include "annotation.h" +#include "config_manager.h" +#include "instrumentation.h" +#include "loop.h" + +bool pycaliper_is_initialized() { return cali_is_initialized() != 0; } + +PYBIND11_MODULE(__pycaliper_impl, m) { + m.attr("__version__") = cali_caliper_version(); + + m.def( + "config_preset", + [](std::map &preset_map) { + for (auto kv : preset_map) { + cali_config_preset(kv.first, kv.second); + } + }, + "Pre-set a config entry in the default config." + "The entry can still be overwritten by environment variables."); + m.def("init", &cali_init, + "Initialize Caliper." + "Typically, it is not necessary to initialize Caliper explicitly." + "Caliper will lazily initialize itself on the first Caliper API call." + "This function is used primarily by the Caliper annotation macros," + "to ensure that Caliper's pre-defined annotation attributes are" + "initialized." + "It can also be used to avoid high initialization costs in the first" + "Caliper API call."); + m.def("is_initialized", &pycaliper_is_initialized, + "Check if Caliper is initialized on this process."); + + auto types_mod = m.def_submodule( + "types", "Special types used by lower-level Caliper APIs."); + + py::enum_ c_attr_type(types_mod, "AttrType"); + c_attr_type.value("CALI_TYPE_INV", CALI_TYPE_INV); + c_attr_type.value("CALI_TYPE_USR", CALI_TYPE_USR); + c_attr_type.value("CALI_TYPE_INT", CALI_TYPE_INT); + c_attr_type.value("CALI_TYPE_UINT", CALI_TYPE_UINT); + c_attr_type.value("CALI_TYPE_STRING", CALI_TYPE_STRING); + c_attr_type.value("CALI_TYPE_ADDR", CALI_TYPE_ADDR); + c_attr_type.value("CALI_TYPE_DOUBLE", CALI_TYPE_DOUBLE); + c_attr_type.value("CALI_TYPE_BOOL", CALI_TYPE_BOOL); + c_attr_type.value("CALI_TYPE_TYPE", CALI_TYPE_TYPE); + c_attr_type.value("CALI_TYPE_PTR", CALI_TYPE_PTR); + c_attr_type.export_values(); + + py::enum_ c_attr_properties(types_mod, "AttrProperties", + py::arithmetic()); + c_attr_properties.value("CALI_ATTR_DEFAULT", CALI_ATTR_DEFAULT); + c_attr_properties.value("CALI_ATTR_ASVALUE", CALI_ATTR_ASVALUE); + c_attr_properties.value("CALI_ATTR_NOMERGE", CALI_ATTR_NOMERGE); + c_attr_properties.value("CALI_ATTR_SCOPE_PROCESS", CALI_ATTR_SCOPE_PROCESS); + c_attr_properties.value("CALI_ATTR_SCOPE_THREAD", CALI_ATTR_SCOPE_THREAD); + c_attr_properties.value("CALI_ATTR_SCOPE_TASK", CALI_ATTR_SCOPE_TASK); + c_attr_properties.value("CALI_ATTR_SKIP_EVENTS", CALI_ATTR_SKIP_EVENTS); + c_attr_properties.value("CALI_ATTR_HIDDEN", CALI_ATTR_HIDDEN); + c_attr_properties.value("CALI_ATTR_NESTED", CALI_ATTR_NESTED); + c_attr_properties.value("CALI_ATTR_GLOBAL", CALI_ATTR_GLOBAL); + c_attr_properties.value("CALI_ATTR_UNALIGNED", CALI_ATTR_UNALIGNED); + c_attr_properties.value("CALI_ATTR_AGGREGATABLE", CALI_ATTR_AGGREGATABLE); + c_attr_properties.value("CALI_ATTR_LEVEL_1", CALI_ATTR_LEVEL_1); + c_attr_properties.value("CALI_ATTR_LEVEL_2", CALI_ATTR_LEVEL_2); + c_attr_properties.value("CALI_ATTR_LEVEL_3", CALI_ATTR_LEVEL_3); + c_attr_properties.value("CALI_ATTR_LEVEL_4", CALI_ATTR_LEVEL_4); + c_attr_properties.value("CALI_ATTR_LEVEL_5", CALI_ATTR_LEVEL_5); + c_attr_properties.value("CALI_ATTR_LEVEL_6", CALI_ATTR_LEVEL_6); + c_attr_properties.value("CALI_ATTR_LEVEL_7", CALI_ATTR_LEVEL_7); + c_attr_properties.export_values(); + + auto annotation_mod = + m.def_submodule("annotation", "Support for Caliper annotation APIs."); + cali::create_caliper_annotation_mod(annotation_mod); + + auto instrumentation_mod = + m.def_submodule("instrumentation", + "Support for higher-level Caliper instrumentation APIs"); + cali::create_caliper_instrumentation_mod(instrumentation_mod); + + auto loop_mod = m.def_submodule("loop", "Support for loop annotations."); + cali::create_caliper_loop_mod(loop_mod); + + auto config_mgr_mod = m.def_submodule( + "config_manager", "Support for dynamic configuration of Caliper."); + cali::create_caliper_config_manager_mod(config_mgr_mod); +} \ No newline at end of file diff --git a/src/interface/python/pycaliper/__init__.py b/src/interface/python/pycaliper/__init__.py new file mode 100644 index 000000000..2a4ed0a8e --- /dev/null +++ b/src/interface/python/pycaliper/__init__.py @@ -0,0 +1,15 @@ +from pycaliper.__pycaliper_impl import ( + __version__, + config_preset, + init, + is_initialized, +) + +import pycaliper.annotation +import pycaliper.config_manager +import pycaliper.high_level +import pycaliper.instrumentation +import pycaliper.loop +import pycaliper.types + +from pycaliper.high_level import annotate_function diff --git a/src/interface/python/pycaliper/annotation.py b/src/interface/python/pycaliper/annotation.py new file mode 100644 index 000000000..0b1f95104 --- /dev/null +++ b/src/interface/python/pycaliper/annotation.py @@ -0,0 +1 @@ +from pycaliper.__pycaliper_impl.annotation import * diff --git a/src/interface/python/pycaliper/config_manager.py b/src/interface/python/pycaliper/config_manager.py new file mode 100644 index 000000000..c33a9a5cc --- /dev/null +++ b/src/interface/python/pycaliper/config_manager.py @@ -0,0 +1 @@ +from pycaliper.__pycaliper_impl.config_manager import * diff --git a/src/interface/python/pycaliper/high_level.py b/src/interface/python/pycaliper/high_level.py new file mode 100644 index 000000000..0e5d35ca7 --- /dev/null +++ b/src/interface/python/pycaliper/high_level.py @@ -0,0 +1,53 @@ +from pycaliper.instrumentation import ( + begin_region, + end_region, + begin_phase, + end_phase, + begin_comm_region, + end_comm_region, +) + +import functools + + +__annotation_decorator_begin_map = { + "region": begin_region, + "phase": begin_phase, + "comm_region": begin_comm_region, +} + + +__annotation_decorator_end_map = { + "region": end_region, + "phase": end_phase, + "comm_region": end_comm_region, +} + + +def annotate_function(name=None, annotation_type="region"): + """Decorator that automatically starts and ends a region around the decorated function. + + :param name: If provided, use as the name of the created Caliper region. + If None, the name will be derived from the name of the decorated function. + :type name: str + :param annotation_type: The type of annotation to use. Can be one of "region" (uses `begin|end_region`), + "phase" (uses `begin|end_phase`), or "comm_region` (uses `begin|end_comm_region`). + """ + + def inner_decorator(func): + real_name = name + if name is None or name == "": + real_name = func.__name__ + if annotation_type not in list(__annotation_decorator_begin_map.keys()): + raise ValueError("Invalid annotation type {}".format(annotation_type)) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + __annotation_decorator_begin_map[annotation_type](real_name) + result = func(*args, **kwargs) + __annotation_decorator_end_map[annotation_type](real_name) + return result + + return wrapper + + return inner_decorator diff --git a/src/interface/python/pycaliper/instrumentation.py b/src/interface/python/pycaliper/instrumentation.py new file mode 100644 index 000000000..46046ef57 --- /dev/null +++ b/src/interface/python/pycaliper/instrumentation.py @@ -0,0 +1 @@ +from pycaliper.__pycaliper_impl.instrumentation import * diff --git a/src/interface/python/pycaliper/loop.py b/src/interface/python/pycaliper/loop.py new file mode 100644 index 000000000..7ee217d3a --- /dev/null +++ b/src/interface/python/pycaliper/loop.py @@ -0,0 +1 @@ +from pycaliper.__pycaliper_impl.loop import * diff --git a/src/interface/python/pycaliper/types.py b/src/interface/python/pycaliper/types.py new file mode 100644 index 000000000..64b836fd6 --- /dev/null +++ b/src/interface/python/pycaliper/types.py @@ -0,0 +1 @@ +from pycaliper.__pycaliper_impl.types import * diff --git a/test/ci_app_tests/CMakeLists.txt b/test/ci_app_tests/CMakeLists.txt index d9d99571b..2f2f5de3d 100644 --- a/test/ci_app_tests/CMakeLists.txt +++ b/test/ci_app_tests/CMakeLists.txt @@ -24,6 +24,9 @@ set(CALIPER_CI_MPI_TEST_APPS ci_test_mpi_channel_manager) set(CALIPER_CI_Fortran_TEST_APPS ci_test_f_ann) +set(CALIPER_CI_Python_TEST_APPS + ci_test_py_ann.py +) foreach(app ${CALIPER_CI_CXX_TEST_APPS}) add_executable(${app} ${app}.cpp) @@ -130,6 +133,22 @@ if (WITH_FORTRAN) list(APPEND PYTHON_SCRIPTS test_fortran_api.py) endif() +if (WITH_PYTHON_BINDINGS) + foreach(file ${CALIPER_CI_Python_TEST_APPS}) + # add_custom_target(${file} ALL + # COMMAND ${CMAKE_COMMAND} -E create_symlink + # ${CMAKE_CURRENT_SOURCE_DIR}/${file} + # ${CMAKE_CURRENT_BINARY_DIR}/${file}) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/${file} + ${CMAKE_CURRENT_BINARY_DIR}/${file} + @ONLY + ) + endforeach() + + list(APPEND PYTHON_SCRIPTS test_python_api.py) +endif() + set(DATA_FILES example_node_info.json) @@ -141,3 +160,7 @@ foreach(file ${PYTHON_SCRIPTS} ${DATA_FILES}) endforeach() add_test(NAME CI_app_tests COMMAND ${CALI_PYTHON_EXECUTABLE} -B -m unittest discover -p "test_*.py") +# if (WITH_PYTHON_BINDINGS) +# message(STATUS "Adding ${PYPATH_TESTING} to PYTHONPATH for CI_app_tests") +# set_tests_properties(CI_app_tests PROPERTIES ENVIRONMENT "PYTHONPATH=${PYPATH_TESTING}") +# endif() diff --git a/test/ci_app_tests/ci_test_py_ann.py b/test/ci_app_tests/ci_test_py_ann.py new file mode 100644 index 000000000..4cbe27ad9 --- /dev/null +++ b/test/ci_app_tests/ci_test_py_ann.py @@ -0,0 +1,59 @@ +# --- Caliper continuous integration test app for Python annotation interface + +import sys + +sys.path.insert(0, "@PYPATH_TESTING@") + +from pycaliper import config_preset +from pycaliper.instrumentation import ( + Attribute, + set_global_byname, + begin_byname, + set_byname, + end_byname, +) +from pycaliper.types import CALI_TYPE_INT, CALI_ATTR_ASVALUE +from pycaliper.config_manager import ConfigManager + + +def main(): + config_preset({"CALI_CHANNEL_FLUSH_ON_EXIT": "false"}) + + mgr = ConfigManager() + if len(sys.argv) > 1: + mgr.add(sys.argv[1]) + + if mgr.error(): + print("Caliper config error:", mgr.err_msg(), file=sys.stderr) + exit(-1) + + mgr.start() + + set_global_byname("global.double", 42.42) + set_global_byname("global.int", 1337) + set_global_byname("global.string", "my global string") + set_global_byname("global.uint", 42) + + iter_attr = Attribute("iteration", CALI_TYPE_INT, CALI_ATTR_ASVALUE) + + begin_byname("phase", "loop") + + for i in range(4): + iter_attr.begin(i) + iter_attr.end() + + end_byname("phase") + + begin_byname("ci_test_c_ann.setbyname") + + set_byname("attr.int", 20) + set_byname("attr.dbl", 1.25) + set_byname("attr.str", "fidibus") + + end_byname("ci_test_c_ann.setbyname") + + mgr.flush() + + +if __name__ == "__main__": + main() diff --git a/test/ci_app_tests/test_python_api.py b/test/ci_app_tests/test_python_api.py new file mode 100644 index 000000000..b28da0b90 --- /dev/null +++ b/test/ci_app_tests/test_python_api.py @@ -0,0 +1,96 @@ +# Tests of the Python API + +import sys + +import unittest + +import calipertest as cat + + +class CaliperPythonAPITest(unittest.TestCase): + """Caliper Python API test cases""" + + def test_py_ann_trace(self): + target_cmd = [ + sys.executable, + "./ci_test_py_ann.py", + "event-trace,output=stdout", + ] + query_cmd = ["../../src/tools/cali-query/cali-query", "-e"] + + caliper_config = {"CALI_LOG_VERBOSITY": "0"} + + query_output = cat.run_test_with_query(target_cmd, query_cmd, caliper_config) + snapshots = cat.get_snapshots_from_text(query_output) + + self.assertTrue(len(snapshots) >= 10) + + self.assertTrue( + cat.has_snapshot_with_keys( + snapshots, {"iteration", "phase", "time.duration.ns", "global.int"} + ) + ) + self.assertTrue( + cat.has_snapshot_with_attributes( + snapshots, {"event.end#phase": "loop", "phase": "loop"} + ) + ) + self.assertTrue( + cat.has_snapshot_with_attributes( + snapshots, + {"event.end#iteration": "3", "iteration": "3", "phase": "loop"}, + ) + ) + self.assertTrue( + cat.has_snapshot_with_keys( + snapshots, + {"attr.int", "attr.dbl", "attr.str", "ci_test_c_ann.setbyname"}, + ) + ) + self.assertTrue( + cat.has_snapshot_with_attributes( + snapshots, {"attr.int": "20", "attr.str": "fidibus"} + ) + ) + + def test_py_ann_globals(self): + target_cmd = [sys.executable, "./ci_test_py_ann.py"] + query_cmd = ["../../src/tools/cali-query/cali-query", "-e", "--list-globals"] + + caliper_config = { + "CALI_CONFIG_PROFILE": "serial-trace", + "CALI_RECORDER_FILENAME": "stdout", + "CALI_LOG_VERBOSITY": "0", + } + + query_output = cat.run_test_with_query(target_cmd, query_cmd, caliper_config) + snapshots = cat.get_snapshots_from_text(query_output) + + self.assertTrue(len(snapshots) == 1) + + self.assertTrue( + cat.has_snapshot_with_keys( + snapshots, + { + "global.double", + "global.string", + "global.int", + "global.uint", + "cali.caliper.version", + }, + ) + ) + self.assertTrue( + cat.has_snapshot_with_attributes( + snapshots, + { + "global.int": "1337", + "global.string": "my global string", + "global.uint": "42", + }, + ) + ) + + +if __name__ == "__main__": + unittest.main()