A GPU backend that runs real kernels across nine crates is a big deal. Just as big: a release that went looking for its own fake results and fixed every one it found.
Today we released sklears 0.2.0 — the release that replaces sklears’s GPU code, previously a set of stubs and always-CPU fallbacks wearing a GPU-shaped API, with a real CUDA foundation built on oxicuda. It’s consumed by nine downstream crates, not a single showcase demo, and it shipped alongside a correctness sweep that hunted down and fixed several results across the workspace that had been silently fabricated.
sklears is the pure-Rust alternative to scikit-learn: No C. No Fortran. No Cython. No GIL. scikit-learn’s own pipeline depends on NumPy, SciPy, and a zoo of compiled extensions — and GPU acceleration usually means reaching for a separate stack (cuML/RAPIDS) with its own CUDA Toolkit and conda-environment requirements. sklears is plain Rust on the SciRS2 stack, with OxiBLAS for BLAS/LAPACK, Oxicode for serialization, and now oxicuda for GPU — which loads libcuda.so/nvcuda.dll dynamically at runtime rather than requiring the CUDA SDK or a C/C++ compiler to build. Compiles to a single static binary (or WASM) and runs everywhere, GPU or no GPU.
Why sklears 0.2.0 is a game changer
GPU acceleration for classical ML in the Python world means:
- scikit-learn itself ships no GPU backend at all — you reach for a second library (cuML/RAPIDS) with a completely different API surface.
- That second library brings its own CUDA Toolkit, driver-version matrix, and usually a conda environment, none of which travels well into a container or a static binary.
- “GPU support” in wrapper libraries often means a silent CPU fallback (or a hard crash) with no clean way to ask “is a device actually bound right now?”
- A GPU path for something like LDA’s generalized eigenproblem or approximate nearest-neighbor search means stitching together yet another package with its own install story.
sklears 0.2.0 ends all of that:
- A real shared GPU foundation.
sklears_core::gpu’sGpuBackend,GpuArray<T: Copy>, andGpuMatrixOpsdispatch genuine on-device kernels via oxicuda-blas/-solver/-manifold/-dnn — not a CPU computation dressed up in a GPU-shaped type. - Nine downstream crates, one API.
sklears-clustering,-cross-decomposition,-decomposition,-discriminant-analysis,-linear,-manifold,-neighbors,-neural, and-svmall consume the same foundation behind their own opt-ingpufeature. - Detection is honest.
GpuBackend::detect()returnsResult<Option<Self>>and genuinely returnsOk(None)with no CUDA driver present, instead of quietly substituting a CPU stub. Every consuming crate’s GPU field is nowOption<GpuContext>/Option<GpuBackend>, so construction succeeds transparently on GPU-less machines. - Real solvers, not placeholders. A Cholesky-reduction generalized eigensolver for LDA (
GpuLDAKernel::solve_generalized_eigen_gpu, equivalent to LAPACK’ssygvd(itype=1)), on-device LU/QR/SVD/eigendecomposition viaoxicuda-solver, and HNSW-backed approximate k-NN/t-SNE viaoxicuda-manifold. - The correctness sweep is the other half of this release. 0.2.0’s Fixed section reads like an honesty audit: a decomposition SVD routine that returned a hardcoded
(eye, ones, eye)for every input, a GPU t-SNE that ignored its input and returned a random embedding as though it had converged, aGraphicalLassosolver with an exact even/oddmax_iterparity bug that silently degenerated every fit to the identity matrix at the crate’s own default of 100 iterations, and aModelFusion::fit()that looped over base models and pushed each one back unfitted. All of these — and more — are now fixed with real implementations and regression tests. - 12,721 tests passing across 36 crates — up from 12,242 at 0.1.2 — with >99% scikit-learn API coverage held.
Technical Deep Dive: what’s under the hood
-
The foundation layer (
sklears-core::gpu).GpuBackendpairs anoxicuda_driver::Contextwith anoxicuda_blas::BlasHandle.GpuArray<T: Copy>is backed by a realoxicuda_memory::DeviceBuffer<T>— there’s no CPU-fallback path hiding inside the type itself, because aGpuArraycan only be constructed from an already-detectedGpuBackend.GpuMatrixOps(matmul/add/mul/scale/transpose) dispatches straight tooxicuda-blaskernels. -
Per-algorithm dispatch, honestly optional. Each of the nine consuming crates gates its GPU path behind its own
gpuCargo feature (e.g.sklears-svm’sgpu = ["dep:oxicuda-blas", "dep:oxicuda-memory", "sklears-core/gpu_support"]) and stores its context asOption<GpuBackend>.sklears-svm’s RBF/Sigmoid kernel post-processing now runs entirely on-device instead of downloading the inner-product matrix first;sklears-neural’stensor_core_gemm_f16/mixed_precision_gemmperform real FP16 tensor-core GEMM viaoxicuda_blas::level3::gemm::<half::f16>. -
Heavier solves via
oxicuda-solver/oxicuda-manifold.sklears-decomposition’sgpu_svd/gpu_eigendecompositionnow calloxicuda_solver::dense::{svd, syevd}with CPU (scirs2_linalg) fallback on failure;sklears-linear’s GPU LU-solve and Householder QR go throughoxicuda_solver::dense::{lu_factorize, lu_solve, qr_factorize, qr_generate_q};sklears-manifold’sGpuTSNE::fit_transformruns the realoxicuda_manifold::tsne_fitoptimizer and itsGpuAccelerator::knn_searchgains an HNSW-backed approximate path, andsklears-neighbors’sGpuKNeighborsSearch::with_anngets the same HNSW-backed route with automatic brute-force fallback for metrics HNSW doesn’t cover. -
A parallel correctness sweep, including real memory-safety fixes. Several 0.2.0 fixes are Miri-detected soundness bugs, not GPU-related:
AlignedAllocator::aligned_vecandMemoryLayoutManager::allocate_alignedboth wrapped custom-aligned allocations in aVec/BoxwhoseDropfrees using the natural (smaller) alignment — an alloc/dealloc mismatch — now fixed with dedicated aligned-buffer types that free themselves with their realLayout.
Getting Started
cargo add sklears
The base CPU API is unchanged — plain training still looks like this:
use sklears::prelude::*;
use sklears::linear_model::LinearRegression;
use sklears::model_selection::train_test_split;
fn main() -> Result<()> {
let dataset = sklears::dataset::make_regression(100, 10, 0.1)?;
let (x_train, x_test, y_train, y_test) =
train_test_split(&dataset.data, &dataset.target, 0.2, Some(42))?;
let model = LinearRegression::new()
.fit_intercept(true)
.fit(&x_train, &y_train)?;
let r2_score = model.score(&x_test, &y_test)?;
println!("R² score: {:.4}", r2_score);
Ok(())
}
0.2.0’s new part is the honest GPU foundation, reachable directly from sklears-core once a crate’s gpu feature is enabled:
use sklears_core::gpu::{GpuArray, GpuBackend, GpuMatrixOps};
// detect() genuinely returns Ok(None) with no CUDA driver present --
// it never substitutes a CPU computation dressed up as a GPU result.
if let Some(backend) = GpuBackend::detect()? {
let a = GpuArray::from_array2(&backend, &x)?;
let b = GpuArray::from_array2(&backend, &weights)?;
let product = a.matmul(&b)?; // real on-device GEMM via oxicuda-blas
let result = product.to_array2()?; // download back to host
println!("GPU result shape: {:?}", result.shape());
} else {
println!("No CUDA device -- falling back to CPU/Rayon automatically");
}
What’s New in 0.2.0
- GPU foundation:
sklears_core::gpurebuilt on real oxicuda kernels (GpuBackend,GpuArray<T: Copy>,GpuMatrixOps), consumed by 9 downstream crates. - LDA on GPU:
sklears-discriminant-analysisgains a real Cholesky-reduction generalized eigensolver behind a new opt-ingpufeature, plus 16 new tests (the module had none before, since it never compiled). - Solvers: real on-device SVD/eigendecomposition (
sklears-decomposition), LU-solve/QR (sklears-linear), t-SNE and HNSW approximate k-NN (sklears-manifold,sklears-neighbors), FP16 tensor-core GEMM (sklears-neural). sklears-compose: newstackingmodule with a real out-of-foldStackingEnsemble;ModelFusion/HierarchicalCompositionnow genuinely train their base models instead of faking it.sklears-gaussian-process: four previously stub modules fully implemented —ConvolutionProcess,FitcGaussianProcessRegressor,KernelSelector,VariationalGaussianProcessClassifier.sklears-datasets: 6 new generator modules, 23 new public dataset generators, 100 new tests.- Re-enabled after being fully excluded from the build:
sklears-core’strait_explorer::graph_visualization(5 newGraphAnalyzeralgorithms) andtrait_explorer::security_analysis(compliance/security-metrics for GDPR, HIPAA, CCPA, SOX, and more). sklearsfacade: thepreprocessingfeature restored, along with 8 algorithm-showcase examples and a benchmark target that depended on it.- Breaking changes (see Tips below):
GpuContext::new()removed workspace-wide in favor of fallibledetect()/with_device_id(); several GPU-related renames acrosssklears-cross-decompositionandsklears-discriminant-analysis. - Removed: ~2,225 lines of unconditionally-compiled GPU stub code from
sklears-simd(gpu.rs,gpu_memory.rs,multi_gpu.rs) — GPU dispatch now belongs exclusively tosklears-core::gpu.
Tips
- GPU is opt-in per crate, not workspace-wide. Enable it where you need it:
sklears-svm = { version = "0.2.0", features = ["gpu"] }(or-linear,-neural,-manifold,-neighbors,-decomposition,-discriminant-analysis,-clustering,-cross-decomposition). Each pulls insklears-core/gpu_supportplus its ownoxicuda-*deps. - Always match on
Option, never assume a GPU is present.GpuBackend::detect()/with_device_id()returnResult<Option<Self>>—Ok(None)is the normal, expected answer on a GPU-less CI runner. Every 0.2.0-migrated crate stores its context asOption<GpuContext>/Option<GpuBackend>for exactly this reason. - Migrating
gpu_supportcode?GpuContext::new()is gone. Construction is now exclusively fallible viadetect()/with_device_id(). If you were calling the old infallible constructor directly, switch to matching on theOptionit now returns. - Renamed APIs to grep for:
sklears-discriminant-analysis’scurrent_backend()is nowbackend() -> Option<&GpuBackend>;sklears-cross-decomposition’s localGpuBackendenum is nowGpuBackendKind(to stop shadowingsklears_core::gpu::GpuBackend), and itsgpu_context() -> &dyn GpuContextis nowgpu_backend() -> Option<&GpuBackend>. - If you fitted a
GraphicalLassomodel before 0.2.0, refit it. The coordinate-descent solver’s even/oddmax_iterparity bug meant every fit at the defaultmax_iter=100silently returned the identity matrix regardless ofalphaor your data. It’s a real block coordinate-descent implementation now — old fitted results should not be trusted. - Use the HNSW path once exact k-NN stops scaling.
GpuKNeighborsSearch::with_annandsklears-manifold’sGpuAccelerator::knn_searchnow take an HNSW-backed approximate route for Euclidean/Cosine metrics, with automatic brute-force fallback for metrics HNSW doesn’t support.
This is the foundation
As of July 14, 2026, sklears sits on:
- SciRS2 0.6.0 — the numerical backbone (linear algebra, stats, optimization, signal, FFT, sparse)
- OxiBLAS — BLAS/LAPACK, pure Rust
- Oxicode — serialization, pure Rust (replacing bincode)
- OxiCUDA 0.5.0 — the new real GPU layer, replacing the old wgpu/cudarc-era stubs
- OxiARC — compression/archiving, pure Rust
- NumRS2 / PandRS — array and dataframe substrate
Beyond the core, sklears feeds into TenfloweRS and TrustformeRS for deep learning, and alongside Celers for streaming data — one pure-Rust stack from raw data through trained models, GPU or no GPU, no Python runtime required.
Repository: https://github.com/cool-japan/sklears
Star the repo if a classical-ML library that tells you the truth about whether your GPU is actually being used is something you want to build on — every star helps the ecosystem grow.
The era of “GPU support” meaning a second Python package and a CUDA Toolkit install is over. Pure Rust machine learning is here — fast, safe, and sovereign.
— KitaSan at COOLJAPAN OÜ July 14, 2026