
Serializable dense and sparse matrix and vector containers, BLAS-like arithmetic and factorizations, sparse Markowitz LU and EtaBasis updates, with runtime-swappable high-performance compute backend.
Koblas provides dense and sparse linear algebra for Kotlin Multiplatform: a well-defined subset of double-precision BLAS and LAPACK, sparse LU factorization, and a swappable compute backend that uses the host OpenBLAS on the Linux and macOS native targets when it is installed.
Kotlin has no standard multiplatform linear algebra library. Koblas is a small one built around serializable containers: DenseMatrix (flat, column-major), DenseVector and SparseVector behind a shared view contract, and a CSC SparseMatrix, transposable in place of a CSR conversion. Arithmetic lives as free functions over the views; the heavier dense operations sit behind the swappable Blas and Lapack interfaces.
The operation set covers level 1 through 3 BLAS kernels, the LU, Cholesky, QR, and symmetric indefinite solver families with condition estimation, and sparse LU and symmetric factorizations. See BLAS Coverage for the exact contract and what is out of scope.
implementation("com.eignex:koblas:<version>")The serializable containers need the kotlinx.serialization runtime; there are no other dependencies.
Solve a general dense system via LU:
val rows = arrayOf(doubleArrayOf(2.0, 1.0), doubleArrayOf(1.0, 3.0))
val a = DenseMatrix.of(rows)
val x = a.lu().solve(doubleArrayOf(3.0, 5.0))Solve a symmetric positive-definite system via Cholesky:
val l = a.cholesky() // A = L·Lᵀ; throws if A is not positive-definite
val xs = solveSpd(l, doubleArrayOf(3.0, 5.0))An estimate that has drifted slightly indefinite can ask for the nearby factor instead of an exception, which is a decision the caller makes rather than a default:
val l = a.cholesky(CholeskyPolicy.Regularize()) // floors non-positive pivotsFactorize a sparse basis and solve both directions:
val cols = listOf(listOf(0 to 2.0, 1 to 1.0), listOf(0 to 1.0, 1 to 3.0))
val s = SparseMatrix.ofColumns(2, 2, cols)
val lu = s.lu()
val forward = lu.solve(doubleArrayOf(3.0, 5.0)) // B x = b
val backward = lu.solve(doubleArrayOf(3.0, 5.0), transpose = true) // Bᵀ x = bEvery routine in this table is implemented in portable Kotlin and tested against reference results on every target.
| Standard routine | Koblas |
|---|---|
| Level 1 (vector) | |
| ddot, daxpy, dscal | dot, axpy, scale (sparse-aware) |
| dnrm2, dasum, idamax | norm2, asum, iamax |
| dcopy, dswap | copy, swap |
| drotg, drot (Givens rotation) | rotg, rot |
| Level 2 (matrix-vector, on Blas) | |
| dgemv (full alpha/beta form) | gemv |
| dger (rank-one update) | ger |
| dsyr, dsyr2 (symmetric rank-1/2 update) | syr, syr2 |
| dsymv (symmetric multiply) | symv |
| dtrsv (triangular solve) | trsv |
| dtrmv (triangular multiply) | trmv |
| Level 3 (matrix-matrix, on Blas) | |
| dgemm (full form, transpose flags) | gemm |
| dsymm (symmetric multiply) | symm |
| dsyrk (symmetric rank-k update) | syrk |
| dsyr2k (symmetric rank-2k update) | syr2k |
| dtrsm (triangular solve, multi-RHS) | trsm |
| dtrmm (triangular multiply, multi-RHS) | trmm |
| LAPACK (factorizations, on Lapack) | |
| dgetrf, dgetrs, dgetri (LU) | factor, solve, invert; determinant is free |
| dgecon, dlange (condition estimate, norms) | rcond; norm1, normInf, normFro are free |
| dtrtri (triangular inverse) | trtri |
| dpotrf, dpotrs, dpotri (Cholesky) | cholesky, solveSpd, invertSpd |
| dgeqrf, dormqr, dgels (QR) | qr, applyQ, solveLeastSquares, solveMinimumNorm |
| dgeqp3 (QR with column pivoting) | qrPivoted, reporting a numerical rank |
| dsytrf, dsytrs (symmetric indefinite LDLᵀ) | ldl, solve |
| Sparse (on SparseBlas and SparseLapack) | |
| sparse matrix-vector product, both directions | gemv |
| sparse triangular solve | trsv |
| unsymmetric sparse LU (Markowitz pivoting) | lu, solve both directions, determinant |
| symmetric sparse LDLᵀ and Cholesky | analyze, ldl, cholesky; the analysis is reusable |
| fill-reducing ordering (minimum degree) | on by default in analyze; SparseOrdering.Natural opts out |
Semantics follow the standard; the exceptions are documented on each function. Factorizations use the LAPACK packed formats, so they interchange between backends.
The API is split by storage. com.eignex.koblas holds the containers and the
free-function arithmetic over them; com.eignex.koblas.dense holds the dense
seams and routines, com.eignex.koblas.sparse the sparse ones. The containers
stay together in the parent package because the view roots are sealed, which is
what gives a serialized snapshot a closed set of concrete storage types.
The dense seam is three interfaces, each named for what it covers: VectorKernels holds the vector-vector routines, Blas the matrix ones, Lapack the factorizations built on them, and LinearAlgebra is the Blas and Lapack pair. All three are ranked and selected independently, so a host providing one library and not the other still accelerates what it can, and koblas composes the winning halves.
The sparse seam mirrors it one for one -- SparseVectorKernels, SparseBlas, SparseLapack, SparseLinearAlgebra -- and shares its machinery rather than paralleling it: one registerBackend, one priority ranking per half, one registry, one KoblasContext holding all six. Knowing one side is knowing the other. Where they genuinely differ is documented at the declaration: the sparse vector kernels have no length threshold, and a sparse factorization is an interface rather than a class because no host solver will describe its factors.
Each group above names the interface its routines belong to, and a member reaches the installed backend; determinant and norm1 are the two plain functions here, marked as such. Members also have a free-function spelling of the same name (trsv, trsm, ger, ...) that forwards to the member, so a call site may use whichever reads better.
Level 1 is reached differently from the other two, because those kernels do nanoseconds of work and a virtual call per invocation would cost more than the kernel. They are specialized at compile time, and consult the VectorKernels interface only once a run is long enough to cover a foreign call. Three of the level 1 routines stay off that seam on purpose: iamax because its tie-breaking and NaN ranking are koblas's own contract and idamax implementations disagree about the latter, copy and swap because copyInto already beats a foreign call.
Out of scope: single precision, complex numbers, banded and packed storage layouts, SVD, and eigendecompositions. Nothing is supported silently: new routines are implemented, tested, and added to the table when a workload needs them.
The routines a steady-state loop repeats have a destination-passing form, so a loop that owns its buffers allocates nothing: solveInto for the dense and symmetric indefinite solves, single or blocked, applyQInto and the two least-squares solves for the QR family, solveInto for the sparse factorization, and factorInto to refactorize into existing factor buffers.
A few routines also need scratch of their own, and those take an optional Workspace: a pool of vectors keyed by width, which you create and hand over. Operations borrow and return buffers, so nesting is safe and one workspace serves whatever dimensions a caller mixes; pools grow on demand, and reserve pays that cost up front. It is accepted wherever a routine needs temporaries: rcond, the syrk mirror buffer (an n² scratch, the largest in the library), ldl, qr, invertSpd, and the blocked multi-RHS solves' staging. norm1 is the routine you call alongside rcond to decide whether a factorization is still accurate enough to reuse, but it takes no workspace and needs none: a column is contiguous under column-major storage, so each column sum finishes before the next begins and one accumulator suffices.
val ws = Workspace().apply { reserve(n, count = 5) }
val x = DoubleArray(n)
repeat(iterations) {
basis.solveInto(b, x, workspace = ws) // no allocation
if (koblas.rcond(lu, anorm, ws) < threshold) koblas.factorInto(a, lu)
}A Workspace is caller-owned and not thread-safe: give each solver its own. Passing none keeps the allocating behaviour, which is always correct.
Backends register themselves through registerBackend, which offers one object as every half it implements, and are ranked by priority, one ranking per interface: whatever the platform provides arrives through registration, and installBackends overrides it with a KoblasContext of your own.
Discovery runs once, on the first read of koblas, on every platform. It resolves
the host libraries this build ships bindings for -- OpenBLAS and SuiteSparse's
UMFPACK -- by soname, and on the JVM it then scans the classpath for third-party
LinearAlgebra providers. A library that is not installed simply is not
registered, and the portable implementation stays in place, so nothing has to be
configured for either case. On the JVM, -Dkoblas.backend=reference registers
nothing at all, and any other value registers only the backend whose name
matches.
Selection is global by default but does not have to be. A KoblasContext holds all
six halves -- the three dense and three sparse -- and is itself a backend, so
context.gemv(...) works wherever koblas.gemv(...) does. koblas.with(blas =
mine) copies the default and replaces one half, which is what tests, benchmarks
and reproducible runs want instead of mutating process state.
What differs between the interfaces is not how a backend is selected but when it
is consulted. The level 2 and 3 multiplies and the factorizations amortize a
virtual call, so every invocation goes through the interface. The level 1
kernels do not, so they are specialized at compile time -- the JVM uses the
incubator Vector API when started with --add-modules=jdk.incubator.vector, and
the other targets use scalar loops -- and reach a registered VectorKernels backend only
for runs at least as long as the level 1 threshold, 64 elements on the native
targets. Every dense inner loop bottoms out here. The sparse kernels have their
own seam and their own reference, and consult it unconditionally -- there is no
compiled-in sparse primitive for a foreign call to have to beat.
Each threshold has a name and an override: koblas.dispatch.level1 and its
level2, level3 and lapack counterparts, as a JVM system property or as
KOBLAS_DISPATCH_LEVEL1 in the environment elsewhere. Storage
is a flat, column-major DoubleArray -- the order LAPACK and Fortran define -- so
a native backend receives raw buffers with no repacking and no row-major wrapper
layer, and every backend must match the reference on the conformance suite.
The sparse factorization has a host backend too: SuiteSparse's UMFPACK, bound on the JVM through java.lang.foreign and resolved by soname, so it activates on any machine with SuiteSparse installed and is absent otherwise. A SparseMatrix crosses to umfpack_di_* with no repacking at all -- koblas's CSC invariant is UMFPACK's stated precondition, checked against the headers. Nothing is bundled; without the library koblas's own Markowitz SparseLu keeps the seam.
On the Linux and macOS native targets koblas resolves the host OpenBLAS with dlopen at program start (libopenblas and liblapacke on Debian/Ubuntu, brew install openblas on macOS) and uses it for the level 2 and 3 routines, the factorizations, and level-1 runs long enough to cover a foreign call. Nothing is bundled and nothing is linked: a host without the libraries runs the portable kernels, so a shipped binary works either way. It matters most here, because these targets have no vector kernels: measured on linuxX64 the host library wins level 2 by 2x to 15x and dense factorization by up to 13x. OPENBLAS_NUM_THREADS opts into threading; the default is single-threaded.
That fallback is silent, which is the right default and a bad thing to discover in production. Ask, when it matters:
koblas.requireAccelerated(BackendSlot.Blas, BackendSlot.Lapack) // throws, naming what fell back
println(koblas.portableSlots) // or just lookAccelerated means a registered backend is being consulted. The compiled-in SIMD kernels do not count, however fast they are -- they are what you get with no host library, so mathBackend reporting simd is not evidence that one was found. The slots are named explicitly because which ones can be accelerated depends on the target and on what koblas ships: there is no host sparse backend yet, so the three sparse slots are portable everywhere today.
The JVM resolves the same host OpenBLAS through java.lang.foreign, binding it
with Linker.Option.critical so a DoubleArray is pinned rather than copied.
Only the routines that win go native: the level-3 products, the LU, LDL and QR
factorizations, the blocked multi-RHS solve, and the condition estimate. The
level-2 routines, the single-vector solves and the whole Cholesky family stay on
the Vector API kernels, which beat a foreign call there — measured, SIMD
Cholesky matches single-threaded OpenBLAS to n=1024 and wins outright at 256.
This is why the JVM artifact targets Java 25: FFM was finalized in 22, and the
backend ships inside koblas rather than as a separate artifact. Native access is
a restricted operation, so pass --enable-native-access=ALL-UNNAMED to silence
the warning; without it the backend still works on 25. A machine with no
OpenBLAS runs the portable kernels, as everywhere else.
The ServiceLoader seam remains for a consumer who wants to supply their own backend, and it outranks the built-in one when its priority is higher.
The seams are independent; print what a runtime resolved with:
println(koblasInfo) // backend=cblas, kernels=scalar+hostKoblas provides dense and sparse linear algebra for Kotlin Multiplatform: a well-defined subset of double-precision BLAS and LAPACK, sparse LU factorization, and a swappable compute backend that uses the host OpenBLAS on the Linux and macOS native targets when it is installed.
Kotlin has no standard multiplatform linear algebra library. Koblas is a small one built around serializable containers: DenseMatrix (flat, column-major), DenseVector and SparseVector behind a shared view contract, and a CSC SparseMatrix, transposable in place of a CSR conversion. Arithmetic lives as free functions over the views; the heavier dense operations sit behind the swappable Blas and Lapack interfaces.
The operation set covers level 1 through 3 BLAS kernels, the LU, Cholesky, QR, and symmetric indefinite solver families with condition estimation, and sparse LU and symmetric factorizations. See BLAS Coverage for the exact contract and what is out of scope.
implementation("com.eignex:koblas:<version>")The serializable containers need the kotlinx.serialization runtime; there are no other dependencies.
Solve a general dense system via LU:
val rows = arrayOf(doubleArrayOf(2.0, 1.0), doubleArrayOf(1.0, 3.0))
val a = DenseMatrix.of(rows)
val x = a.lu().solve(doubleArrayOf(3.0, 5.0))Solve a symmetric positive-definite system via Cholesky:
val l = a.cholesky() // A = L·Lᵀ; throws if A is not positive-definite
val xs = solveSpd(l, doubleArrayOf(3.0, 5.0))An estimate that has drifted slightly indefinite can ask for the nearby factor instead of an exception, which is a decision the caller makes rather than a default:
val l = a.cholesky(CholeskyPolicy.Regularize()) // floors non-positive pivotsFactorize a sparse basis and solve both directions:
val cols = listOf(listOf(0 to 2.0, 1 to 1.0), listOf(0 to 1.0, 1 to 3.0))
val s = SparseMatrix.ofColumns(2, 2, cols)
val lu = s.lu()
val forward = lu.solve(doubleArrayOf(3.0, 5.0)) // B x = b
val backward = lu.solve(doubleArrayOf(3.0, 5.0), transpose = true) // Bᵀ x = bEvery routine in this table is implemented in portable Kotlin and tested against reference results on every target.
| Standard routine | Koblas |
|---|---|
| Level 1 (vector) | |
| ddot, daxpy, dscal | dot, axpy, scale (sparse-aware) |
| dnrm2, dasum, idamax | norm2, asum, iamax |
| dcopy, dswap | copy, swap |
| drotg, drot (Givens rotation) | rotg, rot |
| Level 2 (matrix-vector, on Blas) | |
| dgemv (full alpha/beta form) | gemv |
| dger (rank-one update) | ger |
| dsyr, dsyr2 (symmetric rank-1/2 update) | syr, syr2 |
| dsymv (symmetric multiply) | symv |
| dtrsv (triangular solve) | trsv |
| dtrmv (triangular multiply) | trmv |
| Level 3 (matrix-matrix, on Blas) | |
| dgemm (full form, transpose flags) | gemm |
| dsymm (symmetric multiply) | symm |
| dsyrk (symmetric rank-k update) | syrk |
| dsyr2k (symmetric rank-2k update) | syr2k |
| dtrsm (triangular solve, multi-RHS) | trsm |
| dtrmm (triangular multiply, multi-RHS) | trmm |
| LAPACK (factorizations, on Lapack) | |
| dgetrf, dgetrs, dgetri (LU) | factor, solve, invert; determinant is free |
| dgecon, dlange (condition estimate, norms) | rcond; norm1, normInf, normFro are free |
| dtrtri (triangular inverse) | trtri |
| dpotrf, dpotrs, dpotri (Cholesky) | cholesky, solveSpd, invertSpd |
| dgeqrf, dormqr, dgels (QR) | qr, applyQ, solveLeastSquares, solveMinimumNorm |
| dgeqp3 (QR with column pivoting) | qrPivoted, reporting a numerical rank |
| dsytrf, dsytrs (symmetric indefinite LDLᵀ) | ldl, solve |
| Sparse (on SparseBlas and SparseLapack) | |
| sparse matrix-vector product, both directions | gemv |
| sparse triangular solve | trsv |
| unsymmetric sparse LU (Markowitz pivoting) | lu, solve both directions, determinant |
| symmetric sparse LDLᵀ and Cholesky | analyze, ldl, cholesky; the analysis is reusable |
| fill-reducing ordering (minimum degree) | on by default in analyze; SparseOrdering.Natural opts out |
Semantics follow the standard; the exceptions are documented on each function. Factorizations use the LAPACK packed formats, so they interchange between backends.
The API is split by storage. com.eignex.koblas holds the containers and the
free-function arithmetic over them; com.eignex.koblas.dense holds the dense
seams and routines, com.eignex.koblas.sparse the sparse ones. The containers
stay together in the parent package because the view roots are sealed, which is
what gives a serialized snapshot a closed set of concrete storage types.
The dense seam is three interfaces, each named for what it covers: VectorKernels holds the vector-vector routines, Blas the matrix ones, Lapack the factorizations built on them, and LinearAlgebra is the Blas and Lapack pair. All three are ranked and selected independently, so a host providing one library and not the other still accelerates what it can, and koblas composes the winning halves.
The sparse seam mirrors it one for one -- SparseVectorKernels, SparseBlas, SparseLapack, SparseLinearAlgebra -- and shares its machinery rather than paralleling it: one registerBackend, one priority ranking per half, one registry, one KoblasContext holding all six. Knowing one side is knowing the other. Where they genuinely differ is documented at the declaration: the sparse vector kernels have no length threshold, and a sparse factorization is an interface rather than a class because no host solver will describe its factors.
Each group above names the interface its routines belong to, and a member reaches the installed backend; determinant and norm1 are the two plain functions here, marked as such. Members also have a free-function spelling of the same name (trsv, trsm, ger, ...) that forwards to the member, so a call site may use whichever reads better.
Level 1 is reached differently from the other two, because those kernels do nanoseconds of work and a virtual call per invocation would cost more than the kernel. They are specialized at compile time, and consult the VectorKernels interface only once a run is long enough to cover a foreign call. Three of the level 1 routines stay off that seam on purpose: iamax because its tie-breaking and NaN ranking are koblas's own contract and idamax implementations disagree about the latter, copy and swap because copyInto already beats a foreign call.
Out of scope: single precision, complex numbers, banded and packed storage layouts, SVD, and eigendecompositions. Nothing is supported silently: new routines are implemented, tested, and added to the table when a workload needs them.
The routines a steady-state loop repeats have a destination-passing form, so a loop that owns its buffers allocates nothing: solveInto for the dense and symmetric indefinite solves, single or blocked, applyQInto and the two least-squares solves for the QR family, solveInto for the sparse factorization, and factorInto to refactorize into existing factor buffers.
A few routines also need scratch of their own, and those take an optional Workspace: a pool of vectors keyed by width, which you create and hand over. Operations borrow and return buffers, so nesting is safe and one workspace serves whatever dimensions a caller mixes; pools grow on demand, and reserve pays that cost up front. It is accepted wherever a routine needs temporaries: rcond, the syrk mirror buffer (an n² scratch, the largest in the library), ldl, qr, invertSpd, and the blocked multi-RHS solves' staging. norm1 is the routine you call alongside rcond to decide whether a factorization is still accurate enough to reuse, but it takes no workspace and needs none: a column is contiguous under column-major storage, so each column sum finishes before the next begins and one accumulator suffices.
val ws = Workspace().apply { reserve(n, count = 5) }
val x = DoubleArray(n)
repeat(iterations) {
basis.solveInto(b, x, workspace = ws) // no allocation
if (koblas.rcond(lu, anorm, ws) < threshold) koblas.factorInto(a, lu)
}A Workspace is caller-owned and not thread-safe: give each solver its own. Passing none keeps the allocating behaviour, which is always correct.
Backends register themselves through registerBackend, which offers one object as every half it implements, and are ranked by priority, one ranking per interface: whatever the platform provides arrives through registration, and installBackends overrides it with a KoblasContext of your own.
Discovery runs once, on the first read of koblas, on every platform. It resolves
the host libraries this build ships bindings for -- OpenBLAS and SuiteSparse's
UMFPACK -- by soname, and on the JVM it then scans the classpath for third-party
LinearAlgebra providers. A library that is not installed simply is not
registered, and the portable implementation stays in place, so nothing has to be
configured for either case. On the JVM, -Dkoblas.backend=reference registers
nothing at all, and any other value registers only the backend whose name
matches.
Selection is global by default but does not have to be. A KoblasContext holds all
six halves -- the three dense and three sparse -- and is itself a backend, so
context.gemv(...) works wherever koblas.gemv(...) does. koblas.with(blas =
mine) copies the default and replaces one half, which is what tests, benchmarks
and reproducible runs want instead of mutating process state.
What differs between the interfaces is not how a backend is selected but when it
is consulted. The level 2 and 3 multiplies and the factorizations amortize a
virtual call, so every invocation goes through the interface. The level 1
kernels do not, so they are specialized at compile time -- the JVM uses the
incubator Vector API when started with --add-modules=jdk.incubator.vector, and
the other targets use scalar loops -- and reach a registered VectorKernels backend only
for runs at least as long as the level 1 threshold, 64 elements on the native
targets. Every dense inner loop bottoms out here. The sparse kernels have their
own seam and their own reference, and consult it unconditionally -- there is no
compiled-in sparse primitive for a foreign call to have to beat.
Each threshold has a name and an override: koblas.dispatch.level1 and its
level2, level3 and lapack counterparts, as a JVM system property or as
KOBLAS_DISPATCH_LEVEL1 in the environment elsewhere. Storage
is a flat, column-major DoubleArray -- the order LAPACK and Fortran define -- so
a native backend receives raw buffers with no repacking and no row-major wrapper
layer, and every backend must match the reference on the conformance suite.
The sparse factorization has a host backend too: SuiteSparse's UMFPACK, bound on the JVM through java.lang.foreign and resolved by soname, so it activates on any machine with SuiteSparse installed and is absent otherwise. A SparseMatrix crosses to umfpack_di_* with no repacking at all -- koblas's CSC invariant is UMFPACK's stated precondition, checked against the headers. Nothing is bundled; without the library koblas's own Markowitz SparseLu keeps the seam.
On the Linux and macOS native targets koblas resolves the host OpenBLAS with dlopen at program start (libopenblas and liblapacke on Debian/Ubuntu, brew install openblas on macOS) and uses it for the level 2 and 3 routines, the factorizations, and level-1 runs long enough to cover a foreign call. Nothing is bundled and nothing is linked: a host without the libraries runs the portable kernels, so a shipped binary works either way. It matters most here, because these targets have no vector kernels: measured on linuxX64 the host library wins level 2 by 2x to 15x and dense factorization by up to 13x. OPENBLAS_NUM_THREADS opts into threading; the default is single-threaded.
That fallback is silent, which is the right default and a bad thing to discover in production. Ask, when it matters:
koblas.requireAccelerated(BackendSlot.Blas, BackendSlot.Lapack) // throws, naming what fell back
println(koblas.portableSlots) // or just lookAccelerated means a registered backend is being consulted. The compiled-in SIMD kernels do not count, however fast they are -- they are what you get with no host library, so mathBackend reporting simd is not evidence that one was found. The slots are named explicitly because which ones can be accelerated depends on the target and on what koblas ships: there is no host sparse backend yet, so the three sparse slots are portable everywhere today.
The JVM resolves the same host OpenBLAS through java.lang.foreign, binding it
with Linker.Option.critical so a DoubleArray is pinned rather than copied.
Only the routines that win go native: the level-3 products, the LU, LDL and QR
factorizations, the blocked multi-RHS solve, and the condition estimate. The
level-2 routines, the single-vector solves and the whole Cholesky family stay on
the Vector API kernels, which beat a foreign call there — measured, SIMD
Cholesky matches single-threaded OpenBLAS to n=1024 and wins outright at 256.
This is why the JVM artifact targets Java 25: FFM was finalized in 22, and the
backend ships inside koblas rather than as a separate artifact. Native access is
a restricted operation, so pass --enable-native-access=ALL-UNNAMED to silence
the warning; without it the backend still works on 25. A machine with no
OpenBLAS runs the portable kernels, as everywhere else.
The ServiceLoader seam remains for a consumer who wants to supply their own backend, and it outranks the built-in one when its priority is higher.
The seams are independent; print what a runtime resolved with:
println(koblasInfo) // backend=cblas, kernels=scalar+host