Skip to content

Commit 41be8eb

Browse files
Merge remote-tracking branch 'origin/master' into fix_queue_leak
2 parents b1c3e71 + ce505f8 commit 41be8eb

28 files changed

Lines changed: 2053 additions & 641 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ build_cython
44
cython_debug
55
*.egg-info
66

7+
# Airspeed Velocity (asv) benchmark environments, results and html
8+
benchmarks/.asv/
9+
710
# Byte-compiled / optimized / DLL files
811
__pycache__/
912

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This release is compatible with NumPy 2.5.
4545
* Bumped the default minimum required DPC++ compiler version to `2026.1.1` and migrated to the OpenCL ICD loader from the conda-forge `ocl-icd-system` (Linux) and `khronos-opencl-icd-loader` (Windows) packages [#2905](https://github.com/IntelPython/dpnp/pull/2905)
4646
* Linked the `dpnp_backend_c` library against only the MKL SYCL domains it uses (`BLAS`, `RNG`, `VM`) [#3012](https://github.com/IntelPython/dpnp/pull/3012)
4747
* `dpnp` uses pybind11 3.1.0 [#3015](https://github.com/IntelPython/dpnp/pull/3015)
48+
* Reworked the ASV benchmarks and added end-to-end workload benchmarks derived from dpBench [#2996](https://github.com/IntelPython/dpnp/pull/2996)
4849

4950
### Deprecated
5051

@@ -88,6 +89,7 @@ This release is compatible with NumPy 2.5.
8889
* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024)
8990
* Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3037](https://github.com/IntelPython/dpnp/pull/3037)
9091
* Fixed `dpnp.all` and `dpnp.any` aborting when reducing over an empty axis (e.g. an array with a zero-length dimension) [#3021](https://github.com/IntelPython/dpnp/pull/3021)
92+
* Released the GIL before the blocking OneMKL DFT calls in the FFT extension [#3040](https://github.com/IntelPython/dpnp/pull/3040)
9193
* Fixed a per-call `sycl::queue` leak in `usm_ndarray::get_queue()`/`get_device()` [#3042](https://github.com/IntelPython/dpnp/pull/3042)
9294

9395
### Security

benchmarks/README.md

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# dpnp ASV Benchmarks
2+
3+
Performance benchmarks for [dpnp](https://github.com/IntelPython/dpnp) using
4+
[Airspeed Velocity (ASV)](https://asv.readthedocs.io/en/stable/).
5+
6+
## Coverage
7+
8+
| File | API | Benchmarks | Params | Sizes |
9+
|------|-----|------------|--------|-------|
10+
| `bench_dpbench.py` | `dpnp` (end-to-end workloads) | `BlackScholes`, `L2Norm`, `PairwiseDistance`, `Rambo`, `Gpairs` | `preset`, `precision` | dpBench presets `S`, `M16Gb`, `M`, `L` |
11+
| `bench_elementwise.py` | `dpnp` vs `numpy` | `Unary` (31 ufuncs), `Binary` (7 ufuncs) | `executor`, `ufunc`, `size`, `dtype` (float only) | 2^16, 2^20, 2^24 |
12+
| `bench_linalg.py` | `dpnp` vs `numpy` (`dot`, `matmul`, `inner`, `einsum`; contiguous and transposed) | `MatMul` | `executor`, `order`, `dtype` (float and int) | 16 to 1024 square |
13+
| `bench_linalg.py` | `dpnp.linalg` vs `numpy.linalg` (`det`, `norm`, `solve`, `svd`) | `Linalg` | `executor`, `order`, `dtype` (float only) | 16 to 1024 square |
14+
| `bench_random.py` | `dpnp.random` vs `numpy.random` | `Sample` (`random_sample`, `standard_normal`) | `executor`, `size` | 2^16, 2^20, 2^24 |
15+
| `bench_random.py` | `dpnp.random.RandomState` vs `numpy.random.default_rng` | `TypedSample` (uniform, normal) | `executor`, `size`, `dtype` (float only) | 2^16, 2^20, 2^24 |
16+
17+
### dpBench workloads
18+
19+
`bench_dpbench.py` runs a set of dpnp workloads derived from
20+
[dpBench](https://github.com/IntelPython/dpbench), which live in
21+
`benchmarks/benchmarks/dpbench/workloads`. They measure the end-to-end time of a
22+
whole workload rather than of an individual API call.
23+
24+
| Workload | Domain |
25+
| ------------------- | ------------------ |
26+
| `black_scholes` | Finance |
27+
| `l2_norm` | Distance Compute |
28+
| `pairwise_distance` | Distance Compute |
29+
| `rambo` | Particle Physics |
30+
| `gpairs` | Astrophysics |
31+
32+
Host input data is generated and copied to the device the way dpBench does, and
33+
each kernel ends with `dpnp.synchronize_array_data`, so a single call blocks
34+
until the device work has finished. dpBench is not a dependency. See
35+
[`benchmarks/dpbench/README.md`](benchmarks/dpbench/README.md) for the
36+
source-to-module mapping and the intended differences.
37+
38+
## Device and precision
39+
40+
dpnp allocates on the default SYCL device. Use `ONEAPI_DEVICE_SELECTOR` to
41+
target a specific one:
42+
43+
```bash
44+
ONEAPI_DEVICE_SELECTOR=level_zero:gpu asv run \
45+
--python=same \
46+
--launch-method spawn \
47+
--quick
48+
```
49+
50+
**The parameter matrix is the same on every machine.** All four dpBench presets
51+
are declared statically, so a given benchmark has the same parameter set
52+
everywhere and results are comparable across devices and across the CI pool.
53+
What varies per device is which of those points *run*: `setup` calls
54+
`_dpbench_runner.preset_fits` and raises `SkipNotImplemented` for any preset
55+
whose estimated peak footprint -- the workload's `peak_elements` at the point's
56+
own precision -- exceeds **0.25** of the device's `global_mem_size`. So a large
57+
discrete GPU exercises the bigger problem sizes automatically while a small iGPU
58+
reports `S` and skips the rest, and a skipped point stays visible as a skip
59+
rather than vanishing from the matrix. Since `single` needs half the memory of
60+
`double`, it reaches one preset further on a given device.
61+
62+
The cheapest preset always runs. If even that does not fit, it is attempted
63+
anyway so the failure is a loud allocation error rather than silence.
64+
65+
Note that dpBench's preset names are not ordered by size: `M16Gb` is *smaller*
66+
than `M` for every workload except `rambo`, where it is larger and equal to `L`.
67+
Anything that needs the cheapest preset sorts explicitly rather than relying on
68+
declaration order.
69+
70+
**Both precisions are benchmarked.** Devices without fp64 support (common on
71+
iGPUs) skip the `double` points via `SkipNotImplemented` rather than failing, so
72+
such a device still produces `single` results. The `float64` points of
73+
`bench_elementwise.py` and `bench_linalg.py` skip the same way for the `dpnp`
74+
executor; the `numpy` executor is unaffected. dpBench's own configs request
75+
`double` throughout, and that value is kept in each workload's `PRECISION` for
76+
reference.
77+
78+
`bench_random.py`'s `Sample` skips its `dpnp` points without fp64: those
79+
functions take no `dtype`, so dpnp would return the device's default float
80+
against NumPy's `float64`. `TypedSample` covers the same two distributions
81+
through dpnp's `RandomState` and NumPy's `default_rng`, which do take a `dtype`,
82+
so `float32` is still compared there.
83+
84+
No benchmark module opens a SYCL queue at import time, so benchmark discovery
85+
and `asv check` work on a machine with no usable device; only `setup` needs one.
86+
87+
One caveat on comparability: ASV keys results by machine, commit and
88+
environment, not by device. Benchmarking two devices on the same host therefore
89+
overwrites one set of results with the other. Give each device its own machine
90+
name when you do that:
91+
92+
```bash
93+
ONEAPI_DEVICE_SELECTOR=level_zero:gpu asv run --python=same \
94+
--launch-method spawn --machine "$(hostname)-gpu"
95+
```
96+
97+
## Notes on Measurement
98+
99+
### Process launch method
100+
101+
**Always pass `--launch-method spawn`.** ASV defaults to a forkserver, which
102+
`fork()`s a process that has already initialized a SYCL runtime; the SYCL
103+
runtime is multi-threaded and not fork-safe, so benchmarks may hang until
104+
`default_benchmark_timeout` expires (reported as `failed`) or fail with
105+
`USM Allocation` errors on `level_zero` devices. `spawn` starts a fresh
106+
interpreter per benchmark and avoids this entirely.
107+
108+
### Asynchronous execution
109+
110+
**Every timed body that runs dpnp work must block on it.** dpnp enqueues to a
111+
SYCL queue and returns before the kernel has run, so a body that does not block
112+
measures submission rather than execution. On a CPU device a 1024x1024 float32
113+
`dot` measured **0.3 ms** unsynchronized against **18 ms** synchronized -- which
114+
would have reported dpnp as far faster than NumPy's **10 ms** on work where it is
115+
in fact 1.7x slower.
116+
117+
The dpBench workloads each end with `dpnp.synchronize_array_data`, and the
118+
comparison suites obtain a synchronizer from `_utils.make_synchronizer` in
119+
`setup` and pass every result through it (`self.sync(...)`). For the `numpy`
120+
executor the synchronizer does nothing.
121+
122+
### First-call costs
123+
124+
The first call on a fresh queue pays SYCL kernel/JIT and allocator warmup.
125+
`WorkloadRunner.setup` therefore runs each workload once before ASV starts
126+
timing it, so the dpBench suite is warmed explicitly. The `bench_elementwise.py`,
127+
`bench_linalg.py` and `bench_random.py` suites each run their operation once in
128+
`setup` as well, so a `--quick` measurement is not dominated by one-time cost.
129+
130+
`--quick` still takes a single sample, so use it to check that benchmarks run
131+
rather than to compare them: before the explicit warmups a `--quick`
132+
`dot(a, a.T)` measured 2.6x its contiguous counterpart, where a repeated
133+
measurement puts it at 0.7x.
134+
135+
### Validation
136+
137+
Each workload ships the NumPy `reference` implementation from dpBench. On the
138+
cheapest preset, `setup` compares the dpnp results for all `OUTPUT_ARGS`
139+
against it with `numpy.testing.assert_allclose`, at a tolerance chosen per
140+
precision (`rtol` 1e-3 / `atol` 1e-4 for `single`, 1e-6 / 1e-9 for `double`).
141+
`atol` carries as much weight as `rtol`, because some outputs pass through
142+
zero, where any absolute error is an enormous relative one. A numerically
143+
wrong kernel therefore fails the benchmark instead of being silently timed,
144+
and validation runs outside the timed region.
145+
146+
Only the cheapest preset is validated: the reference runs on the host and at the
147+
larger presets costs far more than the benchmark it guards -- tens of seconds
148+
for `pairwise_distance` at `M16Gb` -- while checking numerics that do not depend
149+
on the problem size.
150+
151+
### Noise at small presets
152+
153+
The smallest sizes are dominated by per-call dispatch overhead and are
154+
noticeably noisier. On a CPU device the run-to-run spread of the median at `S`
155+
was measured between **2%** and **25%** across workloads, against the **20%**
156+
`regressions_thresholds` in `asv.conf.json`, whereas the larger presets settled
157+
to a few percent. Treat `S` as a smoke-test size only and do not use it for
158+
regression gating; prefer the largest preset the device fits.
159+
160+
## Running Benchmarks
161+
162+
ASV cannot build dpnp -- it is a SYCL/DPC++ extension that requires the Intel
163+
oneAPI compiler and a lengthy build -- so the benchmarks always run against an
164+
**existing environment** that already has dpnp installed. A bare `asv run` is
165+
not supported; always pass `--python=same` or `--environment existing:<python>`.
166+
167+
Create an environment
168+
[following these instructions](https://intelpython.github.io/dpnp/quick_start_guide.html),
169+
then install the benchmarking tooling into it:
170+
171+
```bash
172+
conda install -c conda-forge asv scipy
173+
```
174+
175+
`scipy` is needed because `scipy.special.erf` is used by the NumPy reference
176+
that the `black_scholes` benchmark validates its dpnp results against. It is not
177+
a dpnp runtime dependency, so it also has to be listed in `requirements.txt`,
178+
which is what CI installs into the benchmarking environment. Keep that file and
179+
the `benchmark` extra in `pyproject.toml` in step.
180+
181+
Do **not** use `pip install ".[benchmark]"` for an environment that already has
182+
dpnp: dpnp is a scikit-build project, so pip reinstalls the `dpnp` package
183+
itself and triggers a full oneAPI/DPC++ rebuild of the backend just to pull in
184+
two pure-Python dependencies. The `benchmark` extra in `pyproject.toml` records
185+
those two dependencies for the case where dpnp is being built from source
186+
anyway; note that the usual editable-install invocation passes `--no-deps`, so
187+
it does *not* install them:
188+
189+
```bash
190+
pip install --no-build-isolation --no-deps -e .
191+
conda install -c conda-forge asv scipy
192+
```
193+
194+
All commands below are run from the `benchmarks/` directory, where
195+
`asv.conf.json` lives.
196+
197+
Register the machine once. Without this a non-interactive or CI run aborts with
198+
`No information stored about machine`:
199+
200+
```bash
201+
asv machine --yes
202+
```
203+
204+
Validate the whole suite without running it. This is cheap and catches broken
205+
signatures and import errors; it accepts no `--bench`, so it is all-or-nothing:
206+
207+
```bash
208+
asv check --python=same
209+
```
210+
211+
Smoke-run the benchmarks, optionally scoped with `--bench`:
212+
213+
```bash
214+
asv run --python=same --launch-method spawn --quick --bench bench_dpbench
215+
```
216+
217+
This only *prints* results. Without `--set-commit-hash` ASV discards them, so
218+
`asv compare` and `asv publish` will see nothing.
219+
220+
To record results, assert which revision the installed dpnp corresponds to:
221+
222+
```bash
223+
asv run --python=same --launch-method spawn --set-commit-hash HEAD
224+
```
225+
226+
ASV does not verify that claim -- it is your assertion -- and the
227+
`For dpnp commit ...` progress line prints the branch head rather than the
228+
value passed, so trust the result filename or `asv show`.
229+
230+
Pointing ASV at an interpreter explicitly works too:
231+
232+
```bash
233+
asv run --environment existing:/full/conda/path/envs/dpnp_env/bin/python \
234+
--launch-method spawn
235+
```
236+
237+
### Comparing two revisions
238+
239+
`asv continuous` and any `<commit>` range spec cannot be used here: ASV refuses
240+
a range spec when it cannot install the project into the environment. Compare
241+
two recorded runs instead. Rebuild dpnp in the same environment between them,
242+
and omit `--quick` so the statistics path engages:
243+
244+
```bash
245+
# against the old build
246+
asv run --python=same --launch-method spawn --set-commit-hash <old-rev>
247+
# rebuild/reinstall dpnp, then
248+
asv run --python=same --launch-method spawn --set-commit-hash <new-rev>
249+
asv compare <old-rev> <new-rev>
250+
```
251+
252+
View recorded results in a browser:
253+
254+
```bash
255+
asv publish
256+
asv preview
257+
```
258+
259+
The published dashboard only covers the branches listed in `asv.conf.json`
260+
(`master` and `dev-milestone`). Results recorded for a commit on any other
261+
branch are dropped with `Couldn't find <hash> in branches (...)`, so use
262+
`asv compare` for feature-branch and PR work -- it does not consult `branches`.
263+
264+
## Writing new benchmarks
265+
266+
Read ASV's guidelines for writing benchmarks
267+
[here](https://asv.readthedocs.io/en/stable/writing_benchmarks.html).
268+
269+
Parameter axes shared by two or more `bench_*` modules live in `_utils.py`;
270+
single-use axes stay in the module that needs them. Two rules keep results
271+
usable:
272+
273+
* Keep parameter values plain strings, numbers or tuples. A live module or dtype
274+
object renders as `<module 'dpnp' from '/...'>` in the result tables and embeds
275+
a local path in the result identity.
276+
* Keep `params` static. Deriving an axis from the machine makes rows
277+
incomparable between devices; decide per-device behaviour in `setup` instead,
278+
by raising `SkipNotImplemented` (see `bench_dpbench._Workload.setup`).
279+
* Block on dpnp work inside the timed body, or you are timing submission -- see
280+
[Asynchronous execution](#asynchronous-execution).
281+
282+
To add another dpBench workload, follow
283+
[`benchmarks/dpbench/README.md`](benchmarks/dpbench/README.md), then add a
284+
benchmark class for it to `bench_dpbench.py`. Copy an existing one: it is a
285+
banner, a docstring, a `WORKLOAD` attribute and a one-line `time_*` method --
286+
the parameter axes are inherited from `_Workload`.

0 commit comments

Comments
 (0)