Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [dev] - YYYY-MM-DD

### Added
* Added CLI for persistent and ephemeral NumPy FFT patching: `python -m mkl_fft patch install/uninstall/status` for persistent patching across all Python sessions, and `python -m mkl_fft with_patch <command>` for one-shot execution with MKL acceleration

### Changed
* Removed `numpy-base` dependency and `USE_NUMPY_BASE` environment variable from conda recipe [gh-318](https://github.com/IntelPython/mkl_fft/pull/318)
Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,61 @@ numpy.allclose(mkl_res, np_res)
# True
```

---
# Patching Mechanisms

`mkl_fft` provides convenient patch methods to enable MKL-accelerated FFT operations in NumPy with or without modifying your code.

## CLI Quickstart

### Persistent patch (all Python sessions)

```bash
# Install
python -m mkl_fft --patch install

# Status (exit code: 0 = installed, 1 = not installed)
python -m mkl_fft --patch status

# Remove
python -m mkl_fft --patch uninstall
```

### Verify current FFT backend

```bash
python -c "import numpy; print(f'numpy.fft.fft.__module__: {numpy.fft.fft.__module__}')"
```

### One-shot patch (single command only)

```bash
# Script
python -m mkl_fft --with-numpy-patch my_script.py

# Pytest
python -m mkl_fft --with-numpy-patch -m pytest tests/

# One-liner
python -m mkl_fft --with-numpy-patch -c "import numpy; print(f\"numpy.fft.fft.__module__: {numpy.fft.fft.__module__}\")"

# Non-Python command
python -m mkl_fft --with-numpy-patch -- <command> [args...]
```

## Programmatic Quickstart

```python
import mkl_fft

mkl_fft.patch_numpy_fft()
print(mkl_fft.is_patched())
mkl_fft.restore_numpy_fft()

with mkl_fft.mkl_fft():
pass
```

---
# Building from source

Expand Down
85 changes: 85 additions & 0 deletions mkl_fft/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright (c) 2026, 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.

"""Command-line interface for mkl_fft."""

import argparse
import sys
Comment thread
ndgrigorian marked this conversation as resolved.


def main():
"""Entry point for the CLI."""
parser = argparse.ArgumentParser(
prog="python -m mkl_fft",
description="MKL-accelerated FFT for NumPy",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose output"
)
parser.add_argument(
"--patch",
choices=["install", "uninstall", "status"],
help="Manage persistent NumPy FFT patching",
)
parser.add_argument(
"--with-numpy-patch",
dest="with_numpy_patch",
nargs=argparse.REMAINDER,
help="Run command with temporary NumPy FFT patch",
)

args = parser.parse_args()

if args.patch:
from mkl_fft.patch import (
PatchOperationError,
check_status,
install_patch,
uninstall_patch,
)

try:
if args.patch == "install":
install_patch(verbose=args.verbose)
elif args.patch == "uninstall":
uninstall_patch(verbose=args.verbose)
elif args.patch == "status":
sys.exit(0 if check_status(verbose=args.verbose) else 1)
except PatchOperationError as exc:
print(exc, file=sys.stderr)
sys.exit(1)

elif args.with_numpy_patch is not None:
from mkl_fft.with_patch import run_with_numpy_patch

run_with_numpy_patch(args.with_numpy_patch)

else:
parser.print_help()
sys.exit(1)


if __name__ == "__main__":
main()
33 changes: 33 additions & 0 deletions mkl_fft/_patch_startup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright (c) 2026, 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.

"""Helper module for .pth-based persistent patching with error handling."""

try:
import mkl_fft

mkl_fft.patch_numpy_fft()
except Exception:
pass
142 changes: 142 additions & 0 deletions mkl_fft/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright (c) 2026, 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
# 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.

"""Persistent patch management for NumPy FFT submodule."""

import site
import sys
import warnings
from pathlib import Path


class PatchOperationError(RuntimeError):
"""Raised when a persistent patch operation cannot be completed."""


def get_pth_path():
"""Get the path to mkl_fft_patch.pth in the appropriate site-packages."""
site_packages = site.getsitepackages()
if site_packages:
target_site = site_packages[0]
else:
target_site = site.getusersitepackages()
return Path(target_site) / "mkl_fft_patch.pth"


PTH_CONTENT = """import mkl_fft._patch_startup"""


def install_patch(verbose=False):
"""Install persistent NumPy FFT patch using .pth file."""
pth_path = get_pth_path()

if pth_path.exists():
if verbose:
warnings.warn(
f"Persistent patch already installed at {pth_path}",
UserWarning,
stacklevel=2,
)
return

try:
pth_path.parent.mkdir(parents=True, exist_ok=True)
pth_path.write_text(PTH_CONTENT)
if verbose:
print(f"Persistent patch installed at {pth_path}")
print()
print(
"NumPy FFT will now use MKL-accelerated implementations in all"
)
print("Python sessions. To disable, run:")
print(" python -m mkl_fft patch uninstall")
except OSError as e:
raise PatchOperationError(
f"Error installing patch at {pth_path}: {e}\n\n"
"You may need to run with appropriate permissions or install to "
"a user site-packages directory."
) from e


def uninstall_patch(verbose=False):
"""Uninstall persistent NumPy FFT patch."""
pth_path = get_pth_path()

if not pth_path.exists():
if verbose:
print("No persistent patch found.")
return

try:
pth_path.unlink()
if verbose:
print(f"Persistent patch removed from {pth_path}")
print()
print("NumPy FFT will now use the default implementations.")
except OSError as e:
raise PatchOperationError(
f"Error removing patch at {pth_path}: {e}"
) from e


def check_status(verbose=False):
"""Check if persistent patch is installed."""
pth_path = get_pth_path()

if pth_path.exists():
if verbose:
print(f"Persistent patch is installed at {pth_path}")
print()
print(
"NumPy FFT is configured to use MKL-accelerated implementations."
)
return True
else:
if verbose:
print("No persistent patch installed")
print()
print("To enable MKL-accelerated NumPy FFT globally, run:")
print(" python -m mkl_fft patch install")
return False


if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python -m mkl_fft.patch <install|uninstall|status>")
sys.exit(1)

command = sys.argv[1]
try:
if command == "install":
install_patch(verbose=True)
elif command == "uninstall":
uninstall_patch(verbose=True)
elif command == "status":
sys.exit(0 if check_status(verbose=True) else 1)
else:
print(f"Unknown command: {command}")
sys.exit(1)
except PatchOperationError as exc:
print(exc, file=sys.stderr)
sys.exit(1)
Loading
Loading