From 5e4d9f5a73302bf1bbf03608fdde255d03c2dd4e Mon Sep 17 00:00:00 2001 From: vic Date: Thu, 10 Sep 2026 14:44:22 +0200 Subject: [PATCH] Handle zero-update numerical stagnation in the SVM solver --- cpp/src/svm/kernelcache.cuh | 13 +++++++++++++ cpp/src/svm/smosolver.cuh | 17 +++++++++++++++-- cpp/tests/sg/svc_test.cu | 30 +++++++++++++++++++++++++++++- python/cuml/tests/test_svm.py | 8 ++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/cpp/src/svm/kernelcache.cuh b/cpp/src/svm/kernelcache.cuh index 9883ca0f01..bcfc016fa0 100644 --- a/cpp/src/svm/kernelcache.cuh +++ b/cpp/src/svm/kernelcache.cuh @@ -512,6 +512,19 @@ class KernelCache { cache_state = CacheState::WS_INITIALIZED; } + /** + * @brief Finish processing a working set without updating the cache + * + * A block solve can legitimately produce no coefficient updates. In that + * case there is no full kernel tile to process, but the working-set state + * still needs to be closed before another working set can be initialized. + */ + void FinishWorkingSet() + { + ASSERT(cache_state == CacheState::WS_INITIALIZED, "Working set not initialized!"); + cache_state = CacheState::READY; + } + /** * @brief Retrieve kernel indices * diff --git a/cpp/src/svm/smosolver.cuh b/cpp/src/svm/smosolver.cuh index 71a976cd81..9cd732d275 100644 --- a/cpp/src/svm/smosolver.cuh +++ b/cpp/src/svm/smosolver.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -177,7 +177,8 @@ void SmoSolver::Solve(MatrixViewType matrix, stream); RAFT_CUDA_TRY(cudaPeekAtLastError()); // The following should be performed only for elements with nonzero delta_alpha - if (nnz_da > 0) { + bool made_progress = nnz_da > 0; + if (made_progress) { auto batch_descriptor = cache.InitFullTileBatching(nz_da_idx.data(), nnz_da); while (cache.getNextBatchKernel(batch_descriptor)) { @@ -191,6 +192,8 @@ void SmoSolver::Solve(MatrixViewType matrix, batch_descriptor.kernel_data); RAFT_CUDA_TRY(cudaPeekAtLastError()); } + } else { + cache.FinishWorkingSet(); } handle.sync_stream(stream); raft::common::nvtx::pop_range(); @@ -203,6 +206,16 @@ void SmoSolver::Solve(MatrixViewType matrix, if ((max_iter != -1 && n_iter >= max_iter) || n_outer_iter >= max_outer_iter) { keep_going = false; } + if (keep_going && !made_progress) { + const char* advice = std::is_same::value + ? " Try using float64 input or reducing the magnitude of the kernel " + "values." + : " Try rescaling the input data or adjusting the kernel parameters."; + THROW( + "SMO error: solver made no progress while the stopping criterion was not satisfied. " + "This can happen when kernel values are too large for the input precision.%s", + advice); + } if (n_outer_iter % 500 == 0) { CUML_LOG_DEBUG("SMO iteration %d, diff %lf", n_outer_iter, (double)diff); diff --git a/cpp/tests/sg/svc_test.cu b/cpp/tests/sg/svc_test.cu index 25a8402b49..e98f374d65 100644 --- a/cpp/tests/sg/svc_test.cu +++ b/cpp/tests/sg/svc_test.cu @@ -309,6 +309,33 @@ TYPED_TEST_P(KernelCacheTest, EvalTest) } } +TYPED_TEST_P(KernelCacheTest, FinishWorkingSetTest) +{ + KernelParams params{KernelType::LINEAR, 3, 1, 0}; + auto dense_view = + raft::make_device_strided_matrix_view( + this->x_dev.data(), this->n_rows, this->n_cols, 0); + GramMatrixBase* kernel = KernelFactory::create(ML::matrix::to_cuvs(params)); + KernelCache> cache( + this->handle, + dense_view, + this->n_rows, + this->n_cols, + this->n_ws, + kernel, + static_cast(params.kernel), + 0, + C_SVC); + + cache.InitWorkingSet(this->ws_idx_dev.data()); + cache.FinishWorkingSet(); + + // A new working set can be initialized after a solve with no coefficient updates. + cache.InitWorkingSet(this->ws_idx_dev.data()); + cache.FinishWorkingSet(); + delete kernel; +} + TYPED_TEST_P(KernelCacheTest, SvcCacheEvalTest) { KernelParams param{KernelType::LINEAR, 3, 1, 0}; @@ -490,7 +517,8 @@ TYPED_TEST_P(KernelCacheTest, SvrCacheEvalTest) } } -REGISTER_TYPED_TEST_CASE_P(KernelCacheTest, EvalTest, SvcCacheEvalTest, SvrCacheEvalTest); +REGISTER_TYPED_TEST_CASE_P( + KernelCacheTest, EvalTest, FinishWorkingSetTest, SvcCacheEvalTest, SvrCacheEvalTest); INSTANTIATE_TYPED_TEST_CASE_P(My, KernelCacheTest, FloatTypes); template diff --git a/python/cuml/tests/test_svm.py b/python/cuml/tests/test_svm.py index e5b381c9da..83f26f0036 100644 --- a/python/cuml/tests/test_svm.py +++ b/python/cuml/tests/test_svm.py @@ -467,6 +467,14 @@ def test_svr_skl_cmp_weighted(): compare_svr(cuSVR, sklSVR, X, y) +def test_svr_float32_numerical_stagnation_error(): + X = np.arange(5, dtype=np.float32).reshape(-1, 1) + y = np.arange(5, dtype=np.float32) + + with pytest.raises(RuntimeError, match="made no progress.*float64"): + cu_svm.SVR(kernel="poly", degree=10).fit(X, y) + + @pytest.mark.parametrize("classifier", [True, False]) @pytest.mark.parametrize("train_dtype", [np.float32, np.float64]) @pytest.mark.parametrize("test_dtype", [np.float64, np.float32])