2020-12-30 23:09:46 +08:00
|
|
|
use crate::csc::CscMatrix;
|
2021-01-26 00:26:27 +08:00
|
|
|
use crate::csr::CsrMatrix;
|
2020-12-10 20:30:37 +08:00
|
|
|
|
2021-01-26 00:26:27 +08:00
|
|
|
use crate::ops::serial::{
|
|
|
|
spadd_csc_prealloc, spadd_csr_prealloc, spadd_pattern, spmm_csc_dense, spmm_csc_pattern,
|
2022-03-22 06:56:51 +08:00
|
|
|
spmm_csc_prealloc_unchecked, spmm_csr_dense, spmm_csr_pattern, spmm_csr_prealloc_unchecked,
|
2021-01-26 00:26:27 +08:00
|
|
|
};
|
|
|
|
use crate::ops::Op;
|
2021-08-03 00:41:46 +08:00
|
|
|
use nalgebra::allocator::Allocator;
|
|
|
|
use nalgebra::base::storage::RawStorage;
|
2021-01-26 00:26:27 +08:00
|
|
|
use nalgebra::constraint::{DimEq, ShapeConstraint};
|
|
|
|
use nalgebra::{
|
2023-01-14 23:22:27 +08:00
|
|
|
ClosedAdd, ClosedDiv, ClosedMul, ClosedSub, DefaultAllocator, Dim, Dyn, Matrix, OMatrix,
|
2021-01-26 00:26:27 +08:00
|
|
|
Scalar, U1,
|
|
|
|
};
|
|
|
|
use num_traits::{One, Zero};
|
|
|
|
use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub};
|
2020-12-10 20:30:37 +08:00
|
|
|
|
2020-12-30 23:09:46 +08:00
|
|
|
/// Helper macro for implementing binary operators for different matrix types
|
|
|
|
/// See below for usage.
|
|
|
|
macro_rules! impl_bin_op {
|
|
|
|
($trait:ident, $method:ident,
|
2021-01-06 18:04:49 +08:00
|
|
|
<$($life:lifetime),* $(,)? $($scalar_type:ident $(: $bounds:path)?)?>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block)
|
2020-12-30 23:09:46 +08:00
|
|
|
=>
|
|
|
|
{
|
2021-01-04 20:39:41 +08:00
|
|
|
impl<$($life,)* $($scalar_type)?> $trait<$b_type> for $a_type
|
2020-12-30 23:09:46 +08:00
|
|
|
where
|
2021-01-06 18:04:49 +08:00
|
|
|
// Note: The Neg bound is currently required because we delegate e.g.
|
2021-01-05 21:59:54 +08:00
|
|
|
// Sub to SpAdd with negative coefficients. This is not well-defined for
|
|
|
|
// unsigned data types.
|
2021-08-03 00:41:46 +08:00
|
|
|
$($scalar_type: $($bounds + )? Scalar + ClosedAdd + ClosedSub + ClosedMul + Zero + One + Neg<Output=T>)?
|
2020-12-30 23:09:46 +08:00
|
|
|
{
|
|
|
|
type Output = $ret;
|
2021-01-04 20:39:41 +08:00
|
|
|
fn $method(self, $b: $b_type) -> Self::Output {
|
2020-12-30 23:09:46 +08:00
|
|
|
let $a = self;
|
|
|
|
$body
|
|
|
|
}
|
|
|
|
}
|
2021-01-06 18:04:49 +08:00
|
|
|
};
|
2020-12-10 20:30:37 +08:00
|
|
|
}
|
|
|
|
|
2021-01-05 21:59:54 +08:00
|
|
|
/// Implements a +/- b for all combinations of reference and owned matrices, for
|
2020-12-30 23:09:46 +08:00
|
|
|
/// CsrMatrix or CscMatrix.
|
2021-01-05 21:59:54 +08:00
|
|
|
macro_rules! impl_sp_plus_minus {
|
|
|
|
// We first match on some special-case syntax, and forward to the actual implementation
|
|
|
|
($matrix_type:ident, $spadd_fn:ident, +) => {
|
|
|
|
impl_sp_plus_minus!(Add, add, $matrix_type, $spadd_fn, +, T::one());
|
|
|
|
};
|
|
|
|
($matrix_type:ident, $spadd_fn:ident, -) => {
|
|
|
|
impl_sp_plus_minus!(Sub, sub, $matrix_type, $spadd_fn, -, -T::one());
|
|
|
|
};
|
|
|
|
($trait:ident, $method:ident, $matrix_type:ident, $spadd_fn:ident, $sign:tt, $factor:expr) => {
|
|
|
|
impl_bin_op!($trait, $method,
|
|
|
|
<'a, T>(a: &'a $matrix_type<T>, b: &'a $matrix_type<T>) -> $matrix_type<T> {
|
2020-12-30 23:09:46 +08:00
|
|
|
// If both matrices have the same pattern, then we can immediately re-use it
|
2021-01-19 23:53:39 +08:00
|
|
|
let pattern = spadd_pattern(a.pattern(), b.pattern());
|
2020-12-30 23:09:46 +08:00
|
|
|
let values = vec![T::zero(); pattern.nnz()];
|
|
|
|
// We are giving data that is valid by definition, so it is safe to unwrap below
|
|
|
|
let mut result = $matrix_type::try_from_pattern_and_values(pattern, values)
|
|
|
|
.unwrap();
|
|
|
|
$spadd_fn(T::zero(), &mut result, T::one(), Op::NoOp(&a)).unwrap();
|
2021-01-05 21:59:54 +08:00
|
|
|
$spadd_fn(T::one(), &mut result, $factor * T::one(), Op::NoOp(&b)).unwrap();
|
2020-12-30 23:09:46 +08:00
|
|
|
result
|
|
|
|
});
|
2020-12-10 20:30:37 +08:00
|
|
|
|
2021-01-05 21:59:54 +08:00
|
|
|
impl_bin_op!($trait, $method,
|
|
|
|
<'a, T>(a: $matrix_type<T>, b: &'a $matrix_type<T>) -> $matrix_type<T> {
|
2021-01-19 23:53:39 +08:00
|
|
|
&a $sign b
|
2020-12-30 23:09:46 +08:00
|
|
|
});
|
|
|
|
|
2021-01-05 21:59:54 +08:00
|
|
|
impl_bin_op!($trait, $method,
|
|
|
|
<'a, T>(a: &'a $matrix_type<T>, b: $matrix_type<T>) -> $matrix_type<T> {
|
2021-01-19 23:53:39 +08:00
|
|
|
a $sign &b
|
2020-12-30 23:09:46 +08:00
|
|
|
});
|
2021-01-05 21:59:54 +08:00
|
|
|
impl_bin_op!($trait, $method, <T>(a: $matrix_type<T>, b: $matrix_type<T>) -> $matrix_type<T> {
|
|
|
|
a $sign &b
|
2020-12-30 23:09:46 +08:00
|
|
|
});
|
2020-12-10 20:30:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-05 21:59:54 +08:00
|
|
|
impl_sp_plus_minus!(CsrMatrix, spadd_csr_prealloc, +);
|
|
|
|
impl_sp_plus_minus!(CsrMatrix, spadd_csr_prealloc, -);
|
|
|
|
impl_sp_plus_minus!(CscMatrix, spadd_csc_prealloc, +);
|
|
|
|
impl_sp_plus_minus!(CscMatrix, spadd_csc_prealloc, -);
|
2020-12-10 20:30:37 +08:00
|
|
|
|
2020-12-30 23:09:46 +08:00
|
|
|
macro_rules! impl_mul {
|
|
|
|
($($args:tt)*) => {
|
|
|
|
impl_bin_op!(Mul, mul, $($args)*);
|
2020-12-10 20:30:37 +08:00
|
|
|
}
|
2020-12-16 23:17:42 +08:00
|
|
|
}
|
|
|
|
|
2020-12-30 23:09:46 +08:00
|
|
|
/// Implements a + b for all combinations of reference and owned matrices, for
|
|
|
|
/// CsrMatrix or CscMatrix.
|
|
|
|
macro_rules! impl_spmm {
|
|
|
|
($matrix_type:ident, $pattern_fn:expr, $spmm_fn:expr) => {
|
2021-01-04 20:39:41 +08:00
|
|
|
impl_mul!(<'a, T>(a: &'a $matrix_type<T>, b: &'a $matrix_type<T>) -> $matrix_type<T> {
|
2020-12-30 23:09:46 +08:00
|
|
|
let pattern = $pattern_fn(a.pattern(), b.pattern());
|
|
|
|
let values = vec![T::zero(); pattern.nnz()];
|
2021-01-19 23:53:39 +08:00
|
|
|
let mut result = $matrix_type::try_from_pattern_and_values(pattern, values)
|
2020-12-30 23:09:46 +08:00
|
|
|
.unwrap();
|
|
|
|
$spmm_fn(T::zero(),
|
|
|
|
&mut result,
|
|
|
|
T::one(),
|
|
|
|
Op::NoOp(a),
|
|
|
|
Op::NoOp(b))
|
|
|
|
.expect("Internal error: spmm failed (please debug).");
|
|
|
|
result
|
|
|
|
});
|
2021-01-04 20:39:41 +08:00
|
|
|
impl_mul!(<'a, T>(a: &'a $matrix_type<T>, b: $matrix_type<T>) -> $matrix_type<T> { a * &b});
|
|
|
|
impl_mul!(<'a, T>(a: $matrix_type<T>, b: &'a $matrix_type<T>) -> $matrix_type<T> { &a * b});
|
|
|
|
impl_mul!(<T>(a: $matrix_type<T>, b: $matrix_type<T>) -> $matrix_type<T> { &a * &b});
|
2020-12-16 23:17:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-22 06:56:51 +08:00
|
|
|
impl_spmm!(CsrMatrix, spmm_csr_pattern, spmm_csr_prealloc_unchecked);
|
2020-12-30 23:09:46 +08:00
|
|
|
// Need to switch order of operations for CSC pattern
|
2022-03-22 06:56:51 +08:00
|
|
|
impl_spmm!(CscMatrix, spmm_csc_pattern, spmm_csc_prealloc_unchecked);
|
2021-01-04 20:39:41 +08:00
|
|
|
|
|
|
|
/// Implements Scalar * Matrix operations for *concrete* scalar types. The reason this is necessary
|
|
|
|
/// is that we are not able to implement Mul<Matrix<T>> for all T generically due to orphan rules.
|
|
|
|
macro_rules! impl_concrete_scalar_matrix_mul {
|
|
|
|
($matrix_type:ident, $($scalar_type:ty),*) => {
|
|
|
|
// For each concrete scalar type, forward the implementation of scalar * matrix
|
|
|
|
// to matrix * scalar, which we have already implemented through generics
|
|
|
|
$(
|
|
|
|
impl_mul!(<>(a: $scalar_type, b: $matrix_type<$scalar_type>)
|
|
|
|
-> $matrix_type<$scalar_type> { b * a });
|
|
|
|
impl_mul!(<'a>(a: $scalar_type, b: &'a $matrix_type<$scalar_type>)
|
|
|
|
-> $matrix_type<$scalar_type> { b * a });
|
|
|
|
impl_mul!(<'a>(a: &'a $scalar_type, b: $matrix_type<$scalar_type>)
|
|
|
|
-> $matrix_type<$scalar_type> { b * (*a) });
|
|
|
|
impl_mul!(<'a>(a: &'a $scalar_type, b: &'a $matrix_type<$scalar_type>)
|
|
|
|
-> $matrix_type<$scalar_type> { b * *a });
|
|
|
|
)*
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Implements multiplication between matrix and scalar for various matrix types
|
|
|
|
macro_rules! impl_scalar_mul {
|
|
|
|
($matrix_type: ident) => {
|
|
|
|
impl_mul!(<'a, T>(a: &'a $matrix_type<T>, b: &'a T) -> $matrix_type<T> {
|
|
|
|
let values: Vec<_> = a.values()
|
|
|
|
.iter()
|
2021-08-04 23:34:25 +08:00
|
|
|
.map(|v_i| v_i.clone() * b.clone())
|
2021-01-04 20:39:41 +08:00
|
|
|
.collect();
|
2021-01-19 23:53:39 +08:00
|
|
|
$matrix_type::try_from_pattern_and_values(a.pattern().clone(), values).unwrap()
|
2021-01-04 20:39:41 +08:00
|
|
|
});
|
|
|
|
impl_mul!(<'a, T>(a: &'a $matrix_type<T>, b: T) -> $matrix_type<T> {
|
|
|
|
a * &b
|
|
|
|
});
|
|
|
|
impl_mul!(<'a, T>(a: $matrix_type<T>, b: &'a T) -> $matrix_type<T> {
|
|
|
|
let mut a = a;
|
|
|
|
for value in a.values_mut() {
|
2021-08-04 23:34:25 +08:00
|
|
|
*value = b.clone() * value.clone();
|
2021-01-04 20:39:41 +08:00
|
|
|
}
|
|
|
|
a
|
|
|
|
});
|
|
|
|
impl_mul!(<T>(a: $matrix_type<T>, b: T) -> $matrix_type<T> {
|
|
|
|
a * &b
|
|
|
|
});
|
|
|
|
impl_concrete_scalar_matrix_mul!(
|
|
|
|
$matrix_type,
|
2021-01-05 21:59:54 +08:00
|
|
|
i8, i16, i32, i64, isize, f32, f64);
|
2021-01-04 20:39:41 +08:00
|
|
|
|
|
|
|
impl<T> MulAssign<T> for $matrix_type<T>
|
|
|
|
where
|
|
|
|
T: Scalar + ClosedAdd + ClosedMul + Zero + One
|
|
|
|
{
|
|
|
|
fn mul_assign(&mut self, scalar: T) {
|
|
|
|
for val in self.values_mut() {
|
2021-08-04 23:34:25 +08:00
|
|
|
*val *= scalar.clone();
|
2021-01-04 20:39:41 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, T> MulAssign<&'a T> for $matrix_type<T>
|
|
|
|
where
|
|
|
|
T: Scalar + ClosedAdd + ClosedMul + Zero + One
|
|
|
|
{
|
|
|
|
fn mul_assign(&mut self, scalar: &'a T) {
|
|
|
|
for val in self.values_mut() {
|
2021-08-04 23:34:25 +08:00
|
|
|
*val *= scalar.clone();
|
2021-01-04 20:39:41 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl_scalar_mul!(CsrMatrix);
|
2021-01-05 21:59:54 +08:00
|
|
|
impl_scalar_mul!(CscMatrix);
|
|
|
|
|
2021-01-06 18:04:49 +08:00
|
|
|
macro_rules! impl_neg {
|
|
|
|
($matrix_type:ident) => {
|
|
|
|
impl<T> Neg for $matrix_type<T>
|
|
|
|
where
|
2021-01-26 00:26:27 +08:00
|
|
|
T: Scalar + Neg<Output = T>,
|
2021-01-06 18:04:49 +08:00
|
|
|
{
|
|
|
|
type Output = $matrix_type<T>;
|
|
|
|
|
|
|
|
fn neg(mut self) -> Self::Output {
|
|
|
|
for v_i in self.values_mut() {
|
2021-08-04 23:34:25 +08:00
|
|
|
*v_i = -v_i.clone();
|
2021-01-06 18:04:49 +08:00
|
|
|
}
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, T> Neg for &'a $matrix_type<T>
|
|
|
|
where
|
2021-01-26 00:26:27 +08:00
|
|
|
T: Scalar + Neg<Output = T>,
|
2021-01-06 18:04:49 +08:00
|
|
|
{
|
|
|
|
type Output = $matrix_type<T>;
|
|
|
|
|
|
|
|
fn neg(self) -> Self::Output {
|
|
|
|
// TODO: This is inefficient. Ideally we'd have a method that would let us
|
|
|
|
// obtain both the sparsity pattern and values from the matrix,
|
|
|
|
// and then modify the values before creating a new matrix from the pattern
|
|
|
|
// and negated values.
|
2021-01-26 00:26:27 +08:00
|
|
|
-self.clone()
|
2021-01-06 18:04:49 +08:00
|
|
|
}
|
|
|
|
}
|
2021-01-26 00:26:27 +08:00
|
|
|
};
|
2021-01-06 18:04:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl_neg!(CsrMatrix);
|
|
|
|
impl_neg!(CscMatrix);
|
|
|
|
|
|
|
|
macro_rules! impl_div {
|
|
|
|
($matrix_type:ident) => {
|
|
|
|
impl_bin_op!(Div, div, <T: ClosedDiv>(matrix: $matrix_type<T>, scalar: T) -> $matrix_type<T> {
|
|
|
|
let mut matrix = matrix;
|
|
|
|
matrix /= scalar;
|
|
|
|
matrix
|
|
|
|
});
|
|
|
|
impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: $matrix_type<T>, scalar: &T) -> $matrix_type<T> {
|
2021-08-04 23:34:25 +08:00
|
|
|
matrix / scalar.clone()
|
2021-01-06 18:04:49 +08:00
|
|
|
});
|
|
|
|
impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: &'a $matrix_type<T>, scalar: T) -> $matrix_type<T> {
|
|
|
|
let new_values = matrix.values()
|
|
|
|
.iter()
|
2021-08-04 23:34:25 +08:00
|
|
|
.map(|v_i| v_i.clone() / scalar.clone())
|
2021-01-06 18:04:49 +08:00
|
|
|
.collect();
|
2021-01-19 23:53:39 +08:00
|
|
|
$matrix_type::try_from_pattern_and_values(matrix.pattern().clone(), new_values)
|
2021-01-06 18:04:49 +08:00
|
|
|
.unwrap()
|
|
|
|
});
|
|
|
|
impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: &'a $matrix_type<T>, scalar: &'a T) -> $matrix_type<T> {
|
2021-08-04 23:34:25 +08:00
|
|
|
matrix / scalar.clone()
|
2021-01-06 18:04:49 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
impl<T> DivAssign<T> for $matrix_type<T>
|
|
|
|
where T : Scalar + ClosedAdd + ClosedMul + ClosedDiv + Zero + One
|
|
|
|
{
|
|
|
|
fn div_assign(&mut self, scalar: T) {
|
2021-08-04 23:34:25 +08:00
|
|
|
self.values_mut().iter_mut().for_each(|v_i| *v_i /= scalar.clone());
|
2021-01-06 18:04:49 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, T> DivAssign<&'a T> for $matrix_type<T>
|
|
|
|
where T : Scalar + ClosedAdd + ClosedMul + ClosedDiv + Zero + One
|
|
|
|
{
|
|
|
|
fn div_assign(&mut self, scalar: &'a T) {
|
2021-08-04 23:34:25 +08:00
|
|
|
*self /= scalar.clone();
|
2021-01-06 18:04:49 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl_div!(CsrMatrix);
|
2021-01-06 20:07:55 +08:00
|
|
|
impl_div!(CscMatrix);
|
|
|
|
|
|
|
|
macro_rules! impl_spmm_cs_dense {
|
2021-01-25 19:08:57 +08:00
|
|
|
($matrix_type_name:ident, $spmm_fn:ident) => {
|
|
|
|
// Implement ref-ref
|
|
|
|
impl_spmm_cs_dense!(&'a $matrix_type_name<T>, &'a Matrix<T, R, C, S>, $spmm_fn, |lhs, rhs| {
|
2021-08-03 00:41:46 +08:00
|
|
|
let (_, ncols) = rhs.shape_generic();
|
2023-01-14 23:22:27 +08:00
|
|
|
let nrows = Dyn(lhs.nrows());
|
|
|
|
let mut result = OMatrix::<T, Dyn, C>::zeros_generic(nrows, ncols);
|
2021-01-25 19:08:57 +08:00
|
|
|
$spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(lhs), Op::NoOp(rhs));
|
|
|
|
result
|
|
|
|
});
|
|
|
|
|
|
|
|
// Implement the other combinations by deferring to ref-ref
|
|
|
|
impl_spmm_cs_dense!(&'a $matrix_type_name<T>, Matrix<T, R, C, S>, $spmm_fn, |lhs, rhs| {
|
|
|
|
lhs * &rhs
|
|
|
|
});
|
|
|
|
impl_spmm_cs_dense!($matrix_type_name<T>, &'a Matrix<T, R, C, S>, $spmm_fn, |lhs, rhs| {
|
|
|
|
&lhs * rhs
|
|
|
|
});
|
|
|
|
impl_spmm_cs_dense!($matrix_type_name<T>, Matrix<T, R, C, S>, $spmm_fn, |lhs, rhs| {
|
|
|
|
&lhs * &rhs
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Main body of the macro. The first pattern just forwards to this pattern but with
|
|
|
|
// different arguments
|
|
|
|
($sparse_matrix_type:ty, $dense_matrix_type:ty, $spmm_fn:ident,
|
|
|
|
|$lhs:ident, $rhs:ident| $body:tt) =>
|
|
|
|
{
|
|
|
|
impl<'a, T, R, C, S> Mul<$dense_matrix_type> for $sparse_matrix_type
|
2021-01-06 20:07:55 +08:00
|
|
|
where
|
|
|
|
T: Scalar + ClosedMul + ClosedAdd + ClosedSub + ClosedDiv + Neg + Zero + One,
|
|
|
|
R: Dim,
|
|
|
|
C: Dim,
|
2021-08-03 00:41:46 +08:00
|
|
|
S: RawStorage<T, R, C>,
|
2023-01-14 23:22:27 +08:00
|
|
|
DefaultAllocator: Allocator<T, Dyn, C>,
|
2021-01-23 00:56:26 +08:00
|
|
|
// TODO: Is it possible to simplify these bounds?
|
|
|
|
ShapeConstraint:
|
2023-01-14 23:22:27 +08:00
|
|
|
// Bounds so that we can turn OMatrix<T, Dyn, C> into a DMatrixSliceMut
|
|
|
|
DimEq<U1, <<DefaultAllocator as Allocator<T, Dyn, C>>::Buffer as RawStorage<T, Dyn, C>>::RStride>
|
|
|
|
+ DimEq<C, Dyn>
|
|
|
|
+ DimEq<Dyn, <<DefaultAllocator as Allocator<T, Dyn, C>>::Buffer as RawStorage<T, Dyn, C>>::CStride>
|
2021-01-23 00:56:26 +08:00
|
|
|
// Bounds so that we can turn &Matrix<T, R, C, S> into a DMatrixSlice
|
|
|
|
+ DimEq<U1, S::RStride>
|
2023-01-14 23:22:27 +08:00
|
|
|
+ DimEq<R, Dyn>
|
|
|
|
+ DimEq<Dyn, S::CStride>
|
2021-01-06 20:07:55 +08:00
|
|
|
{
|
2021-01-23 00:56:26 +08:00
|
|
|
// We need the column dimension to be generic, so that if RHS is a vector, then
|
|
|
|
// we also get a vector (and not a matrix)
|
2023-01-14 23:22:27 +08:00
|
|
|
type Output = OMatrix<T, Dyn, C>;
|
2021-01-06 20:07:55 +08:00
|
|
|
|
2021-01-25 19:08:57 +08:00
|
|
|
fn mul(self, rhs: $dense_matrix_type) -> Self::Output {
|
|
|
|
let $lhs = self;
|
|
|
|
let $rhs = rhs;
|
|
|
|
$body
|
2021-01-06 20:07:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl_spmm_cs_dense!(CsrMatrix, spmm_csr_dense);
|
2021-01-26 00:26:27 +08:00
|
|
|
impl_spmm_cs_dense!(CscMatrix, spmm_csc_dense);
|