Skip to content

Commit a146ba6

Browse files
authored
Merge branch 'main' into build-with-meson
2 parents d8e612c + 605753f commit a146ba6

14 files changed

Lines changed: 600 additions & 2 deletions

.github/workflows/openssf-scorecard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,6 @@ jobs:
7171

7272
# Upload the results to GitHub's code scanning dashboard.
7373
- name: "Upload to code-scanning"
74-
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
74+
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
7575
with:
7676
sarif_file: results.sarif

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ repos:
4646
- tomli
4747

4848
- repo: https://github.com/psf/black
49-
rev: 26.3.1
49+
rev: 26.5.1
5050
hooks:
5151
- id: black
5252
exclude: "_vendored/conv_template.py"

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ Where `<numpy_version>` should be the latest version from https://software.repos
3434

3535
---
3636

37+
# Patching Mechanisms
38+
39+
`mkl_umath` provides a convenient programmatic patch method to enable MKL-accelerated umath operations in NumPy.
40+
41+
## Programmatic Quickstart
42+
43+
```python
44+
import mkl_umath
45+
import numpy
46+
47+
mkl_umath.patch_numpy_umath()
48+
print(mkl_umath.is_patched())
49+
# run your accelerated numpy workloads here!
50+
mkl_umath.restore_numpy_umath()
51+
```
52+
53+
```python
54+
import mkl_umath
55+
import numpy
56+
with mkl_umath.mkl_umath():
57+
# run your accelerated workloads here!
58+
pass
59+
```
60+
---
61+
3762
## Building
3863

3964
Intel(R) C compiler and Intel(R) OneAPI Math Kernel Library (OneMKL) are required to build `mkl_umath` from source.

