CMake based build for pyngraph module (#3080)

* [MO] Add CMake install for Model Optimizer

* [MO] Update test for version.py

* [MO] Add CMake install for Model Optimizer

* [MO] Update test for version.py

* [MO] Add CMake install for Model Optimizer

* [MO] Update test for version.py

* [nGraph] Python API should be compiled and installed via CMake (41857)

* Refactored wheel setup script to build module using CMake

* Update build instructions

* Added USE_SOURCE_PERMISSIONS to cmake install

* Adjust CMake compiler flags conditions

* fix CPack issue for CI build pipeline

* case insensitive option check

* build only python API if ngraph_DIR provided

* fix lib extension for macOS

* -fixed style (flake8)

 -added paralllel build option & description

* fix flake8 B006 check

* add ngraph_DIR & remove unsed env. variables.

* Reworked build & test instructions to make it more straightforward

* remove unused CMake arguments for setup.py

* make source dir condition more general

* Update BUILDING.md

* Update BUILDING.md

* Update BUILDING.md

* beautified instructions wording

* fix wheel build issue after sourcing setupvars

* Extend user options to build, install and develop commands

Co-authored-by: Andrey Zaytsev <andrey.zaytsev@intel.com>
This commit is contained in:
Sergey Lyubimtsev
2020-12-24 16:57:58 +03:00
committed by GitHub
parent 4a62491927
commit 2e6ea1e290
5 changed files with 300 additions and 495 deletions

View File

@@ -14,214 +14,29 @@
# limitations under the License.
# ******************************************************************************
import distutils.ccompiler
import os
import re
import pathlib
import shutil
import glob
import sysconfig
import sys
import multiprocessing
import setuptools
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
from setuptools.command.install_lib import install_lib
from setuptools.command.install import install as _install
from setuptools.command.develop import develop as _develop
from distutils.command.build import build as _build
__version__ = os.environ.get("NGRAPH_VERSION", "0.0.0.dev0")
PYNGRAPH_ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
PYNGRAPH_SRC_DIR = os.path.join(PYNGRAPH_ROOT_DIR, "src")
NGRAPH_DEFAULT_INSTALL_DIR = os.environ.get("HOME")
NGRAPH_PYTHON_DEBUG = os.environ.get("NGRAPH_PYTHON_DEBUG")
NGRAPH_ROOT_DIR = os.path.normpath(os.path.join(PYNGRAPH_ROOT_DIR, ".."))
OPENVINO_ROOT_DIR = os.path.normpath(os.path.join(PYNGRAPH_ROOT_DIR, "../.."))
# Change current working dircectory to ngraph/python
os.chdir(PYNGRAPH_ROOT_DIR)
debug_optimization_flags = [
"O1", "O2", "O3", "O4", "Ofast", "Os", "Oz", "Og", "O", "DNDEBUG"
]
def find_ngraph_dist_dir():
"""Return location of compiled ngraph library home."""
if os.environ.get("NGRAPH_CPP_BUILD_PATH"):
ngraph_dist_dir = os.environ.get("NGRAPH_CPP_BUILD_PATH")
else:
ngraph_dist_dir = os.path.join(NGRAPH_DEFAULT_INSTALL_DIR, "ngraph_dist")
found = os.path.exists(os.path.join(ngraph_dist_dir, "include/ngraph"))
if not found:
print(
"Cannot find nGraph library in {} make sure that "
"NGRAPH_CPP_BUILD_PATH is set correctly".format(ngraph_dist_dir)
)
sys.exit(1)
else:
print("nGraph library found in {}".format(ngraph_dist_dir))
return ngraph_dist_dir
def find_pybind_headers_dir():
"""Return location of pybind11 headers."""
if os.environ.get("PYBIND_HEADERS_PATH"):
pybind_headers_dir = os.environ.get("PYBIND_HEADERS_PATH")
else:
pybind_headers_dir = os.path.join(PYNGRAPH_ROOT_DIR, "pybind11")
found = os.path.exists(os.path.join(pybind_headers_dir, "include/pybind11"))
if not found:
print(
"Cannot find pybind11 library in {} make sure that "
"PYBIND_HEADERS_PATH is set correctly".format(pybind_headers_dir)
)
sys.exit(1)
else:
print("pybind11 library found in {}".format(pybind_headers_dir))
return pybind_headers_dir
NGRAPH_CPP_DIST_DIR = find_ngraph_dist_dir()
PYBIND11_INCLUDE_DIR = find_pybind_headers_dir() + "/include"
NGRAPH_CPP_INCLUDE_DIR = NGRAPH_CPP_DIST_DIR + "/include"
if os.path.exists(os.path.join(NGRAPH_CPP_DIST_DIR, "lib")):
NGRAPH_CPP_LIBRARY_DIR = os.path.join(NGRAPH_CPP_DIST_DIR, "lib")
elif os.path.exists(os.path.join(NGRAPH_CPP_DIST_DIR, "lib64")):
NGRAPH_CPP_LIBRARY_DIR = os.path.join(NGRAPH_CPP_DIST_DIR, "lib64")
else:
print(
"Cannot find library directory in {}, make sure that nGraph is installed "
"correctly".format(NGRAPH_CPP_DIST_DIR)
)
sys.exit(1)
if sys.platform == "win32":
NGRAPH_CPP_DIST_DIR = os.path.normpath(NGRAPH_CPP_DIST_DIR)
PYBIND11_INCLUDE_DIR = os.path.normpath(PYBIND11_INCLUDE_DIR)
NGRAPH_CPP_INCLUDE_DIR = os.path.normpath(NGRAPH_CPP_INCLUDE_DIR)
NGRAPH_CPP_LIBRARY_DIR = os.path.normpath(NGRAPH_CPP_LIBRARY_DIR)
NGRAPH_CPP_LIBRARY_NAME = "ngraph"
"""For some platforms OpenVINO adds 'd' suffix to library names in debug configuration"""
if len([fn for fn in os.listdir(NGRAPH_CPP_LIBRARY_DIR) if re.search("ngraphd", fn)]):
NGRAPH_CPP_LIBRARY_NAME = "ngraphd"
ONNX_IMPORTER_CPP_LIBRARY_NAME = "onnx_importer"
if len([fn for fn in os.listdir(NGRAPH_CPP_LIBRARY_DIR) if re.search("onnx_importerd", fn)]):
ONNX_IMPORTER_CPP_LIBRARY_NAME = "onnx_importerd"
def _remove_compiler_flags(obj):
"""Make pybind11 more verbose in debug builds."""
for flag in debug_optimization_flags:
try:
if sys.platform == "win32":
obj.compiler.compile_options.remove("/{}".format(flag))
else:
obj.compiler.compiler_so.remove("-{}".format(flag))
obj.compiler.compiler.remove("-{}".format(flag))
except (AttributeError, ValueError):
pass
def parallelCCompile(
self,
sources,
output_dir=None,
macros=None,
include_dirs=None,
debug=0,
extra_preargs=None,
extra_postargs=None,
depends=None,
):
"""Build sources in parallel.
Reference link:
http://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils
Monkey-patch for parallel compilation.
"""
# those lines are copied from distutils.ccompiler.CCompiler directly
macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
output_dir, macros, include_dirs, sources, depends, extra_postargs
)
cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
# parallel code
import multiprocessing.pool
def _single_compile(obj):
try:
src, ext = build[obj]
except KeyError:
return
self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
# convert to list, imap is evaluated on-demand
pool = multiprocessing.pool.ThreadPool()
list(pool.imap(_single_compile, objects))
return objects
distutils.ccompiler.CCompiler.compile = parallelCCompile
def has_flag(compiler, flagname):
"""Check whether a flag is supported by the specified compiler.
As of Python 3.6, CCompiler has a `has_flag` method.
cf http://bugs.python.org/issue26689
"""
import tempfile
with tempfile.NamedTemporaryFile("w", suffix=".cpp") as f:
f.write("int main (int argc, char **argv) { return 0; }")
try:
compiler.compile([f.name], extra_postargs=[flagname])
except setuptools.distutils.errors.CompileError:
return False
return True
def cpp_flag(compiler):
"""Check and return the -std=c++11 compiler flag."""
if sys.platform == "win32":
return "" # C++11 is on by default in MSVC
elif has_flag(compiler, "-std=c++11"):
return "-std=c++11"
else:
raise RuntimeError("Unsupported compiler -- C++11 support is needed!")
sources = [
"pyngraph/axis_set.cpp",
"pyngraph/axis_vector.cpp",
"pyngraph/coordinate.cpp",
"pyngraph/coordinate_diff.cpp",
"pyngraph/dict_attribute_visitor.cpp",
"pyngraph/dimension.cpp",
"pyngraph/function.cpp",
"pyngraph/node.cpp",
"pyngraph/node_input.cpp",
"pyngraph/node_output.cpp",
"pyngraph/node_factory.cpp",
"pyngraph/ops/constant.cpp",
"pyngraph/ops/parameter.cpp",
"pyngraph/ops/result.cpp",
"pyngraph/ops/util/arithmetic_reduction.cpp",
"pyngraph/ops/util/binary_elementwise_arithmetic.cpp",
"pyngraph/ops/util/binary_elementwise_comparison.cpp",
"pyngraph/ops/util/binary_elementwise_logical.cpp",
"pyngraph/ops/util/index_reduction.cpp",
"pyngraph/ops/util/op_annotations.cpp",
"pyngraph/ops/util/regmodule_pyngraph_op_util.cpp",
"pyngraph/ops/util/unary_elementwise_arithmetic.cpp",
"pyngraph/passes/manager.cpp",
"pyngraph/passes/regmodule_pyngraph_passes.cpp",
"pyngraph/partial_shape.cpp",
"pyngraph/pyngraph.cpp",
"pyngraph/shape.cpp",
"pyngraph/strides.cpp",
"pyngraph/tensor_iterator_builder.cpp",
"pyngraph/types/element_type.cpp",
"pyngraph/types/regmodule_pyngraph_types.cpp",
"pyngraph/util.cpp",
"pyngraph/variant.cpp",
"pyngraph/rt_map.cpp",
]
NGRAPH_LIBS = ["ngraph", "onnx_importer"]
packages = [
"ngraph",
@@ -237,131 +52,172 @@ packages = [
"ngraph.impl.passes",
]
sources = [PYNGRAPH_SRC_DIR + "/" + source for source in sources]
include_dirs = [PYNGRAPH_SRC_DIR, NGRAPH_CPP_INCLUDE_DIR, PYBIND11_INCLUDE_DIR]
library_dirs = [NGRAPH_CPP_LIBRARY_DIR]
libraries = [NGRAPH_CPP_LIBRARY_NAME, ONNX_IMPORTER_CPP_LIBRARY_NAME]
extra_compile_args = []
extra_link_args = []
data_files = [
(
"lib",
[
os.path.join(NGRAPH_CPP_LIBRARY_DIR, library)
for library in os.listdir(NGRAPH_CPP_LIBRARY_DIR)
if os.path.isfile(os.path.join(NGRAPH_CPP_LIBRARY_DIR, library))
],
),
]
ext_modules = [
Extension(
"_pyngraph",
sources=sources,
include_dirs=include_dirs,
define_macros=[("VERSION_INFO", __version__)],
library_dirs=library_dirs,
libraries=libraries,
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
language="c++",
),
]
def add_platform_specific_link_args(link_args):
"""Add linker flags specific for the OS detected during the build."""
if sys.platform.startswith("linux"):
link_args += ["-Wl,-rpath,$ORIGIN/../.."]
link_args += ["-z", "noexecstack"]
link_args += ["-z", "relro"]
link_args += ["-z", "now"]
elif sys.platform == "darwin":
link_args += ["-Wl,-rpath,@loader_path/../.."]
link_args += ["-stdlib=libc++"]
elif sys.platform == "win32":
link_args += ["/LTCG"]
class BuildExt(build_ext):
"""A custom build extension for adding compiler-specific options."""
def _add_extra_compile_arg(self, flag, compile_args):
"""Return True if successfully added given flag to compiler args."""
if has_flag(self.compiler, flag):
compile_args += [flag]
return True
return False
def _add_debug_or_release_flags(self):
"""Return compiler flags for Release and Debug build types."""
if NGRAPH_PYTHON_DEBUG in ["TRUE", "ON", True]:
if sys.platform == "win32":
return ["/Od", "/Zi", "/RTC1"]
else:
return ["-O0", "-g"]
else:
if sys.platform == "win32":
return ["/O2"]
else:
return ["-O2", "-D_FORTIFY_SOURCE=2"]
def _add_win_compiler_flags(self, ext):
self._add_extra_compile_arg("/GL", ext.extra_compile_args) # Whole Program Optimization
self._add_extra_compile_arg("/analyze", ext.extra_compile_args)
def _add_unix_compiler_flags(self, ext):
if not self._add_extra_compile_arg("-fstack-protector-strong", ext.extra_compile_args):
self._add_extra_compile_arg("-fstack-protector", ext.extra_compile_args)
self._add_extra_compile_arg("-fvisibility=hidden", ext.extra_compile_args)
self._add_extra_compile_arg("-flto", ext.extra_compile_args)
self._add_extra_compile_arg("-fPIC", ext.extra_compile_args)
ext.extra_compile_args += ["-Wformat", "-Wformat-security"]
def _customize_compiler_flags(self):
"""Modify standard compiler flags."""
try:
# -Wstrict-prototypes is not a valid option for c++
self.compiler.compiler_so.remove("-Wstrict-prototypes")
except (AttributeError, ValueError):
pass
def build_extensions(self):
"""Build extension providing extra compiler flags."""
self._customize_compiler_flags()
for ext in self.extensions:
ext.extra_compile_args += [cpp_flag(self.compiler)]
if sys.platform == "win32":
self._add_win_compiler_flags(ext)
else:
self._add_unix_compiler_flags(ext)
add_platform_specific_link_args(ext.extra_link_args)
ext.extra_compile_args += self._add_debug_or_release_flags()
if sys.platform == "darwin":
ext.extra_compile_args += ["-stdlib=libc++"]
if NGRAPH_PYTHON_DEBUG in ["TRUE", "ON", True]:
_remove_compiler_flags(self)
build_ext.build_extensions(self)
data_files = []
with open(os.path.join(PYNGRAPH_ROOT_DIR, "requirements.txt")) as req:
requirements = req.read().splitlines()
cmdclass = {}
for super_class in [_build, _install, _develop]:
class command(super_class):
"""Add user options for build, install and develop commands."""
cmake_build_types = ["Release", "Debug", "RelWithDebInfo", "MinSizeRel"]
user_options = super_class.user_options + [
("config=", None, "Build configuration [{}].".format("|".join(cmake_build_types))),
("jobs=", None, "Specifies the number of jobs to use with make."),
("cmake-args=", None, "Additional options to be passed to CMake.")
]
def initialize_options(self):
"""Set default values for all the options that this command supports."""
super().initialize_options()
self.config = None
self.jobs = None
self.cmake_args = None
cmdclass[super_class.__name__] = command
class CMakeExtension(Extension):
"""Build extension stub."""
def __init__(self, name, sources=None):
if sources is None:
sources = []
super().__init__(name=name, sources=sources)
class BuildCMakeExt(build_ext):
"""Builds module using cmake instead of the python setuptools implicit build."""
cmake_build_types = ["Release", "Debug", "RelWithDebInfo", "MinSizeRel"]
user_options = [
("config=", None, "Build configuration [{}].".format("|".join(cmake_build_types))),
("jobs=", None, "Specifies the number of jobs to use with make."),
("cmake-args=", None, "Additional options to be passed to CMake.")
]
def initialize_options(self):
"""Set default values for all the options that this command supports."""
super().initialize_options()
self.build_base = "build"
self.config = None
self.jobs = None
self.cmake_args = None
def finalize_options(self):
"""Set final values for all the options that this command supports."""
super().finalize_options()
for cmd in ["build", "install", "develop"]:
self.set_undefined_options(cmd, ("config", "config"),
("jobs", "jobs"),
("cmake_args", "cmake_args"))
if not self.config:
if self.debug:
self.config = "Debug"
else:
self.announce("Set default value for CMAKE_BUILD_TYPE = Release.", level=4)
self.config = "Release"
else:
build_types = [item.lower() for item in self.cmake_build_types]
try:
i = build_types.index(str(self.config).lower())
self.config = self.cmake_build_types[i]
self.debug = True if "Debug" == self.config else False
except ValueError:
self.announce("Unsupported CMAKE_BUILD_TYPE value: " + self.config, level=4)
self.announce("Supported values: {}".format(", ".join(self.cmake_build_types)), level=4)
sys.exit(1)
if self.jobs is None and os.getenv("MAX_JOBS") is not None:
self.jobs = os.getenv("MAX_JOBS")
self.jobs = multiprocessing.cpu_count() if self.jobs is None else int(self.jobs)
def run(self):
"""Run CMake build for modules."""
for extension in self.extensions:
if extension.name == "_pyngraph":
self.build_cmake(extension)
def build_cmake(self, extension: Extension):
"""Cmake configure and build steps."""
self.announce("Preparing the build environment", level=3)
plat_specifier = ".%s-%d.%d" % (self.plat_name, *sys.version_info[:2])
self.build_temp = os.path.join(self.build_base, "temp" + plat_specifier, self.config)
build_dir = pathlib.Path(self.build_temp)
extension_path = pathlib.Path(self.get_ext_fullpath(extension.name))
os.makedirs(build_dir, exist_ok=True)
os.makedirs(extension_path.parent.absolute(), exist_ok=True)
# If ngraph_DIR is not set try to build from OpenVINO root
root_dir = OPENVINO_ROOT_DIR
bin_dir = os.path.join(OPENVINO_ROOT_DIR, "bin")
if os.environ.get("ngraph_DIR") is not None:
root_dir = PYNGRAPH_ROOT_DIR
bin_dir = build_dir
self.announce("Configuring cmake project", level=3)
ext_args = self.cmake_args.split() if self.cmake_args else []
self.spawn(["cmake", "-H" + root_dir, "-B" + self.build_temp,
"-DCMAKE_BUILD_TYPE={}".format(self.config),
"-DNGRAPH_PYTHON_BUILD_ENABLE=ON",
"-DNGRAPH_ONNX_IMPORT_ENABLE=ON"] + ext_args)
self.announce("Building binaries", level=3)
self.spawn(["cmake", "--build", self.build_temp, "--target", extension.name,
"--config", self.config, "-j", str(self.jobs)])
self.announce("Moving built python module to " + str(extension_path), level=3)
pyds = list(glob.iglob("{0}/**/{1}*{2}".format(bin_dir,
extension.name,
sysconfig.get_config_var("EXT_SUFFIX")), recursive=True))
for name in pyds:
self.announce("copy " + os.path.join(name), level=3)
shutil.copy(name, extension_path)
class InstallCMakeLibs(install_lib):
"""Finds and installs NGraph libraries to a package location."""
def run(self):
"""Copy libraries from the bin directory and place them as appropriate."""
self.announce("Adding library files", level=3)
root_dir = os.path.join(OPENVINO_ROOT_DIR, "bin")
if os.environ.get("ngraph_DIR") is not None:
root_dir = pathlib.Path(os.environ["ngraph_DIR"]) / ".."
lib_ext = ""
if "linux" in sys.platform:
lib_ext = ".so"
elif sys.platform == "darwin":
lib_ext = ".dylib"
elif sys.platform == "win32":
lib_ext = ".dll"
libs = []
for ngraph_lib in NGRAPH_LIBS:
libs.extend(list(glob.iglob("{0}/**/*{1}*{2}".format(root_dir,
ngraph_lib, lib_ext), recursive=True)))
if not libs:
raise Exception("NGraph libs not found.")
self.announce("Adding library files" + str(libs), level=3)
self.distribution.data_files.extend([("lib", [os.path.normpath(lib) for lib in libs])])
self.distribution.run_command("install_data")
super().run()
cmdclass["build_ext"] = BuildCMakeExt
cmdclass["install_lib"] = InstallCMakeLibs
setup(
name="ngraph-core",
description="nGraph - Intel's graph compiler and runtime for Neural Networks",
@@ -369,12 +225,12 @@ setup(
author="Intel Corporation",
url="https://github.com/openvinotoolkit/openvino",
license="License :: OSI Approved :: Apache Software License",
ext_modules=ext_modules,
ext_modules=[CMakeExtension(name="_pyngraph")],
package_dir={"": "src"},
packages=packages,
cmdclass={"build_ext": BuildExt},
data_files=data_files,
install_requires=requirements,
data_files=data_files,
zip_safe=False,
extras_require={},
cmdclass=cmdclass
)