Skip to content

Commit 42f2644

Browse files
Add dpnp.linalg.eigvalsh() implementation (#1714)
* Add dpnp.linalg.eigvalsh() impl * Update cupy tests for dpnp.linalg.eigvalsh func * Small update dpnp.linalg.eigh * Update cupy tests for dpnp.linalg.eigh * Add test_eigenvalue_symm to check queue and usm_type * Update test_linalg.py * Optimize dpnp_eigh logic for eigen_mode='N' * Add a logic with a.size==0 to dpnp_eigh * Update docstrings for eigh and eigvalsh * Update cupy tests * Add support for both registers to UPLO * Call dpnp_eigh instead of dpnp_eigvalsh * Add get_symm_herm_numpy_array func to helper.py * Update dpnp tests * Add generate_random_numpy_array func to helper.py * Remove use_seed parameter from generate_random_numpy_array * Address remarks * Generate 4d and more array * Support 4d and more array for dpnp_eigh * Update TestEigenvalue * Fix invalid UPLO test * Make depends parameter optional in heevd.hpp * Add a w/a for MKLD-17201 * Wait for each host_task after calling _syevd * Update test_eigenvalues in test_linalg.py * Update test_eigenvalue in test_sycl_queue.py --------- Co-authored-by: Anton <[email protected]>
1 parent 5f99f4e commit 42f2644

File tree

9 files changed

+461
-209
lines changed

9 files changed

+461
-209
lines changed

dpnp/backend/extensions/lapack/heevd.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ extern std::pair<sycl::event, sycl::event>
4444
const std::int8_t upper_lower,
4545
dpctl::tensor::usm_ndarray eig_vecs,
4646
dpctl::tensor::usm_ndarray eig_vals,
47-
const std::vector<sycl::event> &depends);
47+
const std::vector<sycl::event> &depends = {});
4848

4949
extern void init_heevd_dispatch_table(void);
5050
} // namespace lapack

