常用函数
1. 常用依元素映射 (Elementwise) 函数
在 RSTSR 中,许多 Python Array API 所要求的函数都已经实现。它们大部分可以通过常规的 Rust 函数、或作为 associated methods 可以被调用。
举例而言,在 row-major 前提下,依照 broadcast 规则可以进行两个张量之间的元素比较:
#[rustfmt::skip]
let a = rt::asarray(vec![
5., 2.,
3., 6.,
1., 8.,
]).into_shape([3, 2]);
let b = rt::asarray(vec![3., 4.]);
// broadcasted comparison a >= b
// called by associated method
let c = a.greater_equal(&b);
println!("{:5}", c);
// output:
// [[ true false]
// [ true true ]
// [ false true ]]
也可以对一个张量作正弦值计算:
let b = rt::asarray(vec![3., 4.]);
let d = rt::sin(&b);
println!("{:6.3}", d);
// output: [ 0.141 -0.757]
常见的二元函数包括指数 pow
、地板除法 floor_divide
、大于等于 greater_equal
等等。其中,用于比较用途的二元函数,通常有简称;例如 greater_equal
可以简称为 ge
。
具有简称的二元函数,通常不能通过 associated methods 调用 (为避免与 PartialOrd
等 trait 冲突);但可以通过普通的 Rust 函数调用。
let a = rt::asarray(vec![0, 2, 5, 7]);
let b = rt::asarray(vec![1, 3, 4, 6]);
let c = a.greater_equal(&b);
println!("{:5}", c);
// output: [ false false true true ]
let c = rt::greater_equal(&a, &b);
println!("{:5}", c);
// output: [ false false true true ]
let c = rt::ge(&a, &b);
println!("{:5}", c);
// output: [ false false true true ]
Tensor
作解构RSTSR 中,几乎所有函数都允许传入 &TensorAny
或 TensorView
作为输入;这种情况下,传入的张量本体不会更改或解构。
但对于部分计算 (包括 前一节 中的四则运算等),还同时允许传入占有数据的 Tensor
;依情况该张量的底层数据会被更改,且用户后续无法再使用该张量。这对于 RSTSR 的不少一元函数也是如此;因此使用这些一元函数时,需要留意所有权的情况。
以正弦函数为例,
这会跳出报错信息;对于该错误,编译器给出的提示是有价值的:
error[E0382]: use of moved value: `b`
|
| let b = rt::asarray(vec![3., 4.]);
| - move occurs because `b` has type `...`, which does not implement the `Copy` trait
| let c = rt::sin(b);
| - value moved here
| let d = rt::cos(b);
| ^ value used here after move
|
help: consider borrowing `b`
|
| let c = rt::sin(&b);
| +
2. 映射函数
尽管 RSTSR 实现了许多 elementwise 函数;但我们不太可能对所有函数作实现。对于在 CPU 设备上的张量,我们提供了映射函数 (名称中含有 map 的函数),以满足用户个性化的映射需求。
2.1 一元映射
下面是计算 Gamma 函数的例子。我们使用 mapv
函数作映射;该函数可以连续地执行,但需要注意,RSTSR 并不具备 lazy evaluation 的功能,因此连续地函数式地调用 mapv
迭代映射并不会更高效:
let a: Tensor<f64, _> = rt::asarray(vec![0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]);
let b = a.mapv(libm::lgamma).mapv(libm::exp);
println!("{:6.3}", a);
println!("{:6.3}", b);
// output:
// [ 0.500 1.000 1.500 2.000 2.500 3.000 3.500 4.000]
// [ 1.772 1.000 0.886 1.000 1.329 2.000 3.323 6.000]
// please note that in RSTSR, mapv is not lazy evaluated
// so below code is expected to be more efficient
let b = a.mapv(|x| libm::exp(libm::lgamma(x)));
println!("{:6.3}", b);
// also, function `libm::tgamma` is equivalent to `libm::exp(libm::lgamma(x))`
// when numerical discrepancy is not a concern
let b = a.mapv(libm::tgamma);
println!("{:6.3}", b);
在 NumPy 中,类似的函数应是 np.vectorize
。上述代码在 NumPy 中,可以等价地写为
import numpy as np
import scipy
a = np.linspace(1.0, 10.0, 4096 * 4096)
f = np.vectorize(scipy.special.gamma)
b = f(a)
尽管功能上是相似的,但 NumPy 与 RSTSR (或 crate ndarray) 写出该函数的动机稍有不同。
RSTSR 的 map 系列函数,单纯是进行函数映射,而不是作任何指令集层面的向量化操作 (SIMD)。尽管如此,RSTSR 还是会作部分性能上的优化:
- 执行映射时会尽可能按最连续的维度进行;
- 在张量较大时启用并行。
但即使不使用 RSTSR,用户通过手动对 Vec<T>
作并行循环,也能达到一样的执行效率;这只会增加少许代码复杂程度。
对于 NumPy,由于 Python 手动的 for 循环非常慢,因此当映射关系稍复杂时,就必须要用 NumPy 使用 CPython 等技术加速的映射函数,才能保证运行效率。用户在不借用 Python 方言 (Numba, JAX 等)、或不使用 CPython/ctypes 等策略加速的情况下,除了 np.vectorize
之外很难有其他办法。
2.2 一元可变映射
对可变的 Tensor
与 TensorMut
类型,RSTSR 也提供了 mapvi
函数,以在不分配新的内存的前提下,原地映射变更数值:
let mut vec_a = vec![0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0];
let mut a: TensorMut<f64, _> = rt::asarray(&mut vec_a);
a.mapvi(libm::tgamma);
// original vector is also modified
println!("{:6.3?}", vec_a);
// [ 1.772, 1.000, 0.886, 1.000, 1.329, 2.000, 3.323, 6.000]
2.3 二元映射
对于二元映射,RSTSR 也提供了 mapvb
函数:
#[rustfmt::skip]
let a = rt::asarray(vec![
5., 2.,
3., 6.,
1., 8.,
]).into_shape([3, 2]);
let b = rt::asarray(vec![3., 4.]);
let c = a.mapvb(&b, libm::fmin);
println!("{:6.3}", c);
// output:
// [[ 3.000 2.000]
// [ 3.000 4.000]
// [ 1.000 4.000]]
3. 归约运算
RSTSR 目前支持一部分归约运算。这包括求和、最大值、标准差等。增加后缀 _axes
可以对特定维度作归约运算。
#[rustfmt::skip]
let a = rt::asarray(vec![
5., 2.,
3., 6.,
1., 8.,
]).into_shape([3, 2]);
let b = a.l2_norm();
println!("{:6.3}", b);
// output: 11.790
let b = a.sum_axes(-1);
println!("{:6.3}", b);
// output: [ 7.000 9.000 9.000]
let b = a.argmin_axes(0);
println!("{:6.3}", b);
// output: [ 2 0]
对于高维张量,_axes
后缀函数也可以传入数组,作为被归约的维度:
let a = rt::linspace((-1.0, 1.0, 24)).into_shape([2, 3, 4]);
let b = a.mean_axes([0, 2]);
println!("{:6.3}", b);
// output: [ -0.348 -0.000 0.348]
作为特例,Tensor<bool, B, D>
也可以作 sum
或 sum_axes
运算;对该张量求和时,true
当作 1、false
当作 0:
let a = rt::asarray(vec![false, true, false, true, true]);
let b = a.sum();
println!("{:6}", b);
// output: 3
4. 线性代数 (linalg)
目前,RSTSR 支持一部分 NumPy 与 SciPy 的线性代数功能。典型的线性代数问题包括 Hermite 矩阵的本征值问题、SVD 分解、Cholesky 分解等等。
let device = DeviceFaer::new(4);
#[rustfmt::skip]
let a = rt::asarray((vec![
1.0, 0.5, 1.5,
0.5, 5.0, 2.0,
1.5, 2.0, 8.0,
], &device)).into_shape([3, 3]);
let c = rt::linalg::eigh(&a);
let (eigenvalues, eigenvectors) = c.into();
println!("{:8.5}", eigenvalues);
// [ 0.69007 4.01426 9.29567]
println!("{:8.5}", eigenvectors);
// [[ 0.98056 0.06364 -0.18561]
// [ -0.02335 -0.90137 -0.43242]
// [ -0.19482 0.42835 -0.88236]]
目前 RSTSR 着力开发的后端是 DeviceOpenBLAS
与 DeviceFaer
,且以前者为主。DeviceOpenBLAS
通常所实现的功能更多,这包括但不限于
- 广义本征值问题
rt::linalg::eigh(&a, &b)
; - 三角矩阵求解
rt::linalg::solve_triangular(&a, &b)
; - 通过可变引用复用内存空间,求解本征值问题
rt::linalg::eigh(a.view_mut())
(类似于 SciPy 对应函数的overwrite_a
选项)。
尽管 DeviceFaer
目前还有一些功能没有实现,但它作为纯 Rust 后端,相对于 DeviceOpenBLAS
具有更大的可迁移性。