Skip to content

Commit

Permalink
query for libaio package using known package managers (#1250)
Browse files Browse the repository at this point in the history
* aio: test for libaio with various package managers

* aio: note typical tool used to install libaio package

* setup: abort with error if cannot build requested op

* setup: define op_envvar to return op build environment variable

* setup: call is_compatible once for each op

* setup: only print suggestion to disable op when its envvar not set

* setup: add method to abort from fatal error

* Revert "setup: add method to abort from fatal error"

This reverts commit 0e4cde6.

* setup: add method to abort from fatal error

Co-authored-by: Olatunji Ruwase <olruwase@microsoft.com>
  • Loading branch information
adammoody and tjruwase authored Jul 29, 2021
1 parent 97f7ed9 commit e82060d
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 8 deletions.
44 changes: 43 additions & 1 deletion op_builder/async_io.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""
Copyright 2020 The Microsoft DeepSpeed Team
"""
import distutils.spawn
import subprocess

from .builder import OpBuilder


Expand Down Expand Up @@ -50,6 +53,37 @@ def cxx_args(self):
def extra_ldflags(self):
return ['-laio']

def check_for_libaio_pkg(self):
libs = dict(
dpkg=["-l",
"libaio-dev",
"apt"],
pacman=["-Q",
"libaio",
"pacman"],
rpm=["-q",
"libaio-devel",
"yum"],
)

found = False
for pkgmgr, data in libs.items():
flag, lib, tool = data
path = distutils.spawn.find_executable(pkgmgr)
if path is not None:
cmd = f"{pkgmgr} {flag} {lib}"
result = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
if result.wait() == 0:
found = True
else:
self.warning(
f"{self.NAME}: please install the {lib} package with {tool}")
break
return found

def is_compatible(self):
# Check for the existence of libaio by using distutils
# to compile and link a test program that calls io_submit,
Expand All @@ -59,6 +93,14 @@ def is_compatible(self):
aio_compatible = self.has_function('io_submit', ('aio', ))
if not aio_compatible:
self.warning(
f"{self.NAME} requires libaio but it is missing. Can be fixed by: `apt install libaio-dev`."
f"{self.NAME} requires the dev libaio .so object and headers but these were not found."
)

# Check for the libaio package via known package managers
# to print suggestions on which package to install.
self.check_for_libaio_pkg()

self.warning(
"If libaio is already installed (perhaps from source), try setting the CFLAGS and LDFLAGS environment variables to where it can be found."
)
return super().is_compatible() and aio_compatible
87 changes: 84 additions & 3 deletions op_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
import importlib
from pathlib import Path
import subprocess
import shlex
import shutil
import tempfile
import distutils.ccompiler
import distutils.log
import distutils.sysconfig
from distutils.errors import CompileError, LinkError
from abc import ABC, abstractmethod

YELLOW = '\033[93m'
Expand Down Expand Up @@ -161,9 +167,84 @@ def libraries_installed(self, libraries):
valid = valid or result.wait() == 0
return valid

def has_function(self, funcname, libraries):
compiler = distutils.ccompiler.new_compiler()
return compiler.has_function(funcname, libraries=libraries)
def has_function(self, funcname, libraries, verbose=False):
'''
Test for existence of a function within a tuple of libraries.
This is used as a smoke test to check whether a certain library is avaiable.
As a test, this creates a simple C program that calls the specified function,
and then distutils is used to compile that program and link it with the specified libraries.
Returns True if both the compile and link are successful, False otherwise.
'''
tempdir = None # we create a temporary directory to hold various files
filestderr = None # handle to open file to which we redirect stderr
oldstderr = None # file descriptor for stderr
try:
# Echo compile and link commands that are used.
if verbose:
distutils.log.set_verbosity(1)

# Create a compiler object.
compiler = distutils.ccompiler.new_compiler(verbose=verbose)

# Configure compiler and linker to build according to Python install.
distutils.sysconfig.customize_compiler(compiler)

# Create a temporary directory to hold test files.
tempdir = tempfile.mkdtemp()

# Define a simple C program that calls the function in question
prog = "void %s(void); int main(int argc, char** argv) { %s(); return 0; }" % (
funcname,
funcname)

# Write the test program to a file.
filename = os.path.join(tempdir, 'test.c')
with open(filename, 'w') as f:
f.write(prog)

# Redirect stderr file descriptor to a file to silence compile/link warnings.
if not verbose:
filestderr = open(os.path.join(tempdir, 'stderr.txt'), 'w')
oldstderr = os.dup(sys.stderr.fileno())
os.dup2(filestderr.fileno(), sys.stderr.fileno())

# Attempt to compile the C program into an object file.
cflags = shlex.split(os.environ.get('CFLAGS', ""))
objs = compiler.compile([filename],
extra_preargs=self.strip_empty_entries(cflags))

# Attempt to link the object file into an executable.
# Be sure to tack on any libraries that have been specified.
ldflags = shlex.split(os.environ.get('LDFLAGS', ""))
compiler.link_executable(objs,
os.path.join(tempdir,
'a.out'),
extra_preargs=self.strip_empty_entries(ldflags),
libraries=libraries)

# Compile and link succeeded
return True

except CompileError:
return False

except LinkError:
return False

except:
return False

finally:
# Restore stderr file descriptor and close the stderr redirect file.
if oldstderr is not None:
os.dup2(oldstderr, sys.stderr.fileno())
if filestderr is not None:
filestderr.close()

# Delete the temporary directory holding the test program and stderr files.
if tempdir is not None:
shutil.rmtree(tempdir)

def strip_empty_entries(self, args):
'''
Expand Down
28 changes: 24 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@

from op_builder import ALL_OPS, get_default_compute_capatabilities

RED_START = '\033[31m'
RED_END = '\033[0m'
ERROR = f"{RED_START} [ERROR] {RED_END}"


def abort(msg):
print(f"{ERROR} {msg}")
assert False, msg


def fetch_requirements(path):
with open(path, 'r') as fd:
Expand Down Expand Up @@ -101,16 +110,29 @@ def command_exists(cmd):
return result.wait() == 0


def op_enabled(op_name):
def op_envvar(op_name):
assert hasattr(ALL_OPS[op_name], 'BUILD_VAR'), \
f"{op_name} is missing BUILD_VAR field"
env_var = ALL_OPS[op_name].BUILD_VAR
return ALL_OPS[op_name].BUILD_VAR


def op_enabled(op_name):
env_var = op_envvar(op_name)
return int(os.environ.get(env_var, BUILD_OP_DEFAULT))


compatible_ops = dict.fromkeys(ALL_OPS.keys(), False)
install_ops = dict.fromkeys(ALL_OPS.keys(), False)
for op_name, builder in ALL_OPS.items():
op_compatible = builder.is_compatible()
compatible_ops[op_name] = op_compatible

# If op is requested but not available, throw an error
if op_enabled(op_name) and not op_compatible:
env_var = op_envvar(op_name)
if env_var not in os.environ:
builder.warning(f"One can disable {op_name} with {env_var}=0")
abort(f"Unable to pre-compile {op_name}")

# If op is compatible update install reqs so it can potentially build/run later
if op_compatible:
Expand All @@ -123,8 +145,6 @@ def op_enabled(op_name):
install_ops[op_name] = op_enabled(op_name)
ext_modules.append(builder.builder())

compatible_ops = {op_name: op.is_compatible() for (op_name, op) in ALL_OPS.items()}

print(f'Install Ops={install_ops}')

# Write out version/git info
Expand Down

0 comments on commit e82060d

Please sign in to comment.