Skip to content

Commit 49b476f

Browse files
authored
Eliminate BLAS dependency for ALL crates (#207)
* Integrate ndarray-linalg-rs into linfa-clustering * Restructure linfa-datasets to fix warnings * Add support for linfa-ica * Integrate into PLS (bugs pending) * Integration with linfa-reduction complete * Fix PLS CCA test * Use invh instead of invc in elasticnet since the input might not be positive definite * Integrate into linfa-preprocessing * Integrate into linfa-linear * Integrate into linfa-elasticnet * Optimize gaussian mixture solve_triangular call * Fix example Cargo invocations to not use blas features * Remove BLAS from CI steps and add BLAS test job * Enable most CI steps even in draft PRs * Fix test-blas workflow name * Eliminate BLAS from linfa-logistic * Fix more examples invocations in READMEs * Add non_exhaustive to modified error types so the blas feature is additive * Change linfa-reduction to use truncated SVD and LOBPCG * Switch to linfa-linalg * Fix system BLAS to static BLAS in CI * Centralized README docs on BLAS feature
1 parent 6168a90 commit 49b476f

File tree

50 files changed

+781
-409
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+781
-409
lines changed

.github/workflows/benching.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ jobs:
66
testing:
77
name: benching
88
runs-on: ubuntu-18.04
9-
if: github.event.pull_request.draft == false
10-
container:
11-
image: rustmath/mkl-rust:1.43.0
12-
options: --security-opt seccomp=unconfined
139

1410
steps:
1511
- name: Checkout sources
@@ -26,4 +22,4 @@ jobs:
2622
uses: actions-rs/cargo@v1
2723
with:
2824
command: bench
29-
args: iai --all --features intel-mkl-system
25+
args: iai --all

.github/workflows/checking.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ jobs:
66
check:
77
name: check-${{ matrix.toolchain }}-${{ matrix.os }}
88
runs-on: ${{ matrix.os }}
9-
if: github.event.pull_request.draft == false
109
strategy:
1110
fail-fast: false
1211
matrix:

.github/workflows/codequality.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ jobs:
77
codequality:
88
name: codequality
99
runs-on: ubuntu-latest
10-
if: github.event.pull_request.draft == false
1110
strategy:
1211
matrix:
1312
toolchain:
@@ -42,9 +41,6 @@ jobs:
4241
name: coverage
4342
runs-on: ubuntu-18.04
4443
if: github.event.pull_request.draft == false
45-
container:
46-
image: rustmath/mkl-rust:1.43.0
47-
options: --security-opt seccomp=unconfined
4844

4945
steps:
5046
- name: Checkout sources
@@ -74,7 +70,7 @@ jobs:
7470

7571
- name: Generate code coverage
7672
run: |
77-
cargo tarpaulin --verbose --features intel-mkl-system --timeout 120 --out Xml --all --release
73+
cargo tarpaulin --verbose --timeout 120 --out Xml --all --release
7874
- name: Upload to codecov.io
7975
uses: codecov/codecov-action@v1
8076
with:

.github/workflows/testing.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ jobs:
66
testing:
77
name: testing-${{ matrix.toolchain }}-${{ matrix.os }}
88
runs-on: ${{ matrix.os }}
9-
if: github.event.pull_request.draft == false
109
strategy:
1110
fail-fast: false
1211
matrix:
@@ -28,11 +27,38 @@ jobs:
2827
toolchain: ${{ matrix.toolchain }}
2928
override: true
3029

31-
- name: Log active toolchain
32-
run: rustup show
30+
- name: Run cargo test
31+
uses: actions-rs/cargo@v1
32+
with:
33+
command: test
34+
args: --release --workspace
35+
36+
testing-blas:
37+
name: testing-with-BLAS-${{ matrix.toolchain }}-${{ matrix.os }}
38+
runs-on: ${{ matrix.os }}
39+
strategy:
40+
fail-fast: false
41+
matrix:
42+
toolchain:
43+
- 1.54.0
44+
- stable
45+
os:
46+
- ubuntu-18.04
47+
- windows-2019
48+
49+
steps:
50+
- name: Checkout sources
51+
uses: actions/checkout@v2
52+
53+
- name: Install toolchain
54+
uses: actions-rs/toolchain@v1
55+
with:
56+
profile: minimal
57+
toolchain: ${{ matrix.toolchain }}
58+
override: true
3359

34-
- name: Run cargo test in release mode
60+
- name: Run cargo test with BLAS enabled
3561
uses: actions-rs/cargo@v1
3662
with:
3763
command: test
38-
args: --all --release --features intel-mkl-static
64+
args: --release --workspace --features intel-mkl-static,linfa-clustering/blas,linfa-ica/blas,linfa-reduction/blas,linfa-linear/blas,linfa-preprocessing/blas,linfa-pls/blas,linfa-elasticnet/blas

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ Unreleased
22
========================
33

44
Changes
5-
----------------------
6-
* remove `SeedableRng` trait bound from `KMeans` and `GaussianMixture`
5+
-----------
6+
* remove `SeedableRng` trait bound from `KMeans` and `GaussianMixture`
7+
* BLAS backend no longer required to build Linfa
78

89
Breaking Changes
9-
----------------------
10+
-----------
1011
* parametrize `AsTargets` by the dimensionality of the targets and introduce `AsSingleTargets` and `AsMultiTargets`
1112
* 1D target arrays are no longer converted to 2D when constructing `Dataset`s
1213
* `Dataset` and `DatasetView` can now be parametrized by target dimensionality, with 2D being the default

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,17 @@ If this strikes a chord with you, please take a look at the [roadmap](https://gi
5353

5454
## BLAS/Lapack backend
5555

56-
At the moment you can choose between the following BLAS/LAPACK backends: `openblas`, `netblas` or `intel-mkl`
56+
Some algorithm crates need to use an external library for linear algebra routines. By default, we use a pure-Rust implementation. However, you can also choose an external BLAS/LAPACK backend library instead, by enabling the `blas` feature and a feature corresponding to your BLAS backend. Currently you can choose between the following BLAS/LAPACK backends: `openblas`, `netblas` or `intel-mkl`.
5757

5858
|Backend | Linux | Windows | macOS |
5959
|:--------|:-----:|:-------:|:-----:|
6060
|OpenBLAS |✔️ |- |- |
6161
|Netlib |✔️ |- |- |
6262
|Intel MKL|✔️ |✔️ |✔️ |
6363

64-
For example if you want to use the system IntelMKL library for the PCA example, then pass the corresponding feature:
65-
```
66-
cd linfa-reduction && cargo run --release --example pca --features linfa/intel-mkl-system
67-
```
68-
This selects the `intel-mkl` system library as BLAS/LAPACK backend. On the other hand if you want to compile the library and link it with the generated artifacts, pass `intel-mkl-static`.
64+
Each BLAS backend has two features available. The feature allows you to choose between linking the BLAS library in your system or statically building the library. For example, the features for the `intel-mkl` backend are `intel-mkl-static` and `intel-mkl-system`.
65+
66+
An example set of Cargo flags for enabling the Intel MKL backend on an algorithm crate is `--features blas,linfa/intel-mkl-system`. Note that the BLAS backend features are defined on the `linfa` crate, and should only be specified for the final executable.
6967

7068
# License
7169
Dual-licensed to be compatible with the Rust project.

algorithms/linfa-bayes/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ keywords = ["factorization", "machine-learning", "linfa", "unsupervised"]
1111
categories = ["algorithms", "mathematics", "science"]
1212

1313
[dependencies]
14-
ndarray = { version = "0.15" , features = ["blas", "approx"]}
14+
ndarray = { version = "0.15" , features = ["approx"]}
1515
ndarray-stats = "0.5"
1616
thiserror = "1.0"
1717

algorithms/linfa-clustering/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ categories = ["algorithms", "mathematics", "science"]
1818

1919
[features]
2020
default = []
21+
blas = ["ndarray-linalg", "linfa/ndarray-linalg"]
2122
serde = ["serde_crate", "ndarray/serde", "linfa-nn/serde"]
2223

2324
[dependencies.serde_crate]
@@ -29,15 +30,16 @@ features = ["std", "derive"]
2930

3031
[dependencies]
3132
ndarray = { version = "0.15", features = ["rayon", "approx"]}
32-
ndarray-linalg = "0.14"
33+
linfa-linalg = { version = "0.1", default-features = false }
34+
ndarray-linalg = { version = "0.14", optional = true }
3335
ndarray-rand = "0.14"
3436
ndarray-stats = "0.5"
3537
num-traits = "0.2"
3638
rand_xoshiro = "0.6"
3739
space = "0.12"
3840
thiserror = "1.0"
3941
partitions = "0.2.4"
40-
linfa = { version = "0.5.0", path = "../..", features = ["ndarray-linalg"] }
42+
linfa = { version = "0.5.0", path = "../.." }
4143
linfa-nn = { version = "0.5.0", path = "../linfa-nn" }
4244
noisy_float = "0.2.0"
4345

algorithms/linfa-clustering/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Implementation choices, algorithmic details and a tutorial can be found
2323

2424
**WARNING:** Currently the Approximated DBSCAN implementation is slower than the normal DBSCAN implementation. Therefore DBSCAN should always be used over Approximated DBSCAN.
2525

26+
## BLAS/Lapack backend
27+
28+
See [this section](../../README.md#blaslapack-backend) to enable an external BLAS/LAPACK backend.
29+
2630
## License
2731
Dual-licensed to be compatible with the Rust project.
2832

algorithms/linfa-clustering/src/gaussian_mixture/algorithm.rs

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ use crate::gaussian_mixture::hyperparams::{
33
GmmCovarType, GmmInitMethod, GmmParams, GmmValidParams,
44
};
55
use crate::k_means::KMeans;
6-
use linfa::{
7-
dataset::{WithLapack, WithoutLapack},
8-
prelude::*,
9-
DatasetBase, Float,
10-
};
6+
#[cfg(feature = "blas")]
7+
use linfa::dataset::{WithLapack, WithoutLapack};
8+
use linfa::{prelude::*, DatasetBase, Float};
9+
#[cfg(not(feature = "blas"))]
10+
use linfa_linalg::{cholesky::*, triangular::*};
1111
use ndarray::{s, Array, Array1, Array2, Array3, ArrayBase, Axis, Data, Ix2, Ix3, Zip};
12-
use ndarray_linalg::{cholesky::*, triangular::*, Lapack, Scalar};
12+
#[cfg(feature = "blas")]
13+
use ndarray_linalg::{cholesky::*, triangular::*};
1314
use ndarray_rand::rand::Rng;
1415
use ndarray_rand::rand_distr::Uniform;
1516
use ndarray_rand::RandomExt;
@@ -264,10 +265,18 @@ impl<F: Float> GaussianMixtureModel<F> {
264265
let n_features = covariances.shape()[1];
265266
let mut precisions_chol = Array::zeros((n_clusters, n_features, n_features));
266267
for (k, covariance) in covariances.outer_iter().enumerate() {
267-
let decomp = covariance.with_lapack().cholesky(UPLO::Lower)?;
268-
let sol = decomp
269-
.solve_triangular(UPLO::Lower, Diag::NonUnit, &Array::eye(n_features))?
270-
.without_lapack();
268+
#[cfg(feature = "blas")]
269+
let sol = {
270+
let decomp = covariance.with_lapack().cholesky(UPLO::Lower)?;
271+
decomp
272+
.solve_triangular_into(UPLO::Lower, Diag::NonUnit, Array::eye(n_features))?
273+
.without_lapack()
274+
};
275+
#[cfg(not(feature = "blas"))]
276+
let sol = {
277+
let decomp = covariance.cholesky()?;
278+
decomp.solve_triangular_into(Array::eye(n_features), UPLO::Lower)?
279+
};
271280

272281
precisions_chol.slice_mut(s![k, .., ..]).assign(&sol.t());
273282
}
@@ -461,7 +470,7 @@ impl<F: Float, R: Rng + Clone, D: Data<Elem = F>, T> Fit<ArrayBase<D, Ix2>, T, G
461470
}
462471
}
463472

464-
impl<F: Float + Lapack + Scalar, D: Data<Elem = F>> PredictInplace<ArrayBase<D, Ix2>, Array1<usize>>
473+
impl<F: Float, D: Data<Elem = F>> PredictInplace<ArrayBase<D, Ix2>, Array1<usize>>
465474
for GaussianMixtureModel<F>
466475
{
467476
fn predict_inplace(&self, observations: &ArrayBase<D, Ix2>, targets: &mut Array1<usize>) {
@@ -473,7 +482,7 @@ impl<F: Float + Lapack + Scalar, D: Data<Elem = F>> PredictInplace<ArrayBase<D,
473482

474483
let (_, log_resp) = self.estimate_log_prob_resp(observations);
475484
*targets = log_resp
476-
.mapv(Scalar::exp)
485+
.mapv(F::exp)
477486
.map_axis(Axis(1), |row| row.argmax().unwrap());
478487
}
479488

@@ -486,10 +495,18 @@ impl<F: Float + Lapack + Scalar, D: Data<Elem = F>> PredictInplace<ArrayBase<D,
486495
mod tests {
487496
use super::*;
488497
use approx::{abs_diff_eq, assert_abs_diff_eq};
498+
#[cfg(feature = "blas")]
489499
use lax::error::Error;
490500
use linfa_datasets::generate;
491501
use ndarray::{array, concatenate, ArrayView1, ArrayView2, Axis};
502+
503+
#[cfg(not(feature = "blas"))]
504+
use linfa_linalg::LinalgError;
505+
#[cfg(not(feature = "blas"))]
506+
use linfa_linalg::Result as LAResult;
507+
#[cfg(feature = "blas")]
492508
use ndarray_linalg::error::LinalgError;
509+
#[cfg(feature = "blas")]
493510
use ndarray_linalg::error::Result as LAResult;
494511
use ndarray_rand::rand::prelude::ThreadRng;
495512
use ndarray_rand::rand::SeedableRng;
@@ -514,7 +531,10 @@ mod tests {
514531
}
515532
impl MultivariateNormal {
516533
pub fn new(mean: &ArrayView1<f64>, covariance: &ArrayView2<f64>) -> LAResult<Self> {
534+
#[cfg(feature = "blas")]
517535
let lower = covariance.cholesky(UPLO::Lower)?;
536+
#[cfg(not(feature = "blas"))]
537+
let lower = covariance.cholesky()?;
518538
Ok(MultivariateNormal {
519539
mean: mean.to_owned(),
520540
covariance: covariance.to_owned(),
@@ -603,16 +623,18 @@ mod tests {
603623
.with_rng(rng.clone())
604624
.fit(&dataset);
605625

606-
assert!(
607-
match gmm.expect_err("should generate an error with reg_covar being nul") {
608-
GmmError::LinalgError(e) => match e {
609-
LinalgError::Lapack(Error::LapackComputationalFailure { return_code: 2 }) =>
610-
true,
611-
_ => panic!("should be a lapack error 2"),
612-
},
613-
_ => panic!("should be a linear algebra error"),
626+
match gmm.expect_err("should generate an error with reg_covar being nul") {
627+
GmmError::LinalgError(e) => {
628+
#[cfg(feature = "blas")]
629+
assert!(matches!(
630+
e,
631+
LinalgError::Lapack(Error::LapackComputationalFailure { return_code: 2 })
632+
));
633+
#[cfg(not(feature = "blas"))]
634+
assert!(matches!(e, LinalgError::NotPositiveDefinite));
614635
}
615-
);
636+
e => panic!("should be a linear algebra error: {:?}", e),
637+
}
616638
// Test it passes when default value is used
617639
assert!(GaussianMixtureModel::params(3)
618640
.with_rng(rng)
@@ -632,16 +654,18 @@ mod tests {
632654
.reg_covariance(0.)
633655
.fit(&dataset);
634656

635-
assert!(
636-
match gmm.expect_err("should generate an error with reg_covar being nul") {
637-
GmmError::LinalgError(e) => match e {
638-
LinalgError::Lapack(Error::LapackComputationalFailure { return_code: 1 }) =>
639-
true,
640-
_ => panic!("should be a lapack error 1"),
641-
},
642-
_ => panic!("should be a linear algebra error"),
657+
#[cfg(feature = "blas")]
658+
match gmm.expect_err("should generate an error with reg_covar being nul") {
659+
GmmError::LinalgError(e) => {
660+
assert!(matches!(
661+
e,
662+
LinalgError::Lapack(Error::LapackComputationalFailure { return_code: 1 })
663+
));
643664
}
644-
);
665+
e => panic!("should be a linear algebra error: {:?}", e),
666+
}
667+
#[cfg(not(feature = "blas"))]
668+
gmm.expect_err("should generate an error with reg_covar being nul");
645669

646670
// Test it passes when default value is used
647671
assert!(GaussianMixtureModel::params(1).fit(&dataset).is_ok());

0 commit comments

Comments
 (0)