Skip to content

Commit 2265f74

Browse files
committed
task: module CLI patching methods
1 parent 52ae701 commit 2265f74

5 files changed

Lines changed: 481 additions & 0 deletions

File tree

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,84 @@ numpy.allclose(mkl_res, np_res)
8181
# True
8282
```
8383

84+
---
85+
# Patching Mechanisms
86+
87+
The `mkl_fft` provides convenient ways to enable MKL-accelerated FFT operations in NumPy with or without modifying your code. It supports both persistent patching (applies to all Python sessions) and one-shot execution (applies only to a single command). It also supports Python functions and context managers that do the same.
88+
89+
## Persistent Patching
90+
91+
### Install Persistent Patch
92+
93+
```bash
94+
python -m mkl_fft patch install
95+
```
96+
97+
### Check Patch Status
98+
99+
```bash
100+
python -m mkl_fft patch status
101+
```
102+
103+
Checks whether the persistent patch is currently installed. Returns exit code 0 if installed, 1 if not installed.
104+
105+
### Uninstall Persistent Patch
106+
107+
```bash
108+
python -m mkl_fft patch uninstall
109+
```
110+
111+
Removes the persistent patch file, restoring NumPy to its default FFT implementation.
112+
113+
## One-Shot Execution
114+
115+
```bash
116+
python -m mkl_fft with_patch <command> [args...]
117+
```
118+
119+
Runs a single command with MKL-accelerated FFT enabled. The patch is only active for that specific execution and does not persist.
120+
121+
**Examples:**
122+
123+
```bash
124+
# Run a Python script with MKL acceleration
125+
python -m mkl_fft with_patch python my_script.py
126+
127+
# Run tests with MKL acceleration
128+
python -m mkl_fft with_patch python -m pytest tests/
129+
130+
# Run a Python one-liner
131+
python -m mkl_fft with_patch python -c "import numpy; print(numpy.fft.fft.__module__)"
132+
133+
# Run benchmarks with MKL acceleration
134+
python -m mkl_fft with_patch python run_benchmarks.py
135+
```
136+
137+
## Programmatic Usage
138+
139+
You can also patch NumPy programmatically in your Python code:
140+
141+
```python
142+
import mkl_fft
143+
144+
# Check if currently patched
145+
if mkl_fft.is_patched():
146+
print("NumPy FFT is using MKL")
147+
148+
# Enable patching globally
149+
mkl_fft.patch_numpy_fft()
150+
151+
# Disable patching
152+
mkl_fft.restore_numpy_fft()
153+
154+
# Use as context manager (recommended for temporary patching)
155+
with mkl_fft.mkl_fft():
156+
# NumPy FFT uses MKL inside this block
157+
import numpy as np
158+
result = np.fft.fft(data)
159+
# NumPy FFT restored outside the block
160+
```
161+
84162
---
85163
# Building from source
86164

mkl_fft/__main__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) 2017, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
"""Command-line interface for mkl_fft."""
27+
28+
import sys
29+
30+
31+
def main_impl():
32+
if len(sys.argv) < 2:
33+
print("Usage: python -m mkl_fft <command> [args]")
34+
print()
35+
print("Commands:")
36+
print(" patch install Install persistent NumPy FFT patch")
37+
print(" patch uninstall Uninstall persistent NumPy FFT patch")
38+
print(" patch status Check if persistent patch is installed")
39+
print(" with_patch <cmd> Run command with temporary NumPy FFT patch")
40+
print()
41+
print("Examples:")
42+
print(" python -m mkl_fft patch install")
43+
print(" python -m mkl_fft with_patch python script.py")
44+
sys.exit(1)
45+
46+
command = sys.argv[1]
47+
48+
if command == "patch":
49+
from mkl_fft.patch import main as patch_main
50+
51+
patch_main(sys.argv[2:])
52+
elif command == "with_patch":
53+
from mkl_fft.with_patch import main as with_patch_main
54+
55+
with_patch_main(sys.argv[2:])
56+
else:
57+
print(f"Unknown command: {command}")
58+
sys.exit(1)
59+
60+
61+
def main():
62+
"""Entry point that avoids importing mkl_fft package."""
63+
try:
64+
main_impl()
65+
except Exception:
66+
main_impl()
67+
68+
69+
if __name__ == "__main__":
70+
main_impl()

mkl_fft/patch.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright (c) 2017, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
25+
"""Persistent patch management for NumPy FFT submodule."""
26+
27+
import argparse
28+
import os
29+
import site
30+
import sys
31+
from pathlib import Path
32+
33+
34+
def get_sitecustomize_path():
35+
"""Get the path to sitecustomize.py in the user site-packages."""
36+
user_site = site.getusersitepackages()
37+
if not user_site:
38+
user_site = site.getsitepackages()[0]
39+
return Path(user_site) / "sitecustomize.py"
40+
41+
42+
def get_pth_path():
43+
"""Get the path to mkl_fft.pth in the appropriate site-packages."""
44+
site_packages = site.getsitepackages()
45+
if site_packages:
46+
target_site = site_packages[0]
47+
else:
48+
target_site = site.getusersitepackages()
49+
return Path(target_site) / "mkl_fft_patch.pth"
50+
51+
52+
PATCH_CODE = """# mkl_fft persistent patch - auto-generated
53+
try:
54+
import mkl_fft
55+
mkl_fft.patch_numpy_fft()
56+
except Exception:
57+
pass
58+
"""
59+
60+
PTH_CONTENT = """import mkl_fft; mkl_fft.patch_numpy_fft()"""
61+
62+
63+
def install_patch():
64+
"""Install persistent NumPy FFT patch using .pth file."""
65+
pth_path = get_pth_path()
66+
67+
if pth_path.exists():
68+
print(f"Persistent patch already installed at {pth_path}")
69+
return
70+
71+
try:
72+
pth_path.parent.mkdir(parents=True, exist_ok=True)
73+
pth_path.write_text(PTH_CONTENT)
74+
print(f"✓ Persistent patch installed at {pth_path}")
75+
print()
76+
print("NumPy FFT will now use MKL-accelerated implementations in all")
77+
print("Python sessions. To disable, run:")
78+
print(" python -m mkl_fft patch uninstall")
79+
except (IOError, OSError) as e:
80+
print(f"Error installing patch: {e}")
81+
print()
82+
print("You may need to run with appropriate permissions or install to")
83+
print("a user site-packages directory.")
84+
sys.exit(1)
85+
86+
87+
def uninstall_patch():
88+
"""Uninstall persistent NumPy FFT patch."""
89+
pth_path = get_pth_path()
90+
91+
if not pth_path.exists():
92+
print("No persistent patch found.")
93+
return
94+
95+
try:
96+
pth_path.unlink()
97+
print(f"✓ Persistent patch removed from {pth_path}")
98+
print()
99+
print("NumPy FFT will now use the default implementations.")
100+
except (IOError, OSError) as e:
101+
print(f"Error removing patch: {e}")
102+
sys.exit(1)
103+
104+
105+
def check_status():
106+
"""Check if persistent patch is installed."""
107+
pth_path = get_pth_path()
108+
109+
if pth_path.exists():
110+
print(f"✓ Persistent patch is installed at {pth_path}")
111+
print()
112+
print("NumPy FFT is configured to use MKL-accelerated implementations.")
113+
return True
114+
else:
115+
print("✗ No persistent patch installed")
116+
print()
117+
print("To enable MKL-accelerated NumPy FFT globally, run:")
118+
print(" python -m mkl_fft patch install")
119+
return False
120+
121+
122+
def main(args=None):
123+
"""Main entry point for patch command."""
124+
parser = argparse.ArgumentParser(
125+
prog="python -m mkl_fft patch",
126+
description="Manage persistent NumPy FFT patching with MKL acceleration"
127+
)
128+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
129+
130+
subparsers.add_parser("install", help="Install persistent NumPy FFT patch")
131+
subparsers.add_parser("uninstall", help="Uninstall persistent NumPy FFT patch")
132+
subparsers.add_parser("status", help="Check if persistent patch is installed")
133+
134+
parsed_args = parser.parse_args(args)
135+
136+
if not parsed_args.command:
137+
parser.print_help()
138+
sys.exit(1)
139+
140+
if parsed_args.command == "install":
141+
install_patch()
142+
elif parsed_args.command == "uninstall":
143+
uninstall_patch()
144+
elif parsed_args.command == "status":
145+
sys.exit(0 if check_status() else 1)
146+
147+
148+
if __name__ == "__main__":
149+
main()

mkl_fft/tests/test_cli.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright (c) 2017, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
import pytest
27+
28+
from mkl_fft.patch import check_status, install_patch, uninstall_patch
29+
30+
31+
@pytest.fixture
32+
def mock_pth_path(tmp_path, monkeypatch):
33+
"""Mock the .pth file path to use a temporary directory."""
34+
pth_file = tmp_path / "mkl_fft_patch.pth"
35+
36+
def mock_get_pth_path():
37+
return pth_file
38+
39+
monkeypatch.setattr("mkl_fft.patch.get_pth_path", mock_get_pth_path)
40+
return pth_file
41+
42+
43+
def test_install_patch(mock_pth_path, capsys):
44+
"""Test installing persistent patch."""
45+
install_patch()
46+
47+
assert mock_pth_path.exists()
48+
content = mock_pth_path.read_text()
49+
assert "mkl_fft.patch_numpy_fft()" in content
50+
51+
captured = capsys.readouterr()
52+
assert "Persistent patch installed" in captured.out
53+
54+
55+
def test_install_patch_already_installed(mock_pth_path, capsys):
56+
"""Test installing patch when already installed."""
57+
install_patch()
58+
install_patch()
59+
60+
captured = capsys.readouterr()
61+
assert "already installed" in captured.out
62+
63+
64+
def test_uninstall_patch(mock_pth_path, capsys):
65+
"""Test uninstalling persistent patch."""
66+
install_patch()
67+
assert mock_pth_path.exists()
68+
69+
uninstall_patch()
70+
assert not mock_pth_path.exists()
71+
72+
captured = capsys.readouterr()
73+
assert "Persistent patch removed" in captured.out
74+
75+
76+
def test_uninstall_patch_not_installed(mock_pth_path, capsys):
77+
"""Test uninstalling patch when not installed."""
78+
uninstall_patch()
79+
80+
captured = capsys.readouterr()
81+
assert "No persistent patch found" in captured.out
82+
83+
84+
def test_patch_status_check_function(mock_pth_path):
85+
"""Test check_status function return values."""
86+
assert not check_status()
87+
88+
install_patch()
89+
assert check_status()
90+
91+
uninstall_patch()
92+
assert not check_status()

0 commit comments

Comments
 (0)