koblas

HostLapack

The host LAPACKE as the JVM's Lapack half.

Native where the work is O(n^3): the LU, LDL and QR factorizations, the blocked multi-RHS solve, and the condition estimate. The single-vector solves delegate to the portable kernels, as on every other platform.

The Cholesky family keeps its portable defaults, which is a measurement rather than an omission: the SIMD factorization beat single-threaded OpenBLAS at n=256 (696us against 1524us), matched it at 1024 (49.2ms against 51.4ms), and trailed only at 2048 (635ms against 326ms). Overriding it would make the common sizes slower.

Properties

name

open override val name: String(source)

A short backend identifier for diagnostics (e.g. "reference").

priority

open override val priority: Int(source)

Above the reference (0) and the native dlopen backend (90).

Functions

applyQInto

open override fun applyQInto(qr: QrDecomposition, y: DoubleArray, out: DoubleArray, transpose: Boolean = false): DoubleArray(source)

Apply Q (or Qᵀ when transpose) from qr to y into out, which is returned. out may be y.

cholesky

open override fun cholesky(a: MatrixLike, policy: CholeskyPolicy = CholeskyPolicy.Strict): DenseMatrix(source)

dpotrf above JVM_CHOLESKY_MIN, portable below it, with three adjustments to match the contract.

LAPACK factorizes in place and leaves the strict upper triangle exactly as the input had it, while koblas returns a factor whose upper triangle is zero, so it is cleared. LAPACK has no equivalent of CholeskyPolicy.Regularize: it reports the failing leading minor instead of clamping the pivot, so info > 0 falls back to the portable path, which is what applies the clamp or throws per the flag. And only the lower triangle of the input is read, matching what koblas promises its callers.

The threshold is where the measurement puts it, not at the LAPACK default: SIMD won 2.19x at n=256, tied at 1024, and lost 1.95x at 2048, so this gate opens late.

factor

open override fun factor(a: DenseMatrix): LuDecomposition(source)

LU factorization with partial pivoting of a square a (LAPACK dgetrf); a is not modified. Allocates the factor buffers; factorInto refactorizes into existing ones.

invertSpd

open override fun invertSpd(L: DenseMatrix, workspace: Workspace? = null): DenseMatrix(source)

dpotri above JVM_INVERT_SPD_MIN. It writes only the triangle it is given, where koblas returns the full symmetric inverse, so the result is mirrored; and it overwrites the factor with the inverse, so the factor is copied first rather than destroyed.

ldl

open override fun ldl(a: DenseMatrix, workspace: Workspace? = null): LdlDecomposition(source)

Symmetric indefinite factorization A = L·D·Lᵀ with Bunch–Kaufman partial pivoting (LAPACK dsytrf, lower). As with Blas.symv, only the lower triangle of a is read — the strictly upper triangle may hold anything — and a is not modified. Use this where the matrix is symmetric but not positive definite (KKT systems); for SPD matrices cholesky is cheaper.

qr

open override fun qr(a: DenseMatrix, workspace: Workspace? = null): QrDecomposition(source)

QR factorization A = Q·R of an m×n a via Householder reflections (LAPACK dgeqrf); a is not modified and any shape is accepted. Rank deficiency is not detected — zero diagonal entries of R surface in solveLeastSquares as infinities/NaNs, following the triangular-solve convention.

rcond

open override fun rcond(lu: LuDecomposition, anorm: Double, workspace: Workspace? = null): Double(source)

Reciprocal condition number estimate 1 / (anorm · est(‖A⁻¹‖₁)) from a factorization (LAPACK dgecon). anorm is the 1-norm of the original, unfactored matrix (see norm1), which the caller computes before factoring. Returns 1.0 for the empty factorization and exactly 0.0 when lu is singular or anorm is zero.

This is an order-of-magnitude estimate, not an exact condition number: the estimator never exceeds the true ‖A⁻¹‖₁, so the returned value never understates the conditioning. It is the cheap signal a revised simplex uses to decide when to refactorize. The default implementation is a Hager-style 1-norm estimator over solveInto; backends may substitute a native estimator, so the exact value can differ between backends while agreeing in magnitude.

The estimator needs four vectors and runs several sweeps, so it is the allocation-heaviest routine here — which matters because a simplex calls it to decide when to refactorize. Passing a workspace reuses those buffers across calls and makes it allocation-free.

solveInto

open override fun solveInto(lu: LuDecomposition, b: DoubleArray, out: DoubleArray, transpose: Boolean = false, workspace: Workspace? = null): DoubleArray(source)

Delegated to ReferenceLinearAlgebra for the same reason as Blas.gemv: two triangular solves over the factor are O(n²) work on O(n²) data, so the per-call cost dominates. Measured at n 256 the portable path takes 25 us against 65-136 us through cblas_dtrsv. The blocked solve below keeps dtrsm, which amortizes the same cost across many right-hand sides.


open override fun solveInto(lu: LuDecomposition, b: DenseMatrix, out: DenseMatrix, transpose: Boolean = false, workspace: Workspace? = null): DenseMatrix(source)

Solve A · X = B (or Aᵀ · X = B when transpose) into out, which is returned. out may be b. The transposed direction stages a block before scattering its rows through the permutation, so pass a workspace to lend that n·nrhs buffer.


open override fun solveInto(ldl: LdlDecomposition, b: DenseMatrix, out: DenseMatrix, workspace: Workspace? = null): DenseMatrix(source)