dpnp/linalg/dpnp_iface_linalg.py

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"eig",
7070
"eigh",
7171
"eigvals",
72+
"eigvalsh",
7273
"inv",
7374
"matrix_power",
7475
"matrix_rank",
@@ -247,6 +248,19 @@ def eigh(a, UPLO="L"):
247248
248249
For full documentation refer to :obj:`numpy.linalg.eigh`.
249250
251+
Parameters
252+
----------
253+
a : (..., M, M) {dpnp.ndarray, usm_ndarray}
254+
A complex- or real-valued array whose eigenvalues and eigenvectors are to be computed.
255+
UPLO : {"L", "U"}, optional
256+
Specifies the calculation uses either the lower ("L") or upper ("U")
257+
triangular part of the matrix.
258+
Regardless of this choice, only the real parts of the diagonal are
259+
considered to preserve the Hermite matrix property.
260+
It therefore follows that the imaginary part of the diagonal
261+
will always be treated as zero.
262+
Default: "L".
263+
250264
Returns
251265
-------
252266
w : (..., M) dpnp.ndarray
@@ -256,15 +270,13 @@ def eigh(a, UPLO="L"):
256270
The column ``v[:, i]`` is the normalized eigenvector corresponding
257271
to the eigenvalue ``w[i]``.
258272
259-
Limitations
260-
-----------
261-
Parameter `a` is supported as :class:`dpnp.ndarray` or :class:`dpctl.tensor.usm_ndarray`.
262-
Input array data types are limited by supported DPNP :ref:`Data types`.
263-
264273
See Also
265274
--------
266-
:obj:`dpnp.eig` : eigenvalues and right eigenvectors for non-symmetric arrays.
267-
:obj:`dpnp.eigvals` : eigenvalues of non-symmetric arrays.
275+
:obj:`dpnp.linalg.eigvalsh` : Compute the eigenvalues of a complex Hermitian or
276+
real symmetric matrix.
277+
:obj:`dpnp.linalg.eig` : Compute the eigenvalues and right eigenvectors of
278+
a square array.
279+
:obj:`dpnp.linalg.eigvals` : Compute the eigenvalues of a general matrix.
268280
269281
Examples
270282
--------
@@ -282,20 +294,13 @@ def eigh(a, UPLO="L"):
282294
"""
283295

284296
dpnp.check_supported_arrays_type(a)
297+
check_stacked_2d(a)
298+
check_stacked_square(a)
285299

300+
UPLO = UPLO.upper()
286301
if UPLO not in ("L", "U"):
287302
raise ValueError("UPLO argument must be 'L' or 'U'")
288303

289-
if a.ndim < 2:
290-
raise ValueError(
291-
"%d-dimensional array given. Array must be "
292-
"at least two-dimensional" % a.ndim
293-
)
294-
295-
m, n = a.shape[-2:]
296-
if m != n:
297-
raise ValueError("Last 2 dimensions of the array must be square")
298-
299304
return dpnp_eigh(a, UPLO=UPLO)
300305

301306

@@ -327,6 +332,64 @@ def eigvals(input):
327332
return call_origin(numpy.linalg.eigvals, input)
328333

329334

335+
def eigvalsh(a, UPLO="L"):
336+
"""
337+
eigvalsh(a, UPLO="L")
338+
339+
Compute the eigenvalues of a complex Hermitian or real symmetric matrix.
340+
341+
Main difference from :obj:`dpnp.linalg.eigh`: the eigenvectors are not computed.
342+
343+
For full documentation refer to :obj:`numpy.linalg.eigvalsh`.
344+
345+
Parameters
346+
----------
347+
a : (..., M, M) {dpnp.ndarray, usm_ndarray}
348+
A complex- or real-valued array whose eigenvalues are to be computed.
349+
UPLO : {"L", "U"}, optional
350+
Specifies the calculation uses either the lower ("L") or upper ("U")
351+
triangular part of the matrix.
352+
Regardless of this choice, only the real parts of the diagonal are
353+
considered to preserve the Hermite matrix property.
354+
It therefore follows that the imaginary part of the diagonal
355+
will always be treated as zero.
356+
Default: "L".
357+
358+
Returns
359+
-------
360+
w : (..., M) dpnp.ndarray
361+
The eigenvalues in ascending order, each repeated according to
362+
its multiplicity.
363+
364+
See Also
365+
--------
366+
:obj:`dpnp.linalg.eigh` : Return the eigenvalues and eigenvectors of a complex Hermitian
367+
(conjugate symmetric) or a real symmetric matrix.
368+
:obj:`dpnp.linalg.eigvals` : Compute the eigenvalues of a general matrix.
369+
:obj:`dpnp.linalg.eig` : Compute the eigenvalues and right eigenvectors of
370+
a general matrix.
371+
372+
Examples
373+
--------
374+
>>> import dpnp as np
375+
>>> from dpnp import linalg as LA
376+
>>> a = np.array([[1, -2j], [2j, 5]])
377+
>>> LA.eigvalsh(a)
378+
array([0.17157288, 5.82842712])
379+
380+
"""
381+
382+
dpnp.check_supported_arrays_type(a)
383+
check_stacked_2d(a)
384+
check_stacked_square(a)
385+
386+
UPLO = UPLO.upper()
387+
if UPLO not in ("L", "U"):
388+
raise ValueError("UPLO argument must be 'L' or 'U'")
389+
390+
return dpnp_eigh(a, UPLO=UPLO, eigen_mode="N")
391+
392+
330393
def inv(a):
331394
"""
332395
Compute the (multiplicative) inverse of a matrix.

dpnp/linalg/dpnp_utils_linalg.py

Lines changed: 90 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -888,72 +888,91 @@ def dpnp_det(a):
888888
return det.reshape(shape)
889889

890890

891-
def dpnp_eigh(a, UPLO):
891+
def dpnp_eigh(a, UPLO, eigen_mode="V"):
892892
"""
893-
dpnp_eigh(a, UPLO)
893+
dpnp_eigh(a, UPLO, eigen_mode="V")
894894
895895
Return the eigenvalues and eigenvectors of a complex Hermitian
896896
(conjugate symmetric) or a real symmetric matrix.
897+
Can return both eigenvalues and eigenvectors (`eigen_mode="V"`) or
898+
only eigenvalues (`eigen_mode="N"`).
897899
898900
The main calculation is done by calling an extension function
899901
for LAPACK library of OneMKL. Depending on input type of `a` array,
900902
it will be either ``heevd`` (for complex types) or ``syevd`` (for others).
901903
902904
"""
903905

904-
a_usm_type = a.usm_type
905-
a_sycl_queue = a.sycl_queue
906-
a_order = "C" if a.flags.c_contiguous else "F"
907-
a_usm_arr = dpnp.get_usm_ndarray(a)
906+
# get resulting type of arrays with eigenvalues and eigenvectors
907+
v_type = _common_type(a)
908+
w_type = _real_type(v_type)
908909

