diff --git a/.flake8 b/.flake8 index 5144dc0d..fefbc4a2 100644 --- a/.flake8 +++ b/.flake8 @@ -41,7 +41,9 @@ per-file-ignores = mkl_fft/interfaces/scipy_fft.py: F401 mkl_fft/interfaces/numpy_fft.py: F401 -exclude = _vendored/conv_template.py +exclude = + _vendored/conv_template.py + _vendored/process_src_template.py filename = *.py, *.pyx, *.pxi, *.pxd max_line_length = 80 diff --git a/.github/workflows/build-with-clang.yml b/.github/workflows/build-with-clang.yml index 1e6a79fd..745d151c 100644 --- a/.github/workflows/build-with-clang.yml +++ b/.github/workflows/build-with-clang.yml @@ -40,7 +40,6 @@ jobs: - name: Install Intel OneAPI run: | sudo apt-get install intel-oneapi-compiler-dpcpp-cpp - sudo apt-get install intel-oneapi-tbb sudo apt-get install intel-oneapi-mkl-devel - name: Setup Python @@ -56,7 +55,7 @@ jobs: - name: Install mkl_fft dependencies run: | - pip install cython setuptools">=77" + pip install meson-python ninja cython cmake pip install ${{ matrix.numpy_version }} - name: List oneAPI folder content @@ -73,5 +72,6 @@ jobs: - name: Run mkl_fft tests run: | source ${{ env.ONEAPI_ROOT }}/setvars.sh - pip install scipy mkl-service pytest + pip install scipy pytest + pip install mkl-service --no-deps pytest -s -v --pyargs mkl_fft diff --git a/.github/workflows/build-with-standard-clang.yml b/.github/workflows/build-with-standard-clang.yml new file mode 100644 index 00000000..831c76da --- /dev/null +++ b/.github/workflows/build-with-standard-clang.yml @@ -0,0 +1,64 @@ +name: Build project with standard clang compiler + +on: + pull_request: + push: + branches: [master] + +permissions: read-all + +jobs: + build-with-standard-clang: + runs-on: ubuntu-latest + + strategy: + matrix: + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + numpy_version: ["numpy'>=2'"] + + env: + COMPILER_ROOT: /usr/bin + + defaults: + run: + shell: bash -el {0} + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0 + with: + access_token: ${{ github.token }} + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python }} + architecture: x64 + + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Install mkl_fft dependencies + run: | + pip install meson-python ninja cmake cython mkl-devel + pip install ${{ matrix.numpy_version }} + + - name: Build mkl_fft + run: | + export CC=${{ env.COMPILER_ROOT }}/clang + pip install . --no-build-isolation --no-deps --verbose + + - name: Run mkl_fft tests + run: | + pip install pytest mkl-service scipy + # mkl_fft cannot be installed in editable mode, we need + # to change directory before importing it and running tests + cd .. + python -m pytest -sv --pyargs mkl_fft diff --git a/.github/workflows/build_pip.yaml b/.github/workflows/build_pip.yml similarity index 82% rename from .github/workflows/build_pip.yaml rename to .github/workflows/build_pip.yml index 80cc1e2c..9b7bd6bf 100644 --- a/.github/workflows/build_pip.yaml +++ b/.github/workflows/build_pip.yml @@ -26,11 +26,6 @@ jobs: use_pre: ["", "--pre"] steps: - - name: Install jq - shell: bash -l {0} - run: | - sudo apt-get install jq - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -48,10 +43,8 @@ jobs: - name: Build conda package run: | - pip install --no-cache-dir cython setuptools + pip install --no-cache-dir meson-python ninja cmake cython pip install --no-cache-dir numpy ${{ matrix.use_pre }} - echo "CONDA_PREFFIX is '${CONDA_PREFIX}'" - export MKLROOT=${CONDA_PREFIX} pip install -e ".[test]" --no-build-isolation --verbose pip list python -m pytest -v mkl_fft/tests diff --git a/_vendored/README.md b/_vendored/README.md index b3f3a919..42ec7946 100644 --- a/_vendored/README.md +++ b/_vendored/README.md @@ -1,5 +1,3 @@ ## Vendored files -File `conv_template.py` is copied from NumPy's numpy/distutils folder, since -`numpy.distutils` is absent from the installation layout starting with -Python 3.12 +Files `conv_template.py` and `process_src_template.py` are copied from NumPy's numpy/numpy/_build_utils folder diff --git a/_vendored/conv_template.py b/_vendored/conv_template.py index c8933d1d..3f634737 100644 --- a/_vendored/conv_template.py +++ b/_vendored/conv_template.py @@ -82,8 +82,8 @@ __all__ = ['process_str', 'process_file'] import os -import sys import re +import sys # names for replacement that are already global. global_names = {} @@ -106,12 +106,12 @@ def parse_structure(astr, level): at zero. Returns an empty list if no loops found. """ - if level == 0 : + if level == 0: loopbeg = "/**begin repeat" loopend = "/**end repeat**/" - else : - loopbeg = "/**begin repeat%d" % level - loopend = "/**end repeat%d**/" % level + else: + loopbeg = f"/**begin repeat{level}" + loopend = f"/**end repeat{level}**/" ind = 0 line = 0 @@ -124,9 +124,9 @@ def parse_structure(astr, level): start2 = astr.find("\n", start2) fini1 = astr.find(loopend, start2) fini2 = astr.find("\n", fini1) - line += astr.count("\n", ind, start2+1) - spanlist.append((start, start2+1, fini1, fini2+1, line)) - line += astr.count("\n", start2+1, fini2) + line += astr.count("\n", ind, start2 + 1) + spanlist.append((start, start2 + 1, fini1, fini2 + 1, line)) + line += astr.count("\n", start2 + 1, fini2) ind = fini2 spanlist.sort() return spanlist @@ -135,10 +135,13 @@ def parse_structure(astr, level): def paren_repl(obj): torep = obj.group(1) numrep = obj.group(2) - return ','.join([torep]*int(numrep)) + return ','.join([torep] * int(numrep)) + parenrep = re.compile(r"\(([^)]*)\)\*(\d+)") plainrep = re.compile(r"([^*]+)\*(\d+)") + + def parse_values(astr): # replaces all occurrences of '(a,b,c)*4' in astr # with 'a,b,c,a,b,c,a,b,c,a,b,c'. Empty braces generate @@ -155,7 +158,7 @@ def parse_values(astr): named_re = re.compile(r"#\s*(\w*)\s*=([^#]*)#") exclude_vars_re = re.compile(r"(\w*)=(\w*)") exclude_re = re.compile(":exclude:") -def parse_loop_header(loophead) : +def parse_loop_header(loophead): """Find all named replacements in the header Returns a list of dictionaries, one for each loop iteration, @@ -179,14 +182,13 @@ def parse_loop_header(loophead) : name = rep[0] vals = parse_values(rep[1]) size = len(vals) - if nsub is None : + if nsub is None: nsub = size - elif nsub != size : + elif nsub != size: msg = "Mismatch in number of values, %d != %d\n%s = %s" raise ValueError(msg % (nsub, size, name, vals)) names.append((name, vals)) - # Find any exclude variables excludes = [] @@ -200,30 +202,33 @@ def parse_loop_header(loophead) : # generate list of dictionaries, one for each template iteration dlist = [] - if nsub is None : + if nsub is None: raise ValueError("No substitution variables found") for i in range(nsub): tmp = {name: vals[i] for name, vals in names} dlist.append(tmp) return dlist + replace_re = re.compile(r"@(\w+)@") -def parse_string(astr, env, level, line) : - lineno = "#line %d\n" % line + + +def parse_string(astr, env, level, line): + lineno = f"#line {line}\n" # local function for string replacement, uses env def replace(match): name = match.group(1) - try : + try: val = env[name] except KeyError: - msg = 'line %d: no definition of key "%s"'%(line, name) + msg = f'line {line}: no definition of key "{name}"' raise ValueError(msg) from None return val code = [lineno] struct = parse_structure(astr, level) - if struct : + if struct: # recurse over inner loops oldend = 0 newlevel = level + 1 @@ -234,18 +239,18 @@ def replace(match): oldend = sub[3] newline = line + sub[4] code.append(replace_re.sub(replace, pref)) - try : + try: envlist = parse_loop_header(head) except ValueError as e: - msg = "line %d: %s" % (newline, e) + msg = f"line {newline}: {e}" raise ValueError(msg) - for newenv in envlist : + for newenv in envlist: newenv.update(env) newcode = parse_string(text, newenv, newlevel, newline) code.extend(newcode) suff = astr[oldend:] code.append(replace_re.sub(replace, suff)) - else : + else: # replace keys code.append(replace_re.sub(replace, astr)) code.append('\n') @@ -284,8 +289,8 @@ def process_file(source): try: code = process_str(''.join(lines)) except ValueError as e: - raise ValueError('In "%s" loop at %s' % (sourcefile, e)) from None - return '#line 1 "%s"\n%s' % (sourcefile, code) + raise ValueError(f'In "{sourcefile}" loop at {e}') from None + return f'#line 1 "{sourcefile}"\n{code}' def unique_key(adict): @@ -321,9 +326,10 @@ def main(): try: writestr = process_str(allstr) except ValueError as e: - raise ValueError("In %s loop at %s" % (file, e)) from None + raise ValueError(f"In {file} loop at {e}") from None outfile.write(writestr) + if __name__ == "__main__": main() diff --git a/_vendored/process_src_template.py b/_vendored/process_src_template.py new file mode 100644 index 00000000..f934c222 --- /dev/null +++ b/_vendored/process_src_template.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import argparse +import importlib.util +import os + + +def get_processor(): + # Convoluted because we can't import from numpy + # (numpy is not yet built) + conv_template_path = os.path.join( + os.path.dirname(__file__), + 'conv_template.py' + ) + spec = importlib.util.spec_from_file_location( + 'conv_template', conv_template_path + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.process_file + + +def process_and_write_file(fromfile, outfile): + """Process tempita templated file and write out the result. + + The template file is expected to end in `.src` + (e.g., `.c.src` or `.h.src`). + Processing `npy_somefile.c.src` generates `npy_somefile.c`. + + """ + process_file = get_processor() + content = process_file(fromfile) + with open(outfile, 'w') as f: + f.write(content) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + type=str, + help="Path to the input file" + ) + parser.add_argument( + "-o", + "--outfile", + type=str, + help="Path to the output file" + ) + parser.add_argument( + "-i", + "--ignore", + type=str, + help="An ignored input - may be useful to add a " + "dependency between custom targets", + ) + args = parser.parse_args() + + if not args.infile.endswith('.src'): + raise ValueError(f"Unexpected extension: {args.infile}") + + outfile_abs = os.path.join(os.getcwd(), args.outfile) + process_and_write_file(args.infile, outfile_abs) + + +if __name__ == "__main__": + main() diff --git a/conda-recipe-cf/bld.bat b/conda-recipe-cf/bld.bat index d93a0cdc..a08c27bb 100644 --- a/conda-recipe-cf/bld.bat +++ b/conda-recipe-cf/bld.bat @@ -1,3 +1,2 @@ -set MKLROOT=%PREFIX% -%PYTHON% setup.py build --force install --old-and-unmanageable +%PYTHON% -m pip install --no-build-isolation --no-deps . if errorlevel 1 exit 1 diff --git a/conda-recipe-cf/build.sh b/conda-recipe-cf/build.sh index 6a8fc6d3..562c55f3 100644 --- a/conda-recipe-cf/build.sh +++ b/conda-recipe-cf/build.sh @@ -1,6 +1,3 @@ #!/bin/bash -x -# make sure that compiler has been sourced, if necessary - -export MKLROOT=${PREFIX} -$PYTHON setup.py build --force install --old-and-unmanageable +$PYTHON -m pip install --no-build-isolation --no-deps . diff --git a/conda-recipe-cf/meta.yaml b/conda-recipe-cf/meta.yaml index 6d3a1745..8aad2826 100644 --- a/conda-recipe-cf/meta.yaml +++ b/conda-recipe-cf/meta.yaml @@ -15,11 +15,15 @@ requirements: - {{ compiler('c') }} - {{ stdlib('c') }} host: + - meson-python >=0.13.0 + - meson + - cmake + - ninja - python - python-gil # [py>=314] - pip - - setuptools >=77 - mkl-devel + - llvm-openmp - cython - numpy - wheel >=0.41.3 @@ -27,6 +31,7 @@ requirements: - python - python-gil # [py>=314] - {{ pin_compatible('mkl') }} + - {{ pin_compatible('llvm-openmp') }} - numpy test: diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat index 3a1cbb81..8213fa27 100644 --- a/conda-recipe/bld.bat +++ b/conda-recipe/bld.bat @@ -1,5 +1,3 @@ -set MKLROOT=%PREFIX% - rem Build wheel package if NOT "%WHEELS_OUTPUT_FOLDER%"=="" ( %PYTHON% -m pip wheel --no-build-isolation --no-deps . diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh index f55643db..42e4ac26 100644 --- a/conda-recipe/build.sh +++ b/conda-recipe/build.sh @@ -1,6 +1,5 @@ #!/bin/bash -x -export MKLROOT=$PREFIX export CFLAGS="-I$PREFIX/include $CFLAGS" export LDFLAGS="-Wl,-rpath,\$ORIGIN/../.. -Wl,-rpath,\$ORIGIN/../../.. -L${PREFIX}/lib ${LDFLAGS}" diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 7211767d..0c589d1e 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -25,11 +25,15 @@ requirements: - {{ compiler('c') }} - {{ stdlib('c') }} host: + - meson-python >=0.13.0 + - meson + - cmake + - ninja - python - python-gil # [py>=314] - pip - - setuptools >=77 - mkl-devel + - intel-openmp - cython {% if use_numpy_base %} - numpy-base @@ -41,6 +45,7 @@ requirements: - python - python-gil # [py>=314] - {{ pin_compatible('mkl') }} + - {{ pin_compatible('intel-openmp') }} {% if use_numpy_base %} - numpy-base {% else %} diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..eaaaa082 --- /dev/null +++ b/meson.build @@ -0,0 +1,97 @@ +project( + 'mkl_fft', + ['c', 'cython'], + version: run_command( + 'python', '-c', + 'import os; exec(open("mkl_fft/_version.py").read()); print(__version__)', + check: true + ).stdout().strip(), + default_options: [ + 'buildtype=release', + ] +) + +py = import('python').find_installation(pure: false) + +# numpy includes +np_dir = run_command(py, + ['-c', 'import numpy; print(numpy.get_include())'], + check: true +).stdout().strip() + +inc_np = include_directories(np_dir, 'mkl_fft/src', 'mkl_fft') + +# compiler/linker +# this looks like dead code, but it isn't: removing get_compiler call +# can cause segfaults at runtime (most notably when building with Intel clang) +cc = meson.get_compiler('c') +c_args = ['-DNDEBUG'] + +mkl_dep = dependency('MKL', method: 'cmake', + modules: ['MKL::MKL'], + cmake_args: [ + '-DMKL_ARCH=intel64', + '-DMKL_LINK=dynamic', + '-DMKL_THREADING=intel_thread', + '-DMKL_INTERFACE=lp64' + ], + required: true +) + +# code gen +src_file_cli = files('_vendored/process_src_template.py') + +gen_mklfft_c = custom_target( + 'mklfft_c_generated', + output: 'mklfft.c', + input: 'mkl_fft/src/mklfft.c.src', + command: [py, src_file_cli, '@INPUT@', '-o', '@OUTPUT@'], +) + +# cython +py.extension_module( + '_pydfti', + sources: [ + 'mkl_fft/_pydfti.pyx', + gen_mklfft_c + ], + include_directories: inc_np, + dependencies: [mkl_dep], + c_args: c_args + [ + '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION', + '-DPY_ARRAY_UNIQUE_SYMBOL=mkl_fft_ext' + ], + install: true, + subdir: 'mkl_fft' +) + +# python +py.install_sources( + [ + 'mkl_fft/__init__.py', + 'mkl_fft/_fft_utils.py', + 'mkl_fft/_init_helper.py', + 'mkl_fft/_mkl_fft.py', + 'mkl_fft/_patch_numpy.py', + 'mkl_fft/_version.py', + ], + subdir: 'mkl_fft' +) + +py.install_sources( + [ + 'mkl_fft/interfaces/__init__.py', + 'mkl_fft/interfaces/_float_utils.py', + 'mkl_fft/interfaces/_numpy_fft.py', + 'mkl_fft/interfaces/_numpy_helper.py', + 'mkl_fft/interfaces/_scipy_fft.py', + 'mkl_fft/interfaces/numpy_fft.py', + 'mkl_fft/interfaces/scipy_fft.py', + ], + subdir: 'mkl_fft/interfaces' +) + +install_subdir( + 'mkl_fft/tests', + install_dir: py.get_install_dir() / 'mkl_fft' +) diff --git a/pyproject.toml b/pyproject.toml index a17d776a..638d738e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,13 +24,18 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [build-system] -build-backend = "setuptools.build_meta" -requires = ["setuptools>=77", "Cython", "numpy", "mkl-devel"] +build-backend = "mesonpy" +requires = [ + "meson-python>=0.13.0", + "ninja", + "Cython", + "numpy", + "mkl-devel", + "cmake" +] [project] -authors = [ - {name = "Intel Corporation", email = "scripting@intel.com"} -] +authors = [{name = "Intel Corporation"}] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", @@ -68,7 +73,8 @@ Download = "http://github.com/IntelPython/mkl_fft" Homepage = "http://github.com/IntelPython/mkl_fft" [tool.black] -exclude = "_vendored/conv_template.py" +extend-exclude = "(^|/)_vendored/" +force-exclude = "(^|/)_vendored/" line-length = 80 [tool.cython-lint] @@ -81,15 +87,5 @@ force_grid_wrap = 0 include_trailing_comma = true line_length = 80 multi_line_output = 3 -skip = ["_vendored/conv_template.py"] +skip = ["_vendored/conv_template.py", "_vendored/process_src_template.py"] use_parentheses = true - -[tool.setuptools] -include-package-data = true -packages = ["mkl_fft", "mkl_fft.interfaces"] - -[tool.setuptools.dynamic] -version = {attr = "mkl_fft._version.__version__"} - -[tool.setuptools.package-data] -"mkl_fft" = ["tests/**/*.py"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 2125391a..00000000 --- a/setup.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2017, Intel Corporation -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of Intel Corporation nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -import sys -from os.path import join - -import Cython.Build -import numpy -from setuptools import Extension, setup - -sys.path.insert(0, os.path.dirname(__file__)) # Ensures local imports work -from _vendored.conv_template import process_file as process_c_file # noqa: E402 - - -def extensions(): - mkl_root = os.environ.get("MKLROOT", None) - if mkl_root: - mkl_info = { - "include_dirs": [join(mkl_root, "include")], - "library_dirs": [ - join(mkl_root, "lib"), - join(mkl_root, "lib", "intel64"), - ], - "libraries": ["mkl_rt"], - } - else: - raise ValueError("MKLROOT environment variable not set.") - - mkl_include_dirs = mkl_info.get("include_dirs", []) - mkl_library_dirs = mkl_info.get("library_dirs", []) - mkl_libraries = mkl_info.get("libraries", ["mkl_rt"]) - - mklfft_templ = join("mkl_fft", "src", "mklfft.c.src") - processed_mklfft_fn = join("mkl_fft", "src", "mklfft.c") - src_processed = process_c_file(mklfft_templ) - - with open(processed_mklfft_fn, "w") as fid: - fid.write(src_processed) - - return [ - Extension( - "mkl_fft._pydfti", - sources=[ - join("mkl_fft", "_pydfti.pyx"), - join("mkl_fft", "src", "mklfft.c"), - ], - depends=[ - join("mkl_fft", "src", "mklfft.h"), - join("mkl_fft", "src", "multi_iter.h"), - ], - include_dirs=[join("mkl_fft", "src"), numpy.get_include()] - + mkl_include_dirs, - libraries=mkl_libraries, - library_dirs=mkl_library_dirs, - extra_compile_args=[ - "-DNDEBUG", - # '-ggdb', '-O0', '-Wall', '-Wextra', '-DDEBUG', - ], - define_macros=[ - ("NPY_NO_DEPRECATED_API", None), - ("PY_ARRAY_UNIQUE_SYMBOL", "mkl_fft_ext"), - ], - ) - ] - - -setup( - cmdclass={"build_ext": Cython.Build.build_ext}, - ext_modules=extensions(), - zip_safe=False, -)