Solve A · X = B into out, which is returned. out may be b.

One dsytrs replay per right-hand side, over the contiguous column it occupies. Under row-major storage this was a single widened pass, because extracting a column meant a strided gather; here a column is already a contiguous run, so reusing the tuned vector solve costs nothing but re-reading the factor and keeps the pivot-block bookkeeping in one place instead of two.


Gated by JVM_LDL_SOLVE_MIN rather than by the LAPACK threshold, and disabled by it.

A vector solve is O(n^2) work over O(n^2) data, so it behaves like level 2 and not like the factorization it belongs to. Measured against SIMD it stayed inside the noise band at every size — 1.10x, 1.02x, 1.08x, 1.02x at n=16..128, and SIMD ahead 1.08x at 256 — so there is nothing to win. The gate exists so that is a value someone can change and re-measure, not a decision welded shut.

solveSpd

open override fun solveSpd(L: DenseMatrix, b: DoubleArray): DoubleArray(source)

Gated by JVM_SOLVE_SPD_MIN, which shuts it: dpotrs lost to the SIMD kernels by 3x at n=256 and 12x at 2048. It is O(n^2) work over O(n^2) data, so there is nothing to amortize a call against.

Those numbers were taken under row-major storage, when a second cause compounded the first: LAPACKE transposed the matrix into a column-major temporary on every call. That cost is gone now that koblas stores what LAPACK wants, so the margin here is narrower than measured and the gate is a stale upper bound rather than a current answer; see the note above the constants below.

Link copied to clipboard
open fun applyQ(qr: QrDecomposition, y: DoubleArray, transpose: Boolean = false): DoubleArray

Apply Q (or Qᵀ when transpose) from qr to a length-m y, into a fresh result (LAPACK dormqr restricted to a single column). Allocates; applyQInto does not.

Link copied to clipboard

Refactorize a into out's existing buffers, returning out. A periodic refactorization — the simplex rebuilding its basis, a filter re-decomposing a covariance — otherwise allocates an copy and a pivot array each time; this reuses both. out must have the same dimension as a, and its previous contents are discarded.

Link copied to clipboard
open fun invert(lu: LuDecomposition, workspace: Workspace? = null): DenseMatrix

Invert a general matrix from its LU factorization: returns A⁻¹ given P·A = L·U (LAPACK dgetri).

Link copied to clipboard

Solve A · X = B for the b.cols right-hand-side columns of b at once against a symmetric indefinite factorization (LAPACK dsytrs with nrhs); returns a fresh X. Backends may substitute a native block solve.

Solve A · x = b for a symmetric indefinite factorization ldl (LAPACK dsytrs); returns a fresh x. Symmetry makes the transposed solve identical, so there is no transpose flag. Allocates the result; solveInto writes into a caller-owned destination instead.

open fun solve(lu: LuDecomposition, b: DenseMatrix, transpose: Boolean = false): DenseMatrix

Solve A · X = B (or Aᵀ · X = B when transpose) for the b.cols right-hand-side columns of b at once (LAPACK dgetrs with nrhs); returns a fresh X. The default runs the permutation and the two triangular block solves directly on the shared packed format; backends may substitute a native block solve.

open fun solve(lu: LuDecomposition, b: DoubleArray, transpose: Boolean = false): DoubleArray

Solve A · x = b (or Aᵀ · x = b when transpose) for the factorization lu (LAPACK dgetrs); returns a fresh x. transpose serves the simplex's BTRAN (Bᵀ y = c) against a factored basis. Allocates the result; solveInto writes into a caller-owned destination instead.

Link copied to clipboard
open fun solveLeastSquares(qr: QrDecomposition, b: DoubleArray, workspace: Workspace? = null): DoubleArray

Least-squares solve min ‖A·x − b‖₂ from the factorization (the dgels shape): requires m ≥ n and full column rank, returns the length-n solution x = R⁻¹·(Qᵀb)[0..n). This is the kernel square-root/array filters build on; it composes with cholesky rank-one updates for sliding-window problems.

Link copied to clipboard

Least-squares solve into out, which has length n and is returned. The Qᵀb product needs a length-m intermediate, so pass a workspace to make the call allocation-free.

Link copied to clipboard
open fun solveMinimumNorm(qr: QrDecomposition, b: DoubleArray, workspace: Workspace? = null): DoubleArray

Minimum-norm solution of the underdetermined consistent system A · x = b for a wide m×n A with m <= n and full row rank (LAPACK dgels's underdetermined shape, via QR of the transpose instead of LQ): pass the factorization qr(Aᵀ). With Aᵀ = Q·R we have A = Rᵀ·Qᵀ, so a forward solve Rᵀ·w = b followed by x = Q·(w padded with zeros) gives the solution of smallest 2-norm. b has length m; the result has length n. Rank deficiency is not detected and surfaces as infinities/NaNs, following the triangular-solve convention.

Link copied to clipboard

Minimum-norm solve into out, which has length m (the wide system's column count) and is returned. A length-n intermediate holds the forward solve, so pass a workspace to make the call allocation-free.

Link copied to clipboard
open fun trtri(a: DenseMatrix, lower: Boolean, unitDiag: Boolean = false): DenseMatrix

Invert a triangular matrix in place of a fresh result (LAPACK dtrtri): returns T⁻¹ for the lower or upper triangle of the square a, taking the diagonal as 1 when unitDiag.