benchmarks/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# mkl_umath ASV Benchmarks
2+
3+
Performance benchmarks for [mkl_umath](https://github.com/IntelPython/mkl_umath) using [Airspeed Velocity (ASV)](https://asv.readthedocs.io/en/stable/).
4+
5+
The `npbench/` suite uses kernels from [npbench](https://github.com/spcl/npbench) to measure end-to-end impact of MKL ufunc acceleration in realistic workloads.
6+
7+
### Coverage
8+
9+
| File | Ufuncs | Dtypes | Sizes/Presets |
10+
|------|--------|--------|---------------|
11+
| `micro/bench_micro.py` | 25 unary (`exp`, `log`, `sin`, `cos`, `sqrt`, `cbrt`, etc.) + `arctan2`, `power` | float32, float64 | 10k, 100k, 1M |
12+
| `npbench/bench_softmax.py` | `exp`, `max`, `sum` | float32 | M (32x8x256x256), L (64x16x448x448) |
13+
| `npbench/bench_arc_distance.py` | `sin`, `cos`, `arctan2`, `sqrt` | float64 | M (1M), L (10M) |
14+
| `npbench/bench_go_fast.py` | `tanh` | float64 | M (6k x 6k), L (20k x 20k) |
15+
| `npbench/bench_mandelbrot.py` | `abs`, `multiply`, `add` | complex128 | M (250/500), L (833/1000) |
16+
17+
## Running Benchmarks
18+
19+
Prerequisites:
20+
21+
```bash
22+
pip install asv psutil
23+
```
24+
25+
Run benchmarks against the current commit:
26+
27+
```bash
28+
asv run --python=same --quick HEAD^!
29+
```
30+
31+
Compare two commits:
32+
33+
```bash
34+
asv continuous --python=same HEAD~1 HEAD
35+
```
36+
37+
View results in a browser:
38+
39+
```bash
40+
asv publish
41+
asv preview
42+
```
43+
44+
## Threading
45+
46+
Set `MKL_NUM_THREADS` to control the thread count used by MKL:
47+
48+
```bash
49+
MKL_NUM_THREADS=8 asv run --python=same --quick HEAD^!
50+
```
51+
52+
If `MKL_NUM_THREADS` is not set, `__init__.py` applies a default: **4** threads when the machine has 4 or more physical cores, or **1** (single-threaded) otherwise. This keeps results comparable across CI machines in the shared pool regardless of their total core count. Physical cores are detected via `psutil.cpu_count(logical=False)` (hyperthreads excluded per MKL recommendation).

benchmarks/asv.conf.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": 1,
3+
"project": "mkl_umath",
4+
"project_url": "https://github.com/IntelPython/mkl_umath",
5+
"repo": "..",
6+
"branches": [
7+
"main"
8+
],
9+
"environment_type": "existing",
10+
"benchmark_dir": "benchmarks",
11+
"env_dir": ".asv/env",
12+
"results_dir": ".asv/results",
13+
"html_dir": ".asv/html",
14+
"show_commit_url": "https://github.com/IntelPython/mkl_umath/commit/",
15+
"build_cache_size": 2,
16+
"default_benchmark_timeout": 1500,
17+
"regressions_thresholds": {
18+
".*": 0.2
19+
}
20+
}

benchmarks/benchmarks/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""ASV benchmarks for mkl_umath"""
2+
3+
import os
4+
5+
import psutil
6+
7+
from ._patch_setup import _apply_patches
8+
9+
_MIN_THREADS = 4 # minimum physical cores required for multi-threaded mode
10+
11+
12+
def _physical_cores():
13+
"""Return physical core count; fall back to 1 (conservative)."""
14+
return psutil.cpu_count(logical=False) or 1
15+
16+
17+
def _thread_count():
18+
physical = _physical_cores()
19+
return str(_MIN_THREADS) if physical >= _MIN_THREADS else "1"
20+
21+
22+
_THREADS = os.environ.get("MKL_NUM_THREADS", _thread_count())
23+
os.environ["MKL_NUM_THREADS"] = _THREADS
24+
25+
_apply_patches()
26+
del _apply_patches
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""MKL patch setup — executed once per ASV worker process at import time.
2+
3+
Patches NumPy with the Intel MKL umath implementation.
4+
Hard-fails with a descriptive RuntimeError if mkl_umath is missing or the
5+
patch does not take effect, so benchmarks never silently run on stock NumPy.
6+
"""
7+
8+
_PATCH_MAP = [
9+
("mkl_umath", "patch_numpy_umath"),
10+
]
11+
12+
13+
def _apply_patches():
14+
import numpy as np
15+
16+
patched = {}
17+
18+
for mod_name, patch_fn_name in _PATCH_MAP:
19+
try:
20+
mod = __import__(mod_name)
21+
except ImportError as exc:
22+
raise RuntimeError(
23+
f"[mkl-patch] Cannot import {mod_name}: {exc}\n"
24+
f" Ensure the conda env contains {mod_name} "
25+
f"from the Intel channel.\n"
26+
" Required channels: "
27+
"https://software.repos.intel.com/python/conda"
28+
) from exc
29+
30+
patch_fn = getattr(mod, patch_fn_name, None)
31+
if patch_fn is None:
32+
raise RuntimeError(
33+
f"[mkl-patch] {mod_name} has no {patch_fn_name}(). "
34+
f"Upgrade {mod_name} to a version that exposes "
35+
"the stock-numpy patch API."
36+
)
37+
38+
try:
39+
patch_fn()
40+
except Exception as exc:
41+
raise RuntimeError(
42+
f"[mkl-patch] {mod_name}.{patch_fn_name}() raised: {exc!r}"
43+
) from exc
44+
45+
is_patched_fn = getattr(mod, "is_patched", None)
46+
if callable(is_patched_fn) and not is_patched_fn():
47+
raise RuntimeError(
48+
f"[mkl-patch] {mod_name}.is_patched() returned False "
49+
"after patching. NumPy may have been imported before "
50+
"patching in a conflicting state."
51+
)
52+
53+
patched[mod_name] = mod
54+
55+
_attr_checks = {
56+
"mkl_umath": lambda: np.exp.__module__,
57+
}
58+
for mod_name in patched:
59+
try:
60+
attr = _attr_checks[mod_name]()
61+
except Exception:
62+
attr = "unknown"
63+
print(f"[mkl-patch] {mod_name}: numpy dispatch -> {attr}")
64+
65+
print("[mkl-patch] ALL OK -- mkl_umath active")

benchmarks/benchmarks/micro/__init__.py

Whitespace-only changes.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Micro-benchmarks for mkl_umath unary ufuncs.
2+
3+
Times each ufunc over a Cartesian product of
4+
dtype in [float32, float64]
5+
size in [10_000, 100_000, 1_000_000]
6+
7+
Arrays are pre-allocated in setup() and reused across timing calls.
8+
Patching is applied once at package import via benchmarks._patch_setup.
9+
"""
10+
11+
import numpy as np
12+
13+
_UFUNC_CONFIGS = {
14+
"exp": {"func": np.exp, "low": -10.0, "high": 10.0},
15+
"exp2": {"func": np.exp2, "low": -10.0, "high": 10.0},
16+
"expm1": {"func": np.expm1, "low": -10.0, "high": 10.0},
17+
"log": {"func": np.log, "low": 1e-3, "high": 1e3},
18+
"log2": {"func": np.log2, "low": 1e-3, "high": 1e3},
19+
"log10": {"func": np.log10, "low": 1e-3, "high": 1e3},
20+
"log1p": {"func": np.log1p, "low": 0.0, "high": 10.0},
21+
"sin": {"func": np.sin, "low": -np.pi, "high": np.pi},
22+
"cos": {"func": np.cos, "low": -np.pi, "high": np.pi},
23+
"tan": {"func": np.tan, "low": -1.4, "high": 1.4},
24+
"arcsin": {"func": np.arcsin, "low": -1.0, "high": 1.0},
25+
"arccos": {"func": np.arccos, "low": -1.0, "high": 1.0},
26+
"arctan": {"func": np.arctan, "low": -10.0, "high": 10.0},
27+
"sinh": {"func": np.sinh, "low": -5.0, "high": 5.0},
28+
"cosh": {"func": np.cosh, "low": -5.0, "high": 5.0},
29+
"tanh": {"func": np.tanh, "low": -5.0, "high": 5.0},
30+
"arcsinh": {"func": np.arcsinh, "low": -10.0, "high": 10.0},
31+
"arccosh": {"func": np.arccosh, "low": 1.0, "high": 100.0},
32+
"arctanh": {"func": np.arctanh, "low": -0.99, "high": 0.99},
33+
"sqrt": {"func": np.sqrt, "low": 0.0, "high": 100.0},
34+
"cbrt": {"func": np.cbrt, "low": -100.0, "high": 100.0},
35+
"square": {"func": np.square, "low": -10.0, "high": 10.0},
36+
"fabs": {"func": np.fabs, "low": -100.0, "high": 100.0},
37+
"absolute": {"func": np.absolute, "low": -100.0, "high": 100.0},
38+
"reciprocal": {"func": np.reciprocal, "low": 0.01, "high": 100.0},
39+
}
40+
41+
42+
class BenchMicro:
43+
params = (
44+
sorted(_UFUNC_CONFIGS.keys()),
45+
["float32", "float64"],
46+
[10_000, 100_000, 1_000_000],
47+
)
48+
param_names = ["ufunc", "dtype", "size"]
49+
50+
def setup(self, ufunc, dtype, size):
51+
cfg = _UFUNC_CONFIGS[ufunc]
52+
rng = np.random.default_rng(42)
53+
self.x = rng.uniform(cfg["low"], cfg["high"], size).astype(dtype)
54+
self._func = cfg["func"]
55+
self._func(self.x)
56+
57+
def time_micro(self, ufunc, dtype, size):
58+
self._func(self.x)
59+
60+
61+
class BenchArctan2:
62+
"""Binary ufunc arctan2"""
63+
64+
params = (["float32", "float64"], [10_000, 100_000, 1_000_000])
65+
param_names = ["dtype", "size"]
66+
67+
def setup(self, dtype, size):
68+
rng = np.random.default_rng(42)
69+
self.y = rng.uniform(-1.0, 1.0, size).astype(dtype)
70+
self.x = rng.uniform(-1.0, 1.0, size).astype(dtype)
71+
np.arctan2(self.y, self.x)
72+
73+
def time_arctan2(self, dtype, size):
74+
np.arctan2(self.y, self.x)
75+
76+
77+
class BenchPower:
78+
"""Binary ufunc power (arbitrary exponent via MKL vdPow)"""
79+
80+
params = (["float32", "float64"], [10_000, 100_000, 1_000_000])
81+
param_names = ["dtype", "size"]
82+
83+
def setup(self, dtype, size):
84+
rng = np.random.default_rng(42)
85+
self.base = rng.uniform(0.1, 10.0, size).astype(dtype)
86+
self.exp = rng.uniform(0.5, 3.0, size).astype(dtype)
87+
np.power(self.base, self.exp)
88+
89+
def time_power(self, dtype, size):
90+
np.power(self.base, self.exp)

benchmarks/benchmarks/npbench/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)