|
6 | 6 | import importlib |
7 | 7 | import logging |
8 | 8 | import os |
| 9 | +import pathlib |
9 | 10 | import pickle |
10 | 11 | import platform |
11 | 12 | import re |
@@ -2715,203 +2716,153 @@ def default_blas_ldflags(): |
2715 | 2716 | str |
2716 | 2717 |
|
2717 | 2718 | """ |
2718 | | - warn_record = [] |
2719 | | - try: |
2720 | | - blas_info = np.__config__.get_info("blas_opt") |
2721 | | - |
2722 | | - # If we are in a EPD installation, mkl is available |
2723 | | - if "EPD" in sys.version: |
2724 | | - use_unix_epd = True |
2725 | | - if sys.platform == "win32": |
2726 | | - return " ".join( |
2727 | | - ['-L"%s"' % os.path.join(sys.prefix, "Scripts")] |
2728 | | - + |
2729 | | - # Why on Windows, the library used are not the |
2730 | | - # same as what is in |
2731 | | - # blas_info['libraries']? |
2732 | | - [f"-l{l}" for l in ("mk2_core", "mk2_intel_thread", "mk2_rt")] |
2733 | | - ) |
2734 | | - elif sys.platform == "darwin": |
2735 | | - # The env variable is needed to link with mkl |
2736 | | - new_path = os.path.join(sys.prefix, "lib") |
2737 | | - v = os.getenv("DYLD_FALLBACK_LIBRARY_PATH", None) |
2738 | | - if v is not None: |
2739 | | - # Explicit version could be replaced by a symbolic |
2740 | | - # link called 'Current' created by EPD installer |
2741 | | - # This will resolve symbolic links |
2742 | | - v = os.path.realpath(v) |
2743 | | - |
2744 | | - # The python __import__ don't seam to take into account |
2745 | | - # the new env variable "DYLD_FALLBACK_LIBRARY_PATH" |
2746 | | - # when we set with os.environ['...'] = X or os.putenv() |
2747 | | - # So we warn the user and tell him what todo. |
2748 | | - if v is None or new_path not in v.split(":"): |
2749 | | - _logger.warning( |
2750 | | - "The environment variable " |
2751 | | - "'DYLD_FALLBACK_LIBRARY_PATH' does not contain " |
2752 | | - "the '{new_path}' path in its value. This will make " |
2753 | | - "PyTensor use a slow version of BLAS. Update " |
2754 | | - "'DYLD_FALLBACK_LIBRARY_PATH' to contain the " |
2755 | | - "said value, this will disable this warning." |
2756 | | - ) |
2757 | | - |
2758 | | - use_unix_epd = False |
2759 | | - if use_unix_epd: |
2760 | | - return " ".join( |
2761 | | - ["-L%s" % os.path.join(sys.prefix, "lib")] |
2762 | | - + ["-l%s" % l for l in blas_info["libraries"]] |
2763 | | - ) |
2764 | | - |
2765 | | - # Canopy |
2766 | | - if "Canopy" in sys.prefix: |
2767 | | - subsub = "lib" |
2768 | | - if sys.platform == "win32": |
2769 | | - subsub = "Scripts" |
2770 | | - lib_path = os.path.join(sys.base_prefix, subsub) |
2771 | | - if not os.path.exists(lib_path): |
2772 | | - # Old logic to find the path. I don't think we still |
2773 | | - # need it, but I don't have the time to test all |
2774 | | - # installation configuration. So I keep this as a fall |
2775 | | - # back in case the current expectation don't work. |
2776 | | - |
2777 | | - # This old logic don't work when multiple version of |
2778 | | - # Canopy is installed. |
2779 | | - p = os.path.join(sys.base_prefix, "..", "..", "appdata") |
2780 | | - assert os.path.exists(p), "Canopy changed the location of MKL" |
2781 | | - lib_paths = os.listdir(p) |
2782 | | - # Try to remove subdir that can't contain MKL |
2783 | | - for sub in lib_paths: |
2784 | | - if not os.path.exists(os.path.join(p, sub, subsub)): |
2785 | | - lib_paths.remove(sub) |
2786 | | - assert len(lib_paths) == 1, ( |
2787 | | - "Unexpected case when looking for Canopy MKL libraries", |
2788 | | - p, |
2789 | | - lib_paths, |
2790 | | - [os.listdir(os.path.join(p, sub)) for sub in lib_paths], |
2791 | | - ) |
2792 | | - lib_path = os.path.join(p, lib_paths[0], subsub) |
2793 | | - assert os.path.exists(lib_path), "Canopy changed the location of MKL" |
2794 | | - |
2795 | | - if sys.platform == "linux2" or sys.platform == "darwin": |
2796 | | - return " ".join( |
2797 | | - ["-L%s" % lib_path] + ["-l%s" % l for l in blas_info["libraries"]] |
2798 | | - ) |
2799 | | - elif sys.platform == "win32": |
2800 | | - return " ".join( |
2801 | | - ['-L"%s"' % lib_path] |
2802 | | - + |
2803 | | - # Why on Windows, the library used are not the |
2804 | | - # same as what is in blas_info['libraries']? |
2805 | | - [f"-l{l}" for l in ("mk2_core", "mk2_intel_thread", "mk2_rt")] |
2806 | | - ) |
2807 | 2719 |
|
2808 | | - # MKL |
2809 | | - # If mkl can be imported then use it. On conda: |
2810 | | - # "conda install mkl-service" installs the Python wrapper and |
2811 | | - # the low-level C libraries as well as optimised version of |
2812 | | - # numpy and scipy. |
2813 | | - try: |
2814 | | - import mkl # noqa |
2815 | | - except ImportError: |
2816 | | - pass |
2817 | | - else: |
2818 | | - # This branch is executed if no exception was raised |
2819 | | - if sys.platform == "win32": |
2820 | | - lib_path = os.path.join(sys.prefix, "Library", "bin") |
2821 | | - flags = [f'-L"{lib_path}"'] |
2822 | | - else: |
2823 | | - lib_path = blas_info.get("library_dirs", []) |
2824 | | - flags = [] |
2825 | | - if lib_path: |
2826 | | - flags = [f"-L{lib_path[0]}"] |
2827 | | - if "2018" in mkl.get_version_string(): |
2828 | | - thr = "mkl_gnu_thread" |
2829 | | - else: |
2830 | | - thr = "mkl_intel_thread" |
2831 | | - base_flags = list(flags) |
2832 | | - flags += [f"-l{l}" for l in ("mkl_core", thr, "mkl_rt")] |
2833 | | - res = try_blas_flag(flags) |
2834 | | - |
2835 | | - if not res and sys.platform == "win32" and thr == "mkl_gnu_thread": |
2836 | | - # Check if it would work for intel OpenMP on windows |
2837 | | - flags = base_flags + [ |
2838 | | - f"-l{l}" for l in ("mkl_core", "mkl_intel_thread", "mkl_rt") |
| 2720 | + def check_required_file(paths, required_regexs): |
| 2721 | + libs = [] |
| 2722 | + for req in required_regexs: |
| 2723 | + found = False |
| 2724 | + for path in paths: |
| 2725 | + m = re.search(req, path.name) |
| 2726 | + if m: |
| 2727 | + libs.append((str(path.parent), m.string[slice(*m.span())])) |
| 2728 | + found = True |
| 2729 | + break |
| 2730 | + if not found: |
| 2731 | + raise RuntimeError(f"Required file {req} not found") |
| 2732 | + return libs |
| 2733 | + |
| 2734 | + def get_cxx_library_dirs(): |
| 2735 | + cmd = f"{config.cxx} -print-search-dirs" |
| 2736 | + p = subprocess_Popen( |
| 2737 | + cmd, |
| 2738 | + stdout=subprocess.PIPE, |
| 2739 | + stderr=subprocess.PIPE, |
| 2740 | + stdin=subprocess.PIPE, |
| 2741 | + shell=True, |
| 2742 | + ) |
| 2743 | + (stdout, stderr) = p.communicate(input=b"") |
| 2744 | + maybe_lib_dirs = [ |
| 2745 | + [pathlib.Path(p).resolve() for p in line[len("libraries: =") :].split(":")] |
| 2746 | + for line in stdout.decode(sys.stdout.encoding).splitlines() |
| 2747 | + if line.startswith("libraries: =") |
| 2748 | + ][0] |
| 2749 | + return [str(d) for d in maybe_lib_dirs if d.exists() and d.is_dir()] |
| 2750 | + |
| 2751 | + def check_libs( |
| 2752 | + all_libs, required_libs, extra_compile_flags=None, cxx_library_dirs=None |
| 2753 | + ): |
| 2754 | + if cxx_library_dirs is None: |
| 2755 | + cxx_library_dirs = [] |
| 2756 | + if extra_compile_flags is None: |
| 2757 | + extra_compile_flags = [] |
| 2758 | + found_libs = check_required_file( |
| 2759 | + all_libs, |
| 2760 | + required_libs, |
| 2761 | + ) |
| 2762 | + path_quote = '"' if sys.platform == "win32" else "" |
| 2763 | + libdir_ldflags = list( |
| 2764 | + dict.fromkeys( |
| 2765 | + [ |
| 2766 | + f"-L{path_quote}{lib_path}{path_quote}" |
| 2767 | + for lib_path, _ in found_libs |
| 2768 | + if lib_path not in cxx_library_dirs |
2839 | 2769 | ] |
2840 | | - res = try_blas_flag(flags) |
2841 | | - |
2842 | | - if res: |
2843 | | - check_mkl_openmp() |
2844 | | - return res |
2845 | | - |
2846 | | - flags.extend(["-Wl,-rpath," + l for l in blas_info.get("library_dirs", [])]) |
2847 | | - res = try_blas_flag(flags) |
2848 | | - if res: |
2849 | | - check_mkl_openmp() |
2850 | | - maybe_add_to_os_environ_pathlist("PATH", lib_path[0]) |
2851 | | - return res |
2852 | | - |
2853 | | - # to support path that includes spaces, we need to wrap it with double quotes on Windows |
2854 | | - path_wrapper = '"' if os.name == "nt" else "" |
2855 | | - ret = ( |
2856 | | - # TODO: the Gemm op below should separate the |
2857 | | - # -L and -l arguments into the two callbacks |
2858 | | - # that CLinker uses for that stuff. for now, |
2859 | | - # we just pass the whole ldflags as the -l |
2860 | | - # options part. |
2861 | | - [ |
2862 | | - f"-L{path_wrapper}{l}{path_wrapper}" |
2863 | | - for l in blas_info.get("library_dirs", []) |
2864 | | - ] |
2865 | | - + [f"-l{l}" for l in blas_info.get("libraries", [])] |
2866 | | - + blas_info.get("extra_link_args", []) |
| 2770 | + ) |
2867 | 2771 | ) |
2868 | | - # For some very strange reason, we need to specify -lm twice |
2869 | | - # to get mkl to link correctly. I have no idea why. |
2870 | | - if any("mkl" in fl for fl in ret): |
2871 | | - ret.extend(["-lm", "-lm"]) |
2872 | | - res = try_blas_flag(ret) |
2873 | | - if res: |
2874 | | - if "mkl" in res: |
2875 | | - check_mkl_openmp() |
2876 | | - return res |
2877 | 2772 |
|
2878 | | - # If we are using conda and can't reuse numpy blas, then doing |
2879 | | - # the fallback and test -lblas could give slow computation, so |
2880 | | - # warn about this. |
2881 | | - for warn in warn_record: |
2882 | | - _logger.warning(warn) |
2883 | | - del warn_record |
2884 | | - |
2885 | | - # Some environment don't have the lib dir in LD_LIBRARY_PATH. |
2886 | | - # So add it. |
2887 | | - ret.extend(["-Wl,-rpath," + l for l in blas_info.get("library_dirs", [])]) |
2888 | | - res = try_blas_flag(ret) |
| 2773 | + flags = ( |
| 2774 | + libdir_ldflags |
| 2775 | + + [f"-l{lib_name}" for _, lib_name in found_libs] |
| 2776 | + + extra_compile_flags |
| 2777 | + ) |
| 2778 | + res = try_blas_flag(flags) |
2889 | 2779 | if res: |
2890 | | - if "mkl" in res: |
| 2780 | + if any("mkl" in flag for flag in flags): |
2891 | 2781 | check_mkl_openmp() |
2892 | 2782 | return res |
| 2783 | + else: |
| 2784 | + raise RuntimeError(f"Supplied flags {flags} failed to compile") |
2893 | 2785 |
|
2894 | | - # Add sys.prefix/lib to the runtime search path. On |
2895 | | - # non-system installations of Python that use the |
2896 | | - # system linker, this is generally necessary. |
2897 | | - if sys.platform in ("linux", "darwin"): |
2898 | | - lib_path = os.path.join(sys.prefix, "lib") |
2899 | | - ret.append("-Wl,-rpath," + lib_path) |
2900 | | - res = try_blas_flag(ret) |
2901 | | - if res: |
2902 | | - if "mkl" in res: |
2903 | | - check_mkl_openmp() |
2904 | | - return res |
2905 | | - |
2906 | | - except KeyError: |
| 2786 | + _std_lib_dirs = std_lib_dirs() |
| 2787 | + if len(_std_lib_dirs) > 0: |
| 2788 | + rpath = _std_lib_dirs[0] |
| 2789 | + else: |
| 2790 | + rpath = None |
| 2791 | + |
| 2792 | + cxx_library_dirs = get_cxx_library_dirs() |
| 2793 | + searched_library_dirs = cxx_library_dirs + _std_lib_dirs |
| 2794 | + all_libs = [ |
| 2795 | + l |
| 2796 | + for path in [ |
| 2797 | + pathlib.Path(library_dir) |
| 2798 | + for library_dir in searched_library_dirs |
| 2799 | + if pathlib.Path(library_dir).exists() |
| 2800 | + ] |
| 2801 | + for l in path.iterdir() |
| 2802 | + if l.suffix in {".so", ".dll", ".dylib"} |
| 2803 | + ] |
| 2804 | + |
| 2805 | + if rpath is not None: |
| 2806 | + maybe_add_to_os_environ_pathlist("PATH", rpath) |
| 2807 | + try: |
| 2808 | + # 1. Try to use MKL with INTEL OpenMP threading |
| 2809 | + return check_libs( |
| 2810 | + all_libs, |
| 2811 | + required_libs=[ |
| 2812 | + "mkl_core", |
| 2813 | + "mkl_rt", |
| 2814 | + "mkl_intel_thread", |
| 2815 | + "iomp5", |
| 2816 | + "pthread", |
| 2817 | + ], |
| 2818 | + extra_compile_flags=[f"-Wl,-rpath,{rpath}"] if rpath is not None else [], |
| 2819 | + cxx_library_dirs=cxx_library_dirs, |
| 2820 | + ) |
| 2821 | + except Exception: |
2907 | 2822 | pass |
2908 | | - |
2909 | | - # Even if we could not detect what was used for numpy, or if these |
2910 | | - # libraries are not found, most Linux systems have a libblas.so |
2911 | | - # readily available. We try to see if that's the case, rather |
2912 | | - # than disable blas. To test it correctly, we must load a program. |
2913 | | - # Otherwise, there could be problem in the LD_LIBRARY_PATH. |
2914 | | - return try_blas_flag(["-lblas"]) |
| 2823 | + try: |
| 2824 | + # 2. Try to use MKL with GNU OpenMP threading |
| 2825 | + return check_libs( |
| 2826 | + all_libs, |
| 2827 | + required_libs=["mkl_core", "mkl_rt", "mkl_gnu_thread", "gomp", "pthread"], |
| 2828 | + extra_compile_flags=[f"-Wl,-rpath,{rpath}"] if rpath is not None else [], |
| 2829 | + cxx_library_dirs=cxx_library_dirs, |
| 2830 | + ) |
| 2831 | + except Exception: |
| 2832 | + pass |
| 2833 | + try: |
| 2834 | + # 3. Try to use LAPACK + BLAS |
| 2835 | + return check_libs( |
| 2836 | + all_libs, |
| 2837 | + required_libs=["lapack", "blas", "cblas", "m"], |
| 2838 | + extra_compile_flags=[f"-Wl,-rpath,{rpath}"] if rpath is not None else [], |
| 2839 | + cxx_library_dirs=cxx_library_dirs, |
| 2840 | + ) |
| 2841 | + except Exception: |
| 2842 | + pass |
| 2843 | + try: |
| 2844 | + # 4. Try to use BLAS alone |
| 2845 | + return check_libs( |
| 2846 | + all_libs, |
| 2847 | + required_libs=["blas", "cblas"], |
| 2848 | + extra_compile_flags=[f"-Wl,-rpath,{rpath}"] if rpath is not None else [], |
| 2849 | + cxx_library_dirs=cxx_library_dirs, |
| 2850 | + ) |
| 2851 | + except Exception: |
| 2852 | + pass |
| 2853 | + try: |
| 2854 | + # 5. Try to use openblas |
| 2855 | + return check_libs( |
| 2856 | + all_libs, |
| 2857 | + required_libs=["openblas", "gfortran", "gomp", "m"], |
| 2858 | + extra_compile_flags=["-fopenmp", f"-Wl,-rpath,{rpath}"] |
| 2859 | + if rpath is not None |
| 2860 | + else ["-fopenmp"], |
| 2861 | + cxx_library_dirs=cxx_library_dirs, |
| 2862 | + ) |
| 2863 | + except Exception: |
| 2864 | + pass |
| 2865 | + return "" |
2915 | 2866 |
|
2916 | 2867 |
|
2917 | 2868 | def add_blas_configvars(): |
|
0 commit comments