909-
# 'V' means both eigenvectors and eigenvalues will be calculated
910-
jobz = _jobz["V"]
910+
if a.size == 0:
911+
w = dpnp.empty_like(a, shape=a.shape[:-1], dtype=w_type)
912+
if eigen_mode == "V":
913+
v = dpnp.empty_like(a, dtype=v_type)
914+
return w, v
915+
return w
916+
917+
# `eigen_mode` can be either "N" or "V", specifying the computation mode
918+
# for OneMKL LAPACK `syevd` and `heevd` routines.
919+
# "V" (default) means both eigenvectors and eigenvalues will be calculated
920+
# "N" means only eigenvalues will be calculated
921+
jobz = _jobz[eigen_mode]
911922
uplo = _upper_lower[UPLO]
912923

913-
# get resulting type of arrays with eigenvalues and eigenvectors
914-
a_dtype = a.dtype
915-
lapack_func = "_syevd"
916-
if dpnp.issubdtype(a_dtype, dpnp.complexfloating):
917-
lapack_func = "_heevd"
918-
v_type = a_dtype
919-
w_type = dpnp.float64 if a_dtype == dpnp.complex128 else dpnp.float32
920-
elif dpnp.issubdtype(a_dtype, dpnp.floating):
921-
v_type = w_type = a_dtype
922-
elif a_sycl_queue.sycl_device.has_aspect_fp64:
923-
v_type = w_type = dpnp.float64
924-
else:
925-
v_type = w_type = dpnp.float32
924+
# Get LAPACK function (_syevd for real or _heevd for complex data types)
925+
# to compute all eigenvalues and, optionally, all eigenvectors
926+
lapack_func = (
927+
"_heevd" if dpnp.issubdtype(v_type, dpnp.complexfloating) else "_syevd"
928+
)
929+
930+
a_sycl_queue = a.sycl_queue
931+
a_order = "C" if a.flags.c_contiguous else "F"
926932

927933
if a.ndim > 2:
928-
w = dpnp.empty(
929-
a.shape[:-1],
934+
is_cpu_device = a.sycl_device.has_aspect_cpu
935+
orig_shape = a.shape
936+
# get 3d input array by reshape
937+
a = a.reshape(-1, orig_shape[-2], orig_shape[-1])
938+
a_usm_arr = dpnp.get_usm_ndarray(a)
939+
940+
# allocate a memory for dpnp array of eigenvalues
941+
w = dpnp.empty_like(
942+
a,
943+
shape=orig_shape[:-1],
930944
dtype=w_type,
931-
usm_type=a_usm_type,
932-
sycl_queue=a_sycl_queue,
933945
)
946+
w_orig_shape = w.shape
947+
# get 2d dpnp array with eigenvalues by reshape
948+
w = w.reshape(-1, w_orig_shape[-1])
934949

935950
# need to loop over the 1st dimension to get eigenvalues and eigenvectors of 3d matrix A
936-
op_count = a.shape[0]
937-
if op_count == 0:
938-
return w, dpnp.empty_like(a, dtype=v_type)
939-
940-
eig_vecs = [None] * op_count
941-
ht_copy_ev = [None] * op_count
942-
ht_lapack_ev = [None] * op_count
943-
for i in range(op_count):
951+
batch_size = a.shape[0]
952+
eig_vecs = [None] * batch_size
953+
ht_list_ev = [None] * batch_size * 2
954+
for i in range(batch_size):
944955
# oneMKL LAPACK assumes fortran-like array as input, so
945956
# allocate a memory with 'F' order for dpnp array of eigenvectors
946957
eig_vecs[i] = dpnp.empty_like(a[i], order="F", dtype=v_type)
947958

948959
# use DPCTL tensor function to fill the array of eigenvectors with content of input array
949-
ht_copy_ev[i], copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
960+
ht_list_ev[2 * i], copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
950961
src=a_usm_arr[i],
951962
dst=eig_vecs[i].get_array(),
952963
sycl_queue=a_sycl_queue,
953964
)
954965

966+
# TODO: Remove this w/a when MKLD-17201 is solved.
967+
# Waiting for a host task executing an OneMKL LAPACK syevd call
968+
# on CPU causes deadlock due to serialization of all host tasks
969+
# in the queue.
970+
# We need to wait for each host tasks before calling _seyvd to avoid deadlock.
971+
if lapack_func == "_syevd" and is_cpu_device:
972+
ht_list_ev[2 * i].wait()
973+
955974
# call LAPACK extension function to get eigenvalues and eigenvectors of a portion of matrix A
956-
ht_lapack_ev[i], _ = getattr(li, lapack_func)(
975+
ht_list_ev[2 * i + 1], _ = getattr(li, lapack_func)(
957976
a_sycl_queue,
958977
jobz,
959978
uplo,
@@ -962,29 +981,43 @@ def dpnp_eigh(a, UPLO):
962981
depends=[copy_ev],
963982
)
964983

965-
for i in range(op_count):
966-
ht_lapack_ev[i].wait()
967-
ht_copy_ev[i].wait()
984+
dpctl.SyclEvent.wait_for(ht_list_ev)
985+
986+
w = w.reshape(w_orig_shape)
987+
988+
if eigen_mode == "V":
989+
# combine the list of eigenvectors into a single array
990+
v = dpnp.array(eig_vecs, order=a_order).reshape(orig_shape)
991+
return w, v
992+
return w
968993

969-
# combine the list of eigenvectors into a single array
970-
v = dpnp.array(eig_vecs, order=a_order)
971-
return w, v
972994
else:
973-
# oneMKL LAPACK assumes fortran-like array as input, so
974-
# allocate a memory with 'F' order for dpnp array of eigenvectors
975-
v = dpnp.empty_like(a, order="F", dtype=v_type)
995+
a_usm_arr = dpnp.get_usm_ndarray(a)
996+
ht_list_ev = []
997+
copy_ev = dpctl.SyclEvent()
976998

977-
# use DPCTL tensor function to fill the array of eigenvectors with content of input array
978-
ht_copy_ev, copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
979-
src=a_usm_arr, dst=v.get_array(), sycl_queue=a_sycl_queue
980-
)
999+
# When `eigen_mode == "N"` (jobz == 0), OneMKL LAPACK does not overwrite the input array.
1000+
# If the input array 'a' is already F-contiguous and matches the target data type,
1001+
# we can avoid unnecessary memory allocation and data copying.
1002+
if eigen_mode == "N" and a_order == "F" and a.dtype == v_type:
1003+
v = a
1004+
1005+
else:
1006+
# oneMKL LAPACK assumes fortran-like array as input, so
1007+
# allocate a memory with 'F' order for dpnp array of eigenvectors
1008+
v = dpnp.empty_like(a, order="F", dtype=v_type)
1009+
1010+
# use DPCTL tensor function to fill the array of eigenvectors with content of input array
1011+
ht_copy_ev, copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
1012+
src=a_usm_arr, dst=v.get_array(), sycl_queue=a_sycl_queue
1013+
)
1014+
ht_list_ev.append(ht_copy_ev)
9811015

9821016
# allocate a memory for dpnp array of eigenvalues
983-
w = dpnp.empty(
984-
a.shape[:-1],
1017+
w = dpnp.empty_like(
1018+
a,
1019+
shape=a.shape[:-1],
9851020
dtype=w_type,
986-
usm_type=a_usm_type,
987-
sycl_queue=a_sycl_queue,
9881021
)
9891022

9901023
# call LAPACK extension function to get eigenvalues and eigenvectors of matrix A
@@ -996,8 +1029,9 @@ def dpnp_eigh(a, UPLO):
9961029
w.get_array(),
9971030
depends=[copy_ev],
9981031
)
1032+
ht_list_ev.append(ht_lapack_ev)
9991033

1000-
if a_order != "F":
1034+
if eigen_mode == "V" and a_order != "F":
10011035
# need to align order of eigenvectors with one of input matrix A
10021036
out_v = dpnp.empty_like(v, order=a_order)
10031037
ht_copy_out_ev, _ = ti._copy_usm_ndarray_into_usm_ndarray(
@@ -1006,14 +1040,13 @@ def dpnp_eigh(a, UPLO):
10061040
sycl_queue=a_sycl_queue,
10071041
depends=[lapack_ev],
10081042
)
1009-
ht_copy_out_ev.wait()
1043+
ht_list_ev.append(ht_copy_out_ev)
10101044
else:
10111045
out_v = v
10121046

1013-
ht_lapack_ev.wait()
1014-
ht_copy_ev.wait()
1047+
dpctl.SyclEvent.wait_for(ht_list_ev)
10151048

1016-
return w, out_v
1049+
return (w, out_v) if eigen_mode == "V" else w
10171050

10181051

10191052
def dpnp_inv_batched(a, res_type):

0 commit comments

Comments
 (0)