From 2e5d74111d743568fa936c87ccd27172d92b74f9 Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 16 Oct 2026 20:34:23 +0000 Subject: [PATCH 01/10] test(rocsparse): add ELL triangular solve and SELL SpMV unit tests These paths are only covered by YAML, which misses the slice-size boundaries and the partial last slice. --- .../clients/unittests/CMakeLists.txt | 6 + .../unit_test_internal_level2_ell_sell.cpp | 963 ++++++++++++++++++ 2 files changed, 969 insertions(+) create mode 100644 projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp diff --git a/projects/rocsparse/clients/unittests/CMakeLists.txt b/projects/rocsparse/clients/unittests/CMakeLists.txt index 79b71c3fab3b..a89cdddfd3d0 100644 --- a/projects/rocsparse/clients/unittests/CMakeLists.txt +++ b/projects/rocsparse/clients/unittests/CMakeLists.txt @@ -287,6 +287,12 @@ set(ROCSPARSE_UNIT_TEST_DEVICE_SOURCES # with sibling unit-test branches that append after rocsparse_unit_test_main. # Drives public entry points, linking roc::rocsparse; no compile-in TUs. unit_test_host_level2.cpp + # --- ELL triangular solve / sliced-ELL SpMV [ROCM-33114] ------------------ + # Distinct anchor (right after the level-2 host tests) so this leaf does not + # conflict with sibling unit-test branches. Drives rocsparse_sptrsv on + # rocsparse_format_ell and rocsparse_v2_spmv on rocsparse_format_sell + # through the public generic descriptor API; no compile-in TUs. + unit_test_internal_level2_ell_sell.cpp # --- csr2ell strided-batched grid.y clamp regression [AISPARSE-683] ------- # Distinct anchor at the end of the list so this leaf does not conflict with # sibling branches appending elsewhere. Needs the defining library TU in diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp new file mode 100644 index 000000000000..10d509d54b02 --- /dev/null +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -0,0 +1,963 @@ +/*! \file */ +/* ************************************************************************ + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ************************************************************************ */ + +// +// Device unit tests for two level-2 ELL / sliced-ELL code paths that are +// otherwise only reached by YAML integration tests: +// +// - ELL triangular solve (ellsv), reached through the generic +// rocsparse_sptrsv descriptor API for rocsparse_format_ell +// (library/src/level2/rocsparse_sptrsv.cpp). clients/tests/test_sptrsv_ell.yaml +// only drives it with transA none; this file adds a tiny device-level +// check of the numeric solution, plus a unit-diagonal case and an +// explicit-zero-pivot case. Gated the same way the library gates ELL +// TRSV support (BUILD_WITH_ELL_TRSV / ROCSPARSE_WITH_ELL_TRSV): when that +// symbol is not defined the test is skipped with a clear message instead +// of being compiled out, so it still shows up (as skipped) in a +// BUILD_WITH_ELL_TRSV=OFF build. +// +// - Sliced ELL SpMV (sellmv), reached through rocsparse_v2_spmv for +// rocsparse_format_sell (library/src/level2/rocsparse_sellmv_impl.cpp, +// rocsparse_sellmv.cpp, sellmv_device.h). The dispatch in +// rocsparse_sellmv_impl.cpp picks the small-slice kernel +// (sellmvn_device / sellmvt_device) for sell_slice_size <= 128, the +// large-slice kernel (sellmvn_large_slice_device / +// sellmvt_large_slice_device) above that, and falls back to csrmv when +// sell_slice_size == 1. This file exercises all three, in both +// directions, including a transpose case whose matrix size is not a +// multiple of the slice size (a partial last slice). +// +// NOTE: on current develop, sellmvt_device reads x[row] before checking +// row < m (the bounds check only guards the start/end offsets used by the +// accumulation loop, which is why padding threads never touch +// sell_col_ind / sell_val). That stray read of x is harmless here -- the +// loaded value is never used because the padding thread's own start/end +// range is empty -- but it means the transpose/partial-last-slice test +// below must size x so that the read stays in-bounds for every slice +// (see build_bidiagonal_sell). This fix is already implemented in open PR +// https://github.com/ROCm/rocm-libraries/pull/13339 (not merged at the +// time of writing); this test does not attempt to change +// sellmv_device.h, it only asserts the numeric result, which is correct +// either way. +// +#include "unit_test_utils.hpp" + +#include "rocsparse.h" + +#include + +using namespace rocsparse_ut; + +namespace +{ + // Loose-enough tolerance for the float computations below. Most of the + // matrices are built from small powers of two so the host reference and + // the device result usually match exactly; this only guards against + // summation-order differences. + constexpr double kTol = 1e-4; + + void expect_vectors_near(const std::vector& expected, + const std::vector& actual, + double tol = kTol) + { + ASSERT_EQ(expected.size(), actual.size()); + for(size_t i = 0; i < expected.size(); ++i) + { + EXPECT_NEAR(static_cast(actual[i]), static_cast(expected[i]), tol) + << "index " << i; + } + } + + int64_t count_nonneg(const std::vector& v) + { + int64_t c = 0; + for(int32_t e : v) + { + if(e >= 0) + { + ++c; + } + } + return c; + } + +// =========================================================================== +// ellsv : ELL triangular solve via the generic rocsparse_sptrsv descriptor. +// Only built / exercised when the library is built with ELL TRSV support. +// =========================================================================== +#ifdef ROCSPARSE_WITH_ELL_TRSV + + // Host reference for a tiny lower-triangular ELL solve, matching the + // semantics of rocsparse::ellsv_device (library/src/level2/ellsv_device.h): + // - a unit diagonal entry is ignored for the division even if one is + // stored in the ELL data, + // - an explicit zero on a non-unit diagonal is treated as 1 after + // recording the pivot (so the numeric result still comes out + // deterministic, matching what the kernel computes), + // - only the strictly-lower entries (plus the diagonal) contribute. + // ell_col_ind / ell_val are laid out exactly like rocsparse_create_ell_descr + // expects: column-major, ELL_IND(row, slot, m, width) = slot * m + row. + std::vector host_ellsv_lower(int64_t m, + int64_t n, + int64_t ell_width, + const std::vector& ell_col_ind, + const std::vector& ell_val, + const std::vector& x, + float alpha, + rocsparse_diag_type diag_type, + int64_t* zero_pivot) + { + std::vector y(static_cast(m), 0.0f); + *zero_pivot = -1; + + for(int64_t row = 0; row < m; ++row) + { + float sum = alpha * x[static_cast(row)]; + float diag_val = 1.0f; + + for(int64_t p = 0; p < ell_width; ++p) + { + const int64_t idx = p * m + row; + const int32_t col = ell_col_ind[static_cast(idx)]; + if(col < 0 || col >= n) + { + continue; + } + float val = ell_val[static_cast(idx)]; + + if(col == row) + { + if(diag_type == rocsparse_diag_type_non_unit) + { + if(val == 0.0f) + { + if(*zero_pivot == -1 || row < *zero_pivot) + { + *zero_pivot = row; + } + val = 1.0f; + } + diag_val = 1.0f / val; + } + continue; + } + + // Lower triangular solve: only strictly-lower entries feed + // back already-solved y values. + if(col > row) + { + continue; + } + sum -= val * y[static_cast(col)]; + } + + y[static_cast(row)] + = (diag_type == rocsparse_diag_type_non_unit) ? sum * diag_val : sum; + } + return y; + } + + // Runs buffer_size / analysis / compute on sptrsv_descr for a lower + // triangular ELL matrix A, operation none, alpha on the host. Returns the + // zero-pivot position reported after analysis and after compute (-1 if + // none was reported at that stage). + void run_ellsv(rocsparse_handle handle, + rocsparse_spmat_descr A, + rocsparse_dnvec_descr x, + rocsparse_dnvec_descr y, + float alpha, + int64_t* analysis_pivot, + int64_t* solve_pivot) + { + rocsparse_sptrsv_descr descr = nullptr; + ASSERT_EQ(rocsparse_create_sptrsv_descr(&descr), rocsparse_status_success); + + const rocsparse_operation trans = rocsparse_operation_none; + const rocsparse_sptrsv_alg alg = rocsparse_sptrsv_alg_default; + const rocsparse_datatype dt = rocsparse_datatype_f32_r; + + ASSERT_EQ(rocsparse_sptrsv_set_input( + handle, descr, rocsparse_sptrsv_input_operation, &trans, sizeof(trans), nullptr), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_sptrsv_set_input(handle, descr, rocsparse_sptrsv_input_alg, &alg, sizeof(alg), nullptr), + rocsparse_status_success); + ASSERT_EQ(rocsparse_sptrsv_set_input(handle, + descr, + rocsparse_sptrsv_input_scalar_datatype, + &dt, + sizeof(dt), + nullptr), + rocsparse_status_success); + ASSERT_EQ(rocsparse_sptrsv_set_input(handle, + descr, + rocsparse_sptrsv_input_compute_datatype, + &dt, + sizeof(dt), + nullptr), + rocsparse_status_success); + // Mandatory: the descriptor's analysis policy defaults to an invalid + // sentinel, so rocsparse::ellsv_analysis's ROCSPARSE_CHECKARG_ENUM on it + // fails with rocsparse_status_invalid_value unless this is set. + const rocsparse_analysis_policy apol = rocsparse_analysis_policy_force; + ASSERT_EQ(rocsparse_sptrsv_set_input(handle, + descr, + rocsparse_sptrsv_input_analysis_policy, + &apol, + sizeof(apol), + nullptr), + rocsparse_status_success); + + size_t bs_analysis = 0; + ASSERT_EQ(rocsparse_sptrsv_buffer_size( + handle, descr, A, x, y, rocsparse_sptrsv_stage_analysis, &bs_analysis, nullptr), + rocsparse_status_success); + device_vector buf_a{bs_analysis ? bs_analysis : size_t(1)}; + ASSERT_TRUE(buf_a.ptr); + ASSERT_EQ(rocsparse_sptrsv(handle, + descr, + A, + x, + y, + rocsparse_sptrsv_stage_analysis, + bs_analysis, + bs_analysis ? buf_a.ptr : nullptr, + nullptr), + rocsparse_status_success); + + int64_t pivot = -1; + rocsparse_status pivot_status = rocsparse_sptrsv_get_output( + handle, descr, rocsparse_sptrsv_output_zero_pivot_position, &pivot, sizeof(pivot), nullptr); + ASSERT_TRUE(pivot_status == rocsparse_status_success + || pivot_status == rocsparse_status_zero_pivot); + *analysis_pivot = pivot; + + const void* alpha_ptr = α + ASSERT_EQ(rocsparse_sptrsv_set_input(handle, + descr, + rocsparse_sptrsv_input_scalar_alpha, + alpha_ptr, + sizeof(alpha_ptr), + nullptr), + rocsparse_status_success); + + size_t bs_compute = 0; + ASSERT_EQ(rocsparse_sptrsv_buffer_size( + handle, descr, A, x, y, rocsparse_sptrsv_stage_compute, &bs_compute, nullptr), + rocsparse_status_success); + device_vector buf_c{bs_compute ? bs_compute : size_t(1)}; + ASSERT_TRUE(buf_c.ptr); + ASSERT_EQ(rocsparse_sptrsv(handle, + descr, + A, + x, + y, + rocsparse_sptrsv_stage_compute, + bs_compute, + bs_compute ? buf_c.ptr : nullptr, + nullptr), + rocsparse_status_success); + ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); + + pivot = -1; + pivot_status = rocsparse_sptrsv_get_output( + handle, descr, rocsparse_sptrsv_output_zero_pivot_position, &pivot, sizeof(pivot), nullptr); + ASSERT_TRUE(pivot_status == rocsparse_status_success + || pivot_status == rocsparse_status_zero_pivot); + *solve_pivot = pivot; + + EXPECT_EQ(rocsparse_destroy_sptrsv_descr(descr), rocsparse_status_success); + } + +#endif // ROCSPARSE_WITH_ELL_TRSV + + // =========================================================================== + // sellmv : Sliced ELL SpMV helpers, used regardless of ROCSPARSE_WITH_ELL_TRSV. + // =========================================================================== + + // Host reference for a sliced-ELL SpMV: y := alpha * op(A) * x + beta * y. + // Mirrors the mathematical semantics of sellmv_device.h without any of the + // thread/slice tricks, so it independently checks the kernels' output. + // sell_col_ind / sell_val layout: for slice s spanning + // [slice_offsets[s], slice_offsets[s+1]) with depth = (end - start) / + // sell_slice_size, slot p, local row r (0 <= r < sell_slice_size), the + // entry lives at slice_offsets[s] + p * sell_slice_size + r. + std::vector host_sellmv(int64_t m, + int64_t n, + int64_t sell_slice_size, + const std::vector& slice_offsets, + const std::vector& col_ind, + const std::vector& val, + const std::vector& x, + float alpha, + float beta, + const std::vector& y_init, + rocsparse_operation trans) + { + const int64_t out_size = (trans == rocsparse_operation_none) ? m : n; + std::vector result(static_cast(out_size)); + for(int64_t i = 0; i < out_size; ++i) + { + result[static_cast(i)] = beta * y_init[static_cast(i)]; + } + + const int64_t nslices = (m + sell_slice_size - 1) / sell_slice_size; + for(int64_t s = 0; s < nslices; ++s) + { + const int64_t start = slice_offsets[static_cast(s)]; + const int64_t end = slice_offsets[static_cast(s + 1)]; + const int64_t depth = (end - start) / sell_slice_size; + + for(int64_t r = 0; r < sell_slice_size; ++r) + { + const int64_t row = s * sell_slice_size + r; + if(row >= m) + { + continue; + } + for(int64_t p = 0; p < depth; ++p) + { + const int64_t idx = start + p * sell_slice_size + r; + const int32_t col = col_ind[static_cast(idx)]; + if(col < 0) + { + continue; + } + const float v = val[static_cast(idx)]; + if(trans == rocsparse_operation_none) + { + result[static_cast(row)] += alpha * v * x[static_cast(col)]; + } + else + { + result[static_cast(col)] += alpha * v * x[static_cast(row)]; + } + } + } + } + return result; + } + + // x[i] = i + 1. A constant x would make the non-transposed result the + // plain sum of a row's values, independent of which column each value + // claims, so a scrambled col_ind would go unnoticed. All values are small + // integers, so the float arithmetic stays exact. + std::vector make_varying_x(int64_t size) + { + std::vector x(static_cast(size)); + for(int64_t i = 0; i < size; ++i) + { + x[static_cast(i)] = static_cast(i + 1); + } + return x; + } + + // Builds a tiny general (non-triangular) sliced-ELL matrix with a main + // diagonal (value row+1) and a sub-diagonal (value row, for row >= 1), + // depth 2, padded with -1 / 0 where a slot has no entry (row 0's missing + // sub-diagonal, and every slot in a slice's padding rows). Suitable for + // any sell_slice_size >= 2; for sell_slice_size == 1 use + // build_bidiagonal_sell_csr_exact instead (slice_size == 1 is dispatched + // straight to the CSR fallback, which does not skip padded entries). + void build_bidiagonal_sell(int64_t m, + int64_t sell_slice_size, + std::vector& slice_offsets, + std::vector& col_ind, + std::vector& val) + { + constexpr int64_t depth = 2; + const int64_t nslices = (m + sell_slice_size - 1) / sell_slice_size; + + slice_offsets.assign(static_cast(nslices + 1), 0); + for(int64_t s = 0; s <= nslices; ++s) + { + slice_offsets[static_cast(s)] = static_cast(s * sell_slice_size * depth); + } + + const int64_t colval_size = nslices * sell_slice_size * depth; + col_ind.assign(static_cast(colval_size), -1); + val.assign(static_cast(colval_size), 0.0f); + + for(int64_t s = 0; s < nslices; ++s) + { + const int64_t start = slice_offsets[static_cast(s)]; + for(int64_t r = 0; r < sell_slice_size; ++r) + { + const int64_t row = s * sell_slice_size + r; + if(row >= m) + { + continue; + } + // slot 0: main diagonal. + const int64_t idx0 = start + 0 * sell_slice_size + r; + col_ind[static_cast(idx0)] = static_cast(row); + val[static_cast(idx0)] = static_cast(row + 1); + + // slot 1: sub-diagonal (none for row 0). + if(row >= 1) + { + const int64_t idx1 = start + 1 * sell_slice_size + r; + col_ind[static_cast(idx1)] = static_cast(row - 1); + val[static_cast(idx1)] = static_cast(row); + } + } + } + } + + // Same bidiagonal pattern, but as an exact CSR structure (slice_offsets + // behaves as a CSR row pointer, no padding) for the sell_slice_size == 1 + // dispatch, which routes straight through rocsparse::csrmv_template. + void build_bidiagonal_sell_csr_exact(int64_t m, + std::vector& slice_offsets, + std::vector& col_ind, + std::vector& val) + { + slice_offsets.assign(static_cast(m + 1), 0); + col_ind.clear(); + val.clear(); + + int32_t offset = 0; + for(int64_t row = 0; row < m; ++row) + { + slice_offsets[static_cast(row)] = offset; + if(row >= 1) + { + col_ind.push_back(static_cast(row - 1)); + val.push_back(static_cast(row)); + ++offset; + } + col_ind.push_back(static_cast(row)); + val.push_back(static_cast(row + 1)); + ++offset; + } + slice_offsets[static_cast(m)] = offset; + } + + // Creates the SELL spmat descriptor from host data, runs the two-stage + // (analysis, compute) rocsparse_v2_spmv pipeline with alg = sell, and + // checks the result against `expected`. + void run_v2_spmv_sell(rocsparse_handle handle, + rocsparse_operation trans, + int64_t m, + int64_t n, + const std::vector& slice_offsets, + const std::vector& col_ind, + const std::vector& val, + int64_t sell_slice_size, + const std::vector& hx, + float alpha, + float beta, + const std::vector& hy_init, + const std::vector& expected) + { + device_vector d_offs{slice_offsets}; + device_vector d_cols{col_ind}; + device_vector d_vals{val}; + ASSERT_TRUE(d_offs.ptr && d_cols.ptr && d_vals.ptr); + + const int64_t nnz = count_nonneg(col_ind); + ASSERT_GT(nnz, 0); + + rocsparse_spmat_descr A = nullptr; + ASSERT_EQ(rocsparse_create_sell_descr(&A, + m, + n, + nnz, + sell_slice_size, + static_cast(col_ind.size()), + d_offs.ptr, + d_cols.ptr, + d_vals.ptr, + rocsparse_indextype_i32, + rocsparse_indextype_i32, + rocsparse_index_base_zero, + rocsparse_datatype_f32_r), + rocsparse_status_success); + + const int64_t x_size = (trans == rocsparse_operation_none) ? n : m; + const int64_t y_size = (trans == rocsparse_operation_none) ? m : n; + ASSERT_EQ(static_cast(hx.size()), x_size); + ASSERT_EQ(static_cast(hy_init.size()), y_size); + ASSERT_EQ(static_cast(expected.size()), y_size); + + device_vector x{hx}; + device_vector y{hy_init}; + ASSERT_TRUE(x.ptr && y.ptr); + + rocsparse_dnvec_descr dx = nullptr, dy = nullptr; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, x_size, x.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, y_size, y.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); + + rocsparse_spmv_descr spmv_descr = nullptr; + ASSERT_EQ(rocsparse_create_spmv_descr(&spmv_descr), rocsparse_status_success); + + const rocsparse_spmv_alg alg = rocsparse_spmv_alg_sell; + const rocsparse_datatype dt = rocsparse_datatype_f32_r; + ASSERT_EQ(rocsparse_spmv_set_input( + handle, spmv_descr, rocsparse_spmv_input_alg, &alg, sizeof(alg), nullptr), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmv_set_input( + handle, spmv_descr, rocsparse_spmv_input_operation, &trans, sizeof(trans), nullptr), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmv_set_input(handle, + spmv_descr, + rocsparse_spmv_input_scalar_datatype, + &dt, + sizeof(dt), + nullptr), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmv_set_input(handle, + spmv_descr, + rocsparse_spmv_input_compute_datatype, + &dt, + sizeof(dt), + nullptr), + rocsparse_status_success); + + size_t bs_analysis = 0; + ASSERT_EQ(rocsparse_v2_spmv_buffer_size(handle, + spmv_descr, + A, + dx, + dy, + rocsparse_v2_spmv_stage_analysis, + &bs_analysis, + nullptr), + rocsparse_status_success); + device_vector buf_a{bs_analysis ? bs_analysis : size_t(1)}; + ASSERT_TRUE(buf_a.ptr); + ASSERT_EQ(rocsparse_v2_spmv(handle, + spmv_descr, + &alpha, + A, + dx, + &beta, + dy, + rocsparse_v2_spmv_stage_analysis, + bs_analysis, + bs_analysis ? buf_a.ptr : nullptr, + nullptr), + rocsparse_status_success); + + size_t bs_compute = 0; + ASSERT_EQ(rocsparse_v2_spmv_buffer_size(handle, + spmv_descr, + A, + dx, + dy, + rocsparse_v2_spmv_stage_compute, + &bs_compute, + nullptr), + rocsparse_status_success); + device_vector buf_c{bs_compute ? bs_compute : size_t(1)}; + ASSERT_TRUE(buf_c.ptr); + ASSERT_EQ(rocsparse_v2_spmv(handle, + spmv_descr, + &alpha, + A, + dx, + &beta, + dy, + rocsparse_v2_spmv_stage_compute, + bs_compute, + bs_compute ? buf_c.ptr : nullptr, + nullptr), + rocsparse_status_success); + ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); + + expect_vectors_near(expected, to_host(y)); + + EXPECT_EQ(rocsparse_destroy_spmv_descr(spmv_descr), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); + } + +} // namespace + +// =========================================================================== +// ellsv : ELL triangular solve (rocsparse_sptrsv on rocsparse_format_ell). +// =========================================================================== +class EllSv : public HandleTest +{ +}; + +#ifdef ROCSPARSE_WITH_ELL_TRSV + +// Lower triangular, non-unit diagonal, no pivot. 3x3: +// [2 ] [y0] [4 ] +// [1 4 ] * [y1] = [12] +// [ 2 8 ] [y2] [40] +// All constants are small powers of two, so the float arithmetic is exact. +TEST_F(EllSv, lower_non_unit) +{ + const int64_t m = 3, n = 3, width = 2; + // ELL_IND(row, slot, m, width) = slot * m + row. + std::vector col_ind{0, 0, 1, -1, 1, 2}; + std::vector val{2, 1, 2, 0, 4, 8}; + + device_vector d_col{col_ind}; + device_vector d_val{val}; + ASSERT_TRUE(d_col.ptr && d_val.ptr); + + rocsparse_spmat_descr A = nullptr; + ASSERT_EQ(rocsparse_create_ell_descr( + &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + rocsparse_status_success); + + const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; + const rocsparse_diag_type diag_type = rocsparse_diag_type_non_unit; + const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); + + std::vector hx{4, 12, 40}; + const float alpha = 1.0f; + + device_vector x{hx}; + device_vector y{std::vector(static_cast(m), 0.0f)}; + ASSERT_TRUE(x.ptr && y.ptr); + + rocsparse_dnvec_descr dx = nullptr, dy = nullptr; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + + int64_t analysis_pivot = -2, solve_pivot = -2; + run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + EXPECT_EQ(analysis_pivot, -1); + EXPECT_EQ(solve_pivot, -1); + + int64_t ref_pivot = -1; + std::vector expected + = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); + EXPECT_EQ(ref_pivot, -1); + expect_vectors_near(expected, to_host(y)); + + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); +} + +// Lower triangular, unit diagonal. The stored diagonal entries carry a +// deliberately wrong value (999) that must be ignored by the solve. 3x3: +// [1 ] [y0] [2] +// [0.5 1 ] * [y1] = [4] +// [ 0.25 1] [y2] [8] +TEST_F(EllSv, lower_unit_diagonal) +{ + const int64_t m = 3, n = 3, width = 2; + std::vector col_ind{-1, 0, 1, 0, 1, 2}; + std::vector val{0, 0.5f, 0.25f, 999, 999, 999}; + + device_vector d_col{col_ind}; + device_vector d_val{val}; + ASSERT_TRUE(d_col.ptr && d_val.ptr); + + rocsparse_spmat_descr A = nullptr; + ASSERT_EQ(rocsparse_create_ell_descr( + &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + rocsparse_status_success); + + const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; + const rocsparse_diag_type diag_type = rocsparse_diag_type_unit; + const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); + + std::vector hx{2, 4, 8}; + const float alpha = 1.0f; + + device_vector x{hx}; + device_vector y{std::vector(static_cast(m), 0.0f)}; + ASSERT_TRUE(x.ptr && y.ptr); + + rocsparse_dnvec_descr dx = nullptr, dy = nullptr; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + + int64_t analysis_pivot = -2, solve_pivot = -2; + run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + EXPECT_EQ(analysis_pivot, -1); + EXPECT_EQ(solve_pivot, -1); + + int64_t ref_pivot = -1; + std::vector expected + = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); + EXPECT_EQ(ref_pivot, -1); + // y = {2, 3, 7.25}: the garbage diagonal values must not leak in. + expect_vectors_near(expected, to_host(y)); + + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); +} + +// Lower triangular, non-unit diagonal, explicit zero on the diagonal of row 1 +// (a numeric, not symbolic, zero pivot: the entry is present, just zero +// valued). The analysis stage only checks for a *missing* diagonal entry, so +// it must report no pivot; the compute stage must report the zero pivot and +// still produce the same "treat the zero as 1" numeric result the kernel +// computes. 3x3: +// [2 ] [y0] [4 ] +// [1 0 ] * [y1] = [10] +// [ 3 4 ] [y2] [20] +TEST_F(EllSv, zero_pivot) +{ + const int64_t m = 3, n = 3, width = 2; + std::vector col_ind{0, 0, 1, -1, 1, 2}; + std::vector val{2, 1, 3, 0, 0, 4}; + + device_vector d_col{col_ind}; + device_vector d_val{val}; + ASSERT_TRUE(d_col.ptr && d_val.ptr); + + rocsparse_spmat_descr A = nullptr; + ASSERT_EQ(rocsparse_create_ell_descr( + &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + rocsparse_status_success); + + const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; + const rocsparse_diag_type diag_type = rocsparse_diag_type_non_unit; + const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); + + std::vector hx{4, 10, 20}; + const float alpha = 1.0f; + + device_vector x{hx}; + device_vector y{std::vector(static_cast(m), 0.0f)}; + ASSERT_TRUE(x.ptr && y.ptr); + + rocsparse_dnvec_descr dx = nullptr, dy = nullptr; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + + int64_t analysis_pivot = -2, solve_pivot = -2; + run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + // The diagonal entry is present (just zero), so analysis finds no + // *missing* diagonal; the zero is only caught numerically during solve. + EXPECT_EQ(analysis_pivot, -1); + EXPECT_EQ(solve_pivot, 1); + + int64_t ref_pivot = -1; + std::vector expected + = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); + EXPECT_EQ(ref_pivot, 1); + // y = {2, 8, -1}, using the library's "treat a zero diagonal as 1" rule. + expect_vectors_near(expected, to_host(y)); + + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); + EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); +} + +#else // !ROCSPARSE_WITH_ELL_TRSV + +TEST_F(EllSv, skipped_ell_trsv_not_built) +{ + GTEST_SKIP() << "ELL triangular solve support not built " + "(BUILD_WITH_ELL_TRSV=OFF / ROCSPARSE_WITH_ELL_TRSV not defined); " + "rocsparse_sptrsv on rocsparse_format_ell returns rocsparse_status_not_implemented."; +} + +#endif // ROCSPARSE_WITH_ELL_TRSV + +// =========================================================================== +// sellmv : Sliced ELL SpMV (rocsparse_v2_spmv on rocsparse_format_sell). +// =========================================================================== +class SellMv : public HandleTest +{ +}; + +// Non-transpose, slice size 32 (<=128, small-slice kernel sellmvn_device). +// rocsparse_create_sell_descr requires sell_slice_size <= rows, so m = n = +// slice = 32: a single, exactly-full slice. +TEST_F(SellMv, none_small_slice) +{ + const int64_t m = 32, n = 32, slice = 32; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell(m, slice, offs, cols, vals); + + std::vector hx = make_varying_x(n); + std::vector hy0(static_cast(m), 0.0f); + const float alpha = 2.0f, beta = 0.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + + run_v2_spmv_sell( + handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); +} + +// Transpose, slice size 32 (small-slice kernel sellmvt_device), m = 40 is not +// a multiple of 32 so the second (and last) slice is only 8 rows out of 32 -- +// a partial last slice. This is exactly the configuration where +// sellmvt_device's padding threads (local rows 8..31 of slice 1, global rows +// 40..63) read past the end of x; see the file comment at the top. The +// padding threads' loop range is empty, so the numeric result is unaffected. +TEST_F(SellMv, transpose_small_slice_partial_last_slice) +{ + const int64_t m = 40, n = 40, slice = 32; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell(m, slice, offs, cols, vals); + + std::vector hx = make_varying_x(m); + std::vector hy0(static_cast(n), 0.0f); + const float alpha = 1.0f, beta = 0.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); + + run_v2_spmv_sell(handle, + rocsparse_operation_transpose, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); +} + +// Non-transpose, slice size 256 (>128, large-slice kernel +// sellmvn_large_slice_device). rocsparse_create_sell_descr requires +// sell_slice_size <= rows, which rules out keeping this one under 64 rows (as +// the other cases are); m = n = slice = 256 is the smallest matrix that still +// reaches this kernel, one exactly-full slice. +TEST_F(SellMv, none_large_slice) +{ + const int64_t m = 256, n = 256, slice = 256; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell(m, slice, offs, cols, vals); + + std::vector hx = make_varying_x(n); + std::vector hy0(static_cast(m), 0.0f); + const float alpha = 1.0f, beta = 0.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + + run_v2_spmv_sell( + handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); +} + +// Transpose, slice size 256 (large-slice kernel sellmvt_large_slice_device). +// Same sell_slice_size <= rows constraint as none_large_slice above. +TEST_F(SellMv, transpose_large_slice) +{ + const int64_t m = 256, n = 256, slice = 256; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell(m, slice, offs, cols, vals); + + std::vector hx = make_varying_x(m); + std::vector hy0(static_cast(n), 0.0f); + const float alpha = 1.0f, beta = 0.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); + + run_v2_spmv_sell(handle, + rocsparse_operation_transpose, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); +} + +// Slice size 1: rocsparse_sellmv_impl dispatches this straight to the CSR +// fallback (rocsparse::csrmv_template on the slice-offsets-as-row-pointer +// data), bypassing sellmv_device.h entirely. +TEST_F(SellMv, slice_size_one_csr_fallback) +{ + const int64_t m = 5, n = 5, slice = 1; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell_csr_exact(m, offs, cols, vals); + + std::vector hx = make_varying_x(n); + std::vector hy0(static_cast(m), 0.0f); + const float alpha = 1.0f, beta = 0.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + + run_v2_spmv_sell( + handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); +} + +// beta != 0, so y is read as well as written and the kernels take their +// fma(beta, y[row], ...) path instead of the plain store. Non-transpose, +// slice size 32 (small-slice kernel). All constants are small integers, so +// the float arithmetic is exact. +TEST_F(SellMv, none_small_slice_nonzero_beta) +{ + const int64_t m = 32, n = 32, slice = 32; + std::vector offs, cols; + std::vector vals; + build_bidiagonal_sell(m, slice, offs, cols, vals); + + std::vector hx = make_varying_x(n); + std::vector hy0(static_cast(m)); + for(int64_t i = 0; i < m; ++i) + { + hy0[static_cast(i)] = static_cast(2 * (i + 1)); + } + const float alpha = 2.0f, beta = 3.0f; + + std::vector expected + = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + + run_v2_spmv_sell( + handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); +} From fab6cce5621127584f0b06c38f81222936c4c25a Mon Sep 17 00:00:00 2001 From: Kim Liegeois Date: Tue, 20 Oct 2026 20:49:13 +0000 Subject: [PATCH 02/10] style(rocsparse): apply clang-format --- .../unit_test_internal_level2_ell_sell.cpp | 353 +++++++++++------- 1 file changed, 219 insertions(+), 134 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 10d509d54b02..0fcb4e4309c7 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -118,15 +118,15 @@ namespace // - only the strictly-lower entries (plus the diagonal) contribute. // ell_col_ind / ell_val are laid out exactly like rocsparse_create_ell_descr // expects: column-major, ELL_IND(row, slot, m, width) = slot * m + row. - std::vector host_ellsv_lower(int64_t m, - int64_t n, - int64_t ell_width, + std::vector host_ellsv_lower(int64_t m, + int64_t n, + int64_t ell_width, const std::vector& ell_col_ind, const std::vector& ell_val, const std::vector& x, - float alpha, - rocsparse_diag_type diag_type, - int64_t* zero_pivot) + float alpha, + rocsparse_diag_type diag_type, + int64_t* zero_pivot) { std::vector y(static_cast(m), 0.0f); *zero_pivot = -1; @@ -182,13 +182,13 @@ namespace // triangular ELL matrix A, operation none, alpha on the host. Returns the // zero-pivot position reported after analysis and after compute (-1 if // none was reported at that stage). - void run_ellsv(rocsparse_handle handle, - rocsparse_spmat_descr A, - rocsparse_dnvec_descr x, - rocsparse_dnvec_descr y, - float alpha, - int64_t* analysis_pivot, - int64_t* solve_pivot) + void run_ellsv(rocsparse_handle handle, + rocsparse_spmat_descr A, + rocsparse_dnvec_descr x, + rocsparse_dnvec_descr y, + float alpha, + int64_t* analysis_pivot, + int64_t* solve_pivot) { rocsparse_sptrsv_descr descr = nullptr; ASSERT_EQ(rocsparse_create_sptrsv_descr(&descr), rocsparse_status_success); @@ -197,26 +197,21 @@ namespace const rocsparse_sptrsv_alg alg = rocsparse_sptrsv_alg_default; const rocsparse_datatype dt = rocsparse_datatype_f32_r; + ASSERT_EQ( + rocsparse_sptrsv_set_input( + handle, descr, rocsparse_sptrsv_input_operation, &trans, sizeof(trans), nullptr), + rocsparse_status_success); ASSERT_EQ(rocsparse_sptrsv_set_input( - handle, descr, rocsparse_sptrsv_input_operation, &trans, sizeof(trans), nullptr), + handle, descr, rocsparse_sptrsv_input_alg, &alg, sizeof(alg), nullptr), rocsparse_status_success); ASSERT_EQ( - rocsparse_sptrsv_set_input(handle, descr, rocsparse_sptrsv_input_alg, &alg, sizeof(alg), nullptr), + rocsparse_sptrsv_set_input( + handle, descr, rocsparse_sptrsv_input_scalar_datatype, &dt, sizeof(dt), nullptr), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_sptrsv_set_input( + handle, descr, rocsparse_sptrsv_input_compute_datatype, &dt, sizeof(dt), nullptr), rocsparse_status_success); - ASSERT_EQ(rocsparse_sptrsv_set_input(handle, - descr, - rocsparse_sptrsv_input_scalar_datatype, - &dt, - sizeof(dt), - nullptr), - rocsparse_status_success); - ASSERT_EQ(rocsparse_sptrsv_set_input(handle, - descr, - rocsparse_sptrsv_input_compute_datatype, - &dt, - sizeof(dt), - nullptr), - rocsparse_status_success); // Mandatory: the descriptor's analysis policy defaults to an invalid // sentinel, so rocsparse::ellsv_analysis's ROCSPARSE_CHECKARG_ENUM on it // fails with rocsparse_status_invalid_value unless this is set. @@ -230,9 +225,10 @@ namespace rocsparse_status_success); size_t bs_analysis = 0; - ASSERT_EQ(rocsparse_sptrsv_buffer_size( - handle, descr, A, x, y, rocsparse_sptrsv_stage_analysis, &bs_analysis, nullptr), - rocsparse_status_success); + ASSERT_EQ( + rocsparse_sptrsv_buffer_size( + handle, descr, A, x, y, rocsparse_sptrsv_stage_analysis, &bs_analysis, nullptr), + rocsparse_status_success); device_vector buf_a{bs_analysis ? bs_analysis : size_t(1)}; ASSERT_TRUE(buf_a.ptr); ASSERT_EQ(rocsparse_sptrsv(handle, @@ -246,9 +242,14 @@ namespace nullptr), rocsparse_status_success); - int64_t pivot = -1; - rocsparse_status pivot_status = rocsparse_sptrsv_get_output( - handle, descr, rocsparse_sptrsv_output_zero_pivot_position, &pivot, sizeof(pivot), nullptr); + int64_t pivot = -1; + rocsparse_status pivot_status + = rocsparse_sptrsv_get_output(handle, + descr, + rocsparse_sptrsv_output_zero_pivot_position, + &pivot, + sizeof(pivot), + nullptr); ASSERT_TRUE(pivot_status == rocsparse_status_success || pivot_status == rocsparse_status_zero_pivot); *analysis_pivot = pivot; @@ -281,8 +282,12 @@ namespace ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); pivot = -1; - pivot_status = rocsparse_sptrsv_get_output( - handle, descr, rocsparse_sptrsv_output_zero_pivot_position, &pivot, sizeof(pivot), nullptr); + pivot_status = rocsparse_sptrsv_get_output(handle, + descr, + rocsparse_sptrsv_output_zero_pivot_position, + &pivot, + sizeof(pivot), + nullptr); ASSERT_TRUE(pivot_status == rocsparse_status_success || pivot_status == rocsparse_status_zero_pivot); *solve_pivot = pivot; @@ -303,19 +308,19 @@ namespace // [slice_offsets[s], slice_offsets[s+1]) with depth = (end - start) / // sell_slice_size, slot p, local row r (0 <= r < sell_slice_size), the // entry lives at slice_offsets[s] + p * sell_slice_size + r. - std::vector host_sellmv(int64_t m, - int64_t n, - int64_t sell_slice_size, + std::vector host_sellmv(int64_t m, + int64_t n, + int64_t sell_slice_size, const std::vector& slice_offsets, const std::vector& col_ind, const std::vector& val, const std::vector& x, - float alpha, - float beta, + float alpha, + float beta, const std::vector& y_init, - rocsparse_operation trans) + rocsparse_operation trans) { - const int64_t out_size = (trans == rocsparse_operation_none) ? m : n; + const int64_t out_size = (trans == rocsparse_operation_none) ? m : n; std::vector result(static_cast(out_size)); for(int64_t i = 0; i < out_size; ++i) { @@ -380,8 +385,8 @@ namespace // any sell_slice_size >= 2; for sell_slice_size == 1 use // build_bidiagonal_sell_csr_exact instead (slice_size == 1 is dispatched // straight to the CSR fallback, which does not skip padded entries). - void build_bidiagonal_sell(int64_t m, - int64_t sell_slice_size, + void build_bidiagonal_sell(int64_t m, + int64_t sell_slice_size, std::vector& slice_offsets, std::vector& col_ind, std::vector& val) @@ -392,7 +397,8 @@ namespace slice_offsets.assign(static_cast(nslices + 1), 0); for(int64_t s = 0; s <= nslices; ++s) { - slice_offsets[static_cast(s)] = static_cast(s * sell_slice_size * depth); + slice_offsets[static_cast(s)] + = static_cast(s * sell_slice_size * depth); } const int64_t colval_size = nslices * sell_slice_size * depth; @@ -410,14 +416,14 @@ namespace continue; } // slot 0: main diagonal. - const int64_t idx0 = start + 0 * sell_slice_size + r; + const int64_t idx0 = start + 0 * sell_slice_size + r; col_ind[static_cast(idx0)] = static_cast(row); val[static_cast(idx0)] = static_cast(row + 1); // slot 1: sub-diagonal (none for row 0). if(row >= 1) { - const int64_t idx1 = start + 1 * sell_slice_size + r; + const int64_t idx1 = start + 1 * sell_slice_size + r; col_ind[static_cast(idx1)] = static_cast(row - 1); val[static_cast(idx1)] = static_cast(row); } @@ -428,7 +434,7 @@ namespace // Same bidiagonal pattern, but as an exact CSR structure (slice_offsets // behaves as a CSR row pointer, no padding) for the sell_slice_size == 1 // dispatch, which routes straight through rocsparse::csrmv_template. - void build_bidiagonal_sell_csr_exact(int64_t m, + void build_bidiagonal_sell_csr_exact(int64_t m, std::vector& slice_offsets, std::vector& col_ind, std::vector& val) @@ -466,8 +472,8 @@ namespace const std::vector& val, int64_t sell_slice_size, const std::vector& hx, - float alpha, - float beta, + float alpha, + float beta, const std::vector& hy_init, const std::vector& expected) { @@ -519,16 +525,14 @@ namespace ASSERT_EQ(rocsparse_spmv_set_input( handle, spmv_descr, rocsparse_spmv_input_alg, &alg, sizeof(alg), nullptr), rocsparse_status_success); - ASSERT_EQ(rocsparse_spmv_set_input( - handle, spmv_descr, rocsparse_spmv_input_operation, &trans, sizeof(trans), nullptr), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmv_set_input(handle, - spmv_descr, - rocsparse_spmv_input_scalar_datatype, - &dt, - sizeof(dt), - nullptr), - rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmv_set_input( + handle, spmv_descr, rocsparse_spmv_input_operation, &trans, sizeof(trans), nullptr), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmv_set_input( + handle, spmv_descr, rocsparse_spmv_input_scalar_datatype, &dt, sizeof(dt), nullptr), + rocsparse_status_success); ASSERT_EQ(rocsparse_spmv_set_input(handle, spmv_descr, rocsparse_spmv_input_compute_datatype, @@ -624,37 +628,49 @@ TEST_F(EllSv, lower_non_unit) ASSERT_TRUE(d_col.ptr && d_val.ptr); rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr( - &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_ell_descr(&A, + m, + n, + d_col.ptr, + d_val.ptr, + width, + rocsparse_indextype_i32, + rocsparse_index_base_zero, + rocsparse_datatype_f32_r), rocsparse_status_success); const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; const rocsparse_diag_type diag_type = rocsparse_diag_type_non_unit; const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), - rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); std::vector hx{4, 12, 40}; - const float alpha = 1.0f; + const float alpha = 1.0f; device_vector x{hx}; device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); EXPECT_EQ(analysis_pivot, -1); EXPECT_EQ(solve_pivot, -1); - int64_t ref_pivot = -1; + int64_t ref_pivot = -1; std::vector expected = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, -1); @@ -672,7 +688,7 @@ TEST_F(EllSv, lower_non_unit) // [ 0.25 1] [y2] [8] TEST_F(EllSv, lower_unit_diagonal) { - const int64_t m = 3, n = 3, width = 2; + const int64_t m = 3, n = 3, width = 2; std::vector col_ind{-1, 0, 1, 0, 1, 2}; std::vector val{0, 0.5f, 0.25f, 999, 999, 999}; @@ -681,37 +697,49 @@ TEST_F(EllSv, lower_unit_diagonal) ASSERT_TRUE(d_col.ptr && d_val.ptr); rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr( - &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_ell_descr(&A, + m, + n, + d_col.ptr, + d_val.ptr, + width, + rocsparse_indextype_i32, + rocsparse_index_base_zero, + rocsparse_datatype_f32_r), rocsparse_status_success); const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; const rocsparse_diag_type diag_type = rocsparse_diag_type_unit; const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), - rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); std::vector hx{2, 4, 8}; - const float alpha = 1.0f; + const float alpha = 1.0f; device_vector x{hx}; device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); EXPECT_EQ(analysis_pivot, -1); EXPECT_EQ(solve_pivot, -1); - int64_t ref_pivot = -1; + int64_t ref_pivot = -1; std::vector expected = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, -1); @@ -734,7 +762,7 @@ TEST_F(EllSv, lower_unit_diagonal) // [ 3 4 ] [y2] [20] TEST_F(EllSv, zero_pivot) { - const int64_t m = 3, n = 3, width = 2; + const int64_t m = 3, n = 3, width = 2; std::vector col_ind{0, 0, 1, -1, 1, 2}; std::vector val{2, 1, 3, 0, 0, 4}; @@ -743,30 +771,42 @@ TEST_F(EllSv, zero_pivot) ASSERT_TRUE(d_col.ptr && d_val.ptr); rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr( - &A, m, n, d_col.ptr, d_val.ptr, width, rocsparse_indextype_i32, rocsparse_index_base_zero, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_ell_descr(&A, + m, + n, + d_col.ptr, + d_val.ptr, + width, + rocsparse_indextype_i32, + rocsparse_index_base_zero, + rocsparse_datatype_f32_r), rocsparse_status_success); const rocsparse_fill_mode fill_mode = rocsparse_fill_mode_lower; const rocsparse_diag_type diag_type = rocsparse_diag_type_non_unit; const rocsparse_matrix_type mat_type = rocsparse_matrix_type_general; - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), - rocsparse_status_success); - ASSERT_EQ(rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), - rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_fill_mode, &fill_mode, sizeof(fill_mode)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_diag_type, &diag_type, sizeof(diag_type)), + rocsparse_status_success); + ASSERT_EQ( + rocsparse_spmat_set_attribute(A, rocsparse_spmat_matrix_type, &mat_type, sizeof(mat_type)), + rocsparse_status_success); std::vector hx{4, 10, 20}; - const float alpha = 1.0f; + const float alpha = 1.0f; device_vector x{hx}; device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); @@ -775,7 +815,7 @@ TEST_F(EllSv, zero_pivot) EXPECT_EQ(analysis_pivot, -1); EXPECT_EQ(solve_pivot, 1); - int64_t ref_pivot = -1; + int64_t ref_pivot = -1; std::vector expected = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, 1); @@ -791,9 +831,10 @@ TEST_F(EllSv, zero_pivot) TEST_F(EllSv, skipped_ell_trsv_not_built) { - GTEST_SKIP() << "ELL triangular solve support not built " - "(BUILD_WITH_ELL_TRSV=OFF / ROCSPARSE_WITH_ELL_TRSV not defined); " - "rocsparse_sptrsv on rocsparse_format_ell returns rocsparse_status_not_implemented."; + GTEST_SKIP() + << "ELL triangular solve support not built " + "(BUILD_WITH_ELL_TRSV=OFF / ROCSPARSE_WITH_ELL_TRSV not defined); " + "rocsparse_sptrsv on rocsparse_format_ell returns rocsparse_status_not_implemented."; } #endif // ROCSPARSE_WITH_ELL_TRSV @@ -810,20 +851,31 @@ class SellMv : public HandleTest // slice = 32: a single, exactly-full slice. TEST_F(SellMv, none_small_slice) { - const int64_t m = 32, n = 32, slice = 32; + const int64_t m = 32, n = 32, slice = 32; std::vector offs, cols; std::vector vals; build_bidiagonal_sell(m, slice, offs, cols, vals); std::vector hx = make_varying_x(n); std::vector hy0(static_cast(m), 0.0f); - const float alpha = 2.0f, beta = 0.0f; + const float alpha = 2.0f, beta = 0.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell( - handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); + run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); } // Transpose, slice size 32 (small-slice kernel sellmvt_device), m = 40 is not @@ -834,17 +886,17 @@ TEST_F(SellMv, none_small_slice) // padding threads' loop range is empty, so the numeric result is unaffected. TEST_F(SellMv, transpose_small_slice_partial_last_slice) { - const int64_t m = 40, n = 40, slice = 32; + const int64_t m = 40, n = 40, slice = 32; std::vector offs, cols; std::vector vals; build_bidiagonal_sell(m, slice, offs, cols, vals); std::vector hx = make_varying_x(m); std::vector hy0(static_cast(n), 0.0f); - const float alpha = 1.0f, beta = 0.0f; + const float alpha = 1.0f, beta = 0.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); run_v2_spmv_sell(handle, rocsparse_operation_transpose, @@ -868,37 +920,48 @@ TEST_F(SellMv, transpose_small_slice_partial_last_slice) // reaches this kernel, one exactly-full slice. TEST_F(SellMv, none_large_slice) { - const int64_t m = 256, n = 256, slice = 256; + const int64_t m = 256, n = 256, slice = 256; std::vector offs, cols; std::vector vals; build_bidiagonal_sell(m, slice, offs, cols, vals); std::vector hx = make_varying_x(n); std::vector hy0(static_cast(m), 0.0f); - const float alpha = 1.0f, beta = 0.0f; + const float alpha = 1.0f, beta = 0.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell( - handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); + run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); } // Transpose, slice size 256 (large-slice kernel sellmvt_large_slice_device). // Same sell_slice_size <= rows constraint as none_large_slice above. TEST_F(SellMv, transpose_large_slice) { - const int64_t m = 256, n = 256, slice = 256; + const int64_t m = 256, n = 256, slice = 256; std::vector offs, cols; std::vector vals; build_bidiagonal_sell(m, slice, offs, cols, vals); std::vector hx = make_varying_x(m); std::vector hy0(static_cast(n), 0.0f); - const float alpha = 1.0f, beta = 0.0f; + const float alpha = 1.0f, beta = 0.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); run_v2_spmv_sell(handle, rocsparse_operation_transpose, @@ -920,20 +983,31 @@ TEST_F(SellMv, transpose_large_slice) // data), bypassing sellmv_device.h entirely. TEST_F(SellMv, slice_size_one_csr_fallback) { - const int64_t m = 5, n = 5, slice = 1; + const int64_t m = 5, n = 5, slice = 1; std::vector offs, cols; std::vector vals; build_bidiagonal_sell_csr_exact(m, offs, cols, vals); std::vector hx = make_varying_x(n); std::vector hy0(static_cast(m), 0.0f); - const float alpha = 1.0f, beta = 0.0f; + const float alpha = 1.0f, beta = 0.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell( - handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); + run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); } // beta != 0, so y is read as well as written and the kernels take their @@ -942,7 +1016,7 @@ TEST_F(SellMv, slice_size_one_csr_fallback) // the float arithmetic is exact. TEST_F(SellMv, none_small_slice_nonzero_beta) { - const int64_t m = 32, n = 32, slice = 32; + const int64_t m = 32, n = 32, slice = 32; std::vector offs, cols; std::vector vals; build_bidiagonal_sell(m, slice, offs, cols, vals); @@ -955,9 +1029,20 @@ TEST_F(SellMv, none_small_slice_nonzero_beta) } const float alpha = 2.0f, beta = 3.0f; - std::vector expected - = host_sellmv(m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); + std::vector expected = host_sellmv( + m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell( - handle, rocsparse_operation_none, m, n, offs, cols, vals, slice, hx, alpha, beta, hy0, expected); + run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected); } From 63d7936d8bbb5bffac530bf7b5a165389c8ea865 Mon Sep 17 00:00:00 2001 From: kliegeois Date: Wed, 21 Oct 2026 16:52:16 +0000 Subject: [PATCH 03/10] test(rocsparse): reword SELL partial-last-slice test comments (ROCM-33114) Drop the file-header note and say in the test comment what the transpose partial-last-slice case checks and what it does not. --- .../unit_test_internal_level2_ell_sell.cpp | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 0fcb4e4309c7..91326c7bf813 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -48,19 +48,6 @@ // directions, including a transpose case whose matrix size is not a // multiple of the slice size (a partial last slice). // -// NOTE: on current develop, sellmvt_device reads x[row] before checking -// row < m (the bounds check only guards the start/end offsets used by the -// accumulation loop, which is why padding threads never touch -// sell_col_ind / sell_val). That stray read of x is harmless here -- the -// loaded value is never used because the padding thread's own start/end -// range is empty -- but it means the transpose/partial-last-slice test -// below must size x so that the read stays in-bounds for every slice -// (see build_bidiagonal_sell). This fix is already implemented in open PR -// https://github.com/ROCm/rocm-libraries/pull/13339 (not merged at the -// time of writing); this test does not attempt to change -// sellmv_device.h, it only asserts the numeric result, which is correct -// either way. -// #include "unit_test_utils.hpp" #include "rocsparse.h" @@ -878,12 +865,10 @@ TEST_F(SellMv, none_small_slice) expected); } -// Transpose, slice size 32 (small-slice kernel sellmvt_device), m = 40 is not -// a multiple of 32 so the second (and last) slice is only 8 rows out of 32 -- -// a partial last slice. This is exactly the configuration where -// sellmvt_device's padding threads (local rows 8..31 of slice 1, global rows -// 40..63) read past the end of x; see the file comment at the top. The -// padding threads' loop range is empty, so the numeric result is unaffected. +// Transpose, slice size 32 (small-slice kernel sellmvt_device). m = 40 is not a +// multiple of 32, so the last slice holds 8 rows and its local rows 8..31 are +// padding threads past row m. Those threads must not read x or the slice data. +// This test checks only y, so it does not detect an out-of-bounds read by them. TEST_F(SellMv, transpose_small_slice_partial_last_slice) { const int64_t m = 40, n = 40, slice = 32; From 360de5f508051b0b595d8c9b16fd3882eb583d7c Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 23 Oct 2026 16:29:30 +0000 Subject: [PATCH 04/10] test(rocsparse): compare ELL/SELL unit-test results exactly (ROCM-33114) Every input is a small integer or dyadic fraction and every partial sum stays below 2^24, so the old 1e-4 absolute tolerance was already an exact comparison for most values. Say so and use EXPECT_EQ. Drop the unused include. --- .../unit_test_internal_level2_ell_sell.cpp | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 91326c7bf813..6be513b2d561 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -52,27 +52,18 @@ #include "rocsparse.h" -#include - using namespace rocsparse_ut; namespace { - // Loose-enough tolerance for the float computations below. Most of the - // matrices are built from small powers of two so the host reference and - // the device result usually match exactly; this only guards against - // summation-order differences. - constexpr double kTol = 1e-4; - - void expect_vectors_near(const std::vector& expected, - const std::vector& actual, - double tol = kTol) + // Every input is a small integer or dyadic fraction and every partial sum + // stays below 2^24, so float results are exact regardless of summation order. + void expect_vectors_eq(const std::vector& expected, const std::vector& actual) { ASSERT_EQ(expected.size(), actual.size()); for(size_t i = 0; i < expected.size(); ++i) { - EXPECT_NEAR(static_cast(actual[i]), static_cast(expected[i]), tol) - << "index " << i; + EXPECT_EQ(actual[i], expected[i]) << "index " << i; } } @@ -579,7 +570,7 @@ namespace rocsparse_status_success); ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); - expect_vectors_near(expected, to_host(y)); + expect_vectors_eq(expected, to_host(y)); EXPECT_EQ(rocsparse_destroy_spmv_descr(spmv_descr), rocsparse_status_success); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); @@ -602,7 +593,6 @@ class EllSv : public HandleTest // [2 ] [y0] [4 ] // [1 4 ] * [y1] = [12] // [ 2 8 ] [y2] [40] -// All constants are small powers of two, so the float arithmetic is exact. TEST_F(EllSv, lower_non_unit) { const int64_t m = 3, n = 3, width = 2; @@ -661,7 +651,7 @@ TEST_F(EllSv, lower_non_unit) std::vector expected = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, -1); - expect_vectors_near(expected, to_host(y)); + expect_vectors_eq(expected, to_host(y)); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); @@ -731,7 +721,7 @@ TEST_F(EllSv, lower_unit_diagonal) = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, -1); // y = {2, 3, 7.25}: the garbage diagonal values must not leak in. - expect_vectors_near(expected, to_host(y)); + expect_vectors_eq(expected, to_host(y)); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); @@ -807,7 +797,7 @@ TEST_F(EllSv, zero_pivot) = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, 1); // y = {2, 8, -1}, using the library's "treat a zero diagonal as 1" rule. - expect_vectors_near(expected, to_host(y)); + expect_vectors_eq(expected, to_host(y)); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); From 399bc78d32639cb2c68ec5ce1166f2e40b7ba7bf Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 23 Oct 2026 16:29:50 +0000 Subject: [PATCH 05/10] test(rocsparse): check ELL solve pivot status against the reported position (ROCM-33114) Preset the zero-pivot output to -2 so a get_output that never writes it is caught, and require rocsparse_status_success exactly when the position is -1 and rocsparse_status_zero_pivot otherwise, as testing_sptrsv_ell does. --- .../unittests/unit_test_internal_level2_ell_sell.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 6be513b2d561..af546cc5ef62 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -220,7 +220,7 @@ namespace nullptr), rocsparse_status_success); - int64_t pivot = -1; + int64_t pivot = -2; rocsparse_status pivot_status = rocsparse_sptrsv_get_output(handle, descr, @@ -228,8 +228,8 @@ namespace &pivot, sizeof(pivot), nullptr); - ASSERT_TRUE(pivot_status == rocsparse_status_success - || pivot_status == rocsparse_status_zero_pivot); + ASSERT_EQ(pivot_status, + pivot == -1 ? rocsparse_status_success : rocsparse_status_zero_pivot); *analysis_pivot = pivot; const void* alpha_ptr = α @@ -259,15 +259,15 @@ namespace rocsparse_status_success); ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); - pivot = -1; + pivot = -2; pivot_status = rocsparse_sptrsv_get_output(handle, descr, rocsparse_sptrsv_output_zero_pivot_position, &pivot, sizeof(pivot), nullptr); - ASSERT_TRUE(pivot_status == rocsparse_status_success - || pivot_status == rocsparse_status_zero_pivot); + ASSERT_EQ(pivot_status, + pivot == -1 ? rocsparse_status_success : rocsparse_status_zero_pivot); *solve_pivot = pivot; EXPECT_EQ(rocsparse_destroy_sptrsv_descr(descr), rocsparse_status_success); From 0473ea1dac8bcb7bb3585eca025ba8e18de5d04a Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 23 Oct 2026 16:30:12 +0000 Subject: [PATCH 06/10] test(rocsparse): stop ELL/SELL unit tests on helper assertion failures (ROCM-33114) run_ellsv and run_v2_spmv_sell are void helpers, so a failing ASSERT inside them only returned from the helper and the test went on to check stale output. Wrap every call in ASSERT_NO_FATAL_FAILURE. --- .../unit_test_internal_level2_ell_sell.cpp | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index af546cc5ef62..47e206197a72 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -643,7 +643,7 @@ TEST_F(EllSv, lower_non_unit) rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; - run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + ASSERT_NO_FATAL_FAILURE(run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot)); EXPECT_EQ(analysis_pivot, -1); EXPECT_EQ(solve_pivot, -1); @@ -712,7 +712,7 @@ TEST_F(EllSv, lower_unit_diagonal) rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; - run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + ASSERT_NO_FATAL_FAILURE(run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot)); EXPECT_EQ(analysis_pivot, -1); EXPECT_EQ(solve_pivot, -1); @@ -786,7 +786,7 @@ TEST_F(EllSv, zero_pivot) rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; - run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot); + ASSERT_NO_FATAL_FAILURE(run_ellsv(handle, A, dx, dy, alpha, &analysis_pivot, &solve_pivot)); // The diagonal entry is present (just zero), so analysis finds no // *missing* diagonal; the zero is only caught numerically during solve. EXPECT_EQ(analysis_pivot, -1); @@ -840,19 +840,19 @@ TEST_F(SellMv, none_small_slice) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell(handle, - rocsparse_operation_none, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } // Transpose, slice size 32 (small-slice kernel sellmvt_device). m = 40 is not a @@ -873,19 +873,19 @@ TEST_F(SellMv, transpose_small_slice_partial_last_slice) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); - run_v2_spmv_sell(handle, - rocsparse_operation_transpose, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_transpose, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } // Non-transpose, slice size 256 (>128, large-slice kernel @@ -907,19 +907,19 @@ TEST_F(SellMv, none_large_slice) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell(handle, - rocsparse_operation_none, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } // Transpose, slice size 256 (large-slice kernel sellmvt_large_slice_device). @@ -938,19 +938,19 @@ TEST_F(SellMv, transpose_large_slice) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); - run_v2_spmv_sell(handle, - rocsparse_operation_transpose, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_transpose, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } // Slice size 1: rocsparse_sellmv_impl dispatches this straight to the CSR @@ -970,19 +970,19 @@ TEST_F(SellMv, slice_size_one_csr_fallback) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell(handle, - rocsparse_operation_none, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } // beta != 0, so y is read as well as written and the kernels take their @@ -1007,17 +1007,17 @@ TEST_F(SellMv, none_small_slice_nonzero_beta) std::vector expected = host_sellmv( m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - run_v2_spmv_sell(handle, - rocsparse_operation_none, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected); + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + rocsparse_operation_none, + m, + n, + offs, + cols, + vals, + slice, + hx, + alpha, + beta, + hy0, + expected)); } From fc05d4e94d042cd410b962257e3eaed924a702b4 Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 23 Oct 2026 16:31:43 +0000 Subject: [PATCH 07/10] test(rocsparse): cover SELL slice-size boundaries and deep rows (ROCM-33114) The m = slice = 256 cases ran one full slice with iter == 1, so neither half of the large-slice row_in_range guard could be false. Replace them with slice 128 and 129 at m = 600 and slice 300 at m = 700, in both directions: the last small-slice size, the first large-slice size with threads outside the slice, and two passes per slice, each with a partial last slice; m = 600 also leaves a surplus block in the launch grid. y now carries a sentinel tail past its length, so a non-transpose write past row m fails the test. Add a small-slice case per direction with 10 entries per row, so the strided loop takes a second step and every reduction level combines real data. build_bidiagonal_sell becomes build_banded_sell with a depth. --- .../unit_test_internal_level2_ell_sell.cpp | 231 +++++++++++------- 1 file changed, 142 insertions(+), 89 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 47e206197a72..81b4e0fbd2d4 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -44,9 +44,9 @@ // (sellmvn_device / sellmvt_device) for sell_slice_size <= 128, the // large-slice kernel (sellmvn_large_slice_device / // sellmvt_large_slice_device) above that, and falls back to csrmv when -// sell_slice_size == 1. This file exercises all three, in both -// directions, including a transpose case whose matrix size is not a -// multiple of the slice size (a partial last slice). +// sell_slice_size == 1. This file exercises all three; the two sliced +// kernels in both directions on both sides of the 128 boundary, with +// partial last slices. // #include "unit_test_utils.hpp" @@ -356,21 +356,21 @@ namespace return x; } - // Builds a tiny general (non-triangular) sliced-ELL matrix with a main - // diagonal (value row+1) and a sub-diagonal (value row, for row >= 1), - // depth 2, padded with -1 / 0 where a slot has no entry (row 0's missing - // sub-diagonal, and every slot in a slice's padding rows). Suitable for - // any sell_slice_size >= 2; for sell_slice_size == 1 use - // build_bidiagonal_sell_csr_exact instead (slice_size == 1 is dispatched + // Builds a lower-banded sliced-ELL matrix: slot p of row r holds column + // r - p with value r + 1 - p, for p < depth and r - p >= 0. Missing entries + // (the first rows' missing sub-diagonals and every slot in a slice's padding + // rows) are padded with -1 / 0. depth == 2 is the bidiagonal matrix used by + // most cases. Suitable for any sell_slice_size >= 2; for sell_slice_size == 1 + // use build_bidiagonal_sell_csr_exact instead (slice_size == 1 is dispatched // straight to the CSR fallback, which does not skip padded entries). - void build_bidiagonal_sell(int64_t m, - int64_t sell_slice_size, - std::vector& slice_offsets, - std::vector& col_ind, - std::vector& val) + void build_banded_sell(int64_t m, + int64_t sell_slice_size, + int64_t depth, + std::vector& slice_offsets, + std::vector& col_ind, + std::vector& val) { - constexpr int64_t depth = 2; - const int64_t nslices = (m + sell_slice_size - 1) / sell_slice_size; + const int64_t nslices = (m + sell_slice_size - 1) / sell_slice_size; slice_offsets.assign(static_cast(nslices + 1), 0); for(int64_t s = 0; s <= nslices; ++s) @@ -393,17 +393,11 @@ namespace { continue; } - // slot 0: main diagonal. - const int64_t idx0 = start + 0 * sell_slice_size + r; - col_ind[static_cast(idx0)] = static_cast(row); - val[static_cast(idx0)] = static_cast(row + 1); - - // slot 1: sub-diagonal (none for row 0). - if(row >= 1) + for(int64_t p = 0; p < depth && row - p >= 0; ++p) { - const int64_t idx1 = start + 1 * sell_slice_size + r; - col_ind[static_cast(idx1)] = static_cast(row - 1); - val[static_cast(idx1)] = static_cast(row); + const int64_t idx = start + p * sell_slice_size + r; + col_ind[static_cast(idx)] = static_cast(row - p); + val[static_cast(idx)] = static_cast(row + 1 - p); } } } @@ -440,7 +434,8 @@ namespace // Creates the SELL spmat descriptor from host data, runs the two-stage // (analysis, compute) rocsparse_v2_spmv pipeline with alg = sell, and - // checks the result against `expected`. + // checks the result against `expected`. y is allocated with a sentinel tail + // past its length, so a write past the last row fails the check. void run_v2_spmv_sell(rocsparse_handle handle, rocsparse_operation trans, int64_t m, @@ -485,8 +480,13 @@ namespace ASSERT_EQ(static_cast(hy_init.size()), y_size); ASSERT_EQ(static_cast(expected.size()), y_size); + constexpr size_t y_tail = 512; + constexpr float y_sentinel = -12345.0f; + std::vector hy(hy_init); + hy.resize(hy_init.size() + y_tail, y_sentinel); + device_vector x{hx}; - device_vector y{hy_init}; + device_vector y{hy}; ASSERT_TRUE(x.ptr && y.ptr); rocsparse_dnvec_descr dx = nullptr, dy = nullptr; @@ -570,7 +570,12 @@ namespace rocsparse_status_success); ASSERT_EQ(hipDeviceSynchronize(), hipSuccess); - expect_vectors_eq(expected, to_host(y)); + const std::vector hy_out = to_host(y); + expect_vectors_eq(expected, std::vector(hy_out.begin(), hy_out.begin() + y_size)); + for(size_t i = expected.size(); i < hy_out.size(); ++i) + { + EXPECT_EQ(hy_out[i], y_sentinel) << "write past y at index " << i; + } EXPECT_EQ(rocsparse_destroy_spmv_descr(spmv_descr), rocsparse_status_success); EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); @@ -578,6 +583,54 @@ namespace EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } + // Runs build_banded_sell(m, sell_slice_size, depth) with x[i] = i + 1 through + // run_v2_spmv_sell and checks it against host_sellmv. + void check_banded_sellmv(rocsparse_handle handle, + rocsparse_operation trans, + int64_t m, + int64_t n, + int64_t sell_slice_size, + int64_t depth, + float alpha, + float beta, + const std::vector& hy_init) + { + std::vector offs, cols; + std::vector vals; + build_banded_sell(m, sell_slice_size, depth, offs, cols, vals); + + const std::vector hx = make_varying_x(trans == rocsparse_operation_none ? n : m); + const std::vector expected + = host_sellmv(m, n, sell_slice_size, offs, cols, vals, hx, alpha, beta, hy_init, trans); + + ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, + trans, + m, + n, + offs, + cols, + vals, + sell_slice_size, + hx, + alpha, + beta, + hy_init, + expected)); + } + + // sell_slice_size > 128 selects the large-slice kernel (256 threads per + // slice). Slice 128 is the largest small-slice size, slice 129 leaves threads + // 129..255 outside the slice, and slice 300 needs two passes (iter == 2). m + // is not a multiple of the slice, so the last slice is partial; at m = 600 + // the sqrt-shaped launch grid also has a surplus block. + struct SellSliceCase + { + int64_t m; + int64_t sell_slice_size; + }; + + constexpr SellSliceCase slice_boundary_cases[] = {{600, 128}, {600, 129}, {700, 300}}; + } // namespace // =========================================================================== @@ -831,7 +884,7 @@ TEST_F(SellMv, none_small_slice) const int64_t m = 32, n = 32, slice = 32; std::vector offs, cols; std::vector vals; - build_bidiagonal_sell(m, slice, offs, cols, vals); + build_banded_sell(m, slice, 2, offs, cols, vals); std::vector hx = make_varying_x(n); std::vector hy0(static_cast(m), 0.0f); @@ -864,7 +917,7 @@ TEST_F(SellMv, transpose_small_slice_partial_last_slice) const int64_t m = 40, n = 40, slice = 32; std::vector offs, cols; std::vector vals; - build_bidiagonal_sell(m, slice, offs, cols, vals); + build_banded_sell(m, slice, 2, offs, cols, vals); std::vector hx = make_varying_x(m); std::vector hy0(static_cast(n), 0.0f); @@ -888,69 +941,69 @@ TEST_F(SellMv, transpose_small_slice_partial_last_slice) expected)); } -// Non-transpose, slice size 256 (>128, large-slice kernel -// sellmvn_large_slice_device). rocsparse_create_sell_descr requires -// sell_slice_size <= rows, which rules out keeping this one under 64 rows (as -// the other cases are); m = n = slice = 256 is the smallest matrix that still -// reaches this kernel, one exactly-full slice. -TEST_F(SellMv, none_large_slice) +TEST_F(SellMv, none_slice_size_boundaries) { - const int64_t m = 256, n = 256, slice = 256; - std::vector offs, cols; - std::vector vals; - build_bidiagonal_sell(m, slice, offs, cols, vals); - - std::vector hx = make_varying_x(n); - std::vector hy0(static_cast(m), 0.0f); - const float alpha = 1.0f, beta = 0.0f; - - std::vector expected = host_sellmv( - m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_none); - - ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, - rocsparse_operation_none, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected)); + for(const SellSliceCase& c : slice_boundary_cases) + { + SCOPED_TRACE(testing::Message() << "m = " << c.m << ", slice = " << c.sell_slice_size); + ASSERT_NO_FATAL_FAILURE( + check_banded_sellmv(handle, + rocsparse_operation_none, + c.m, + c.m, + c.sell_slice_size, + 2, + 1.0f, + 0.0f, + std::vector(static_cast(c.m), 7.0f))); + } } -// Transpose, slice size 256 (large-slice kernel sellmvt_large_slice_device). -// Same sell_slice_size <= rows constraint as none_large_slice above. -TEST_F(SellMv, transpose_large_slice) +TEST_F(SellMv, transpose_slice_size_boundaries) { - const int64_t m = 256, n = 256, slice = 256; - std::vector offs, cols; - std::vector vals; - build_bidiagonal_sell(m, slice, offs, cols, vals); - - std::vector hx = make_varying_x(m); - std::vector hy0(static_cast(n), 0.0f); - const float alpha = 1.0f, beta = 0.0f; + for(const SellSliceCase& c : slice_boundary_cases) + { + SCOPED_TRACE(testing::Message() << "m = " << c.m << ", slice = " << c.sell_slice_size); + ASSERT_NO_FATAL_FAILURE( + check_banded_sellmv(handle, + rocsparse_operation_transpose, + c.m, + c.m, + c.sell_slice_size, + 2, + 1.0f, + 0.0f, + std::vector(static_cast(c.m), 7.0f))); + } +} - std::vector expected = host_sellmv( - m, n, slice, offs, cols, vals, hx, alpha, beta, hy0, rocsparse_operation_transpose); +// Ten entries per row in the small-slice kernel, which gives each row 8 +// threads: threads 0 and 1 take a second strided step, and the non-transpose +// reduction combines real data at every level. The last slice holds 8 rows. +TEST_F(SellMv, none_small_slice_depth10) +{ + ASSERT_NO_FATAL_FAILURE(check_banded_sellmv(handle, + rocsparse_operation_none, + 40, + 40, + 32, + 10, + 2.0f, + 0.0f, + std::vector(40, 7.0f))); +} - ASSERT_NO_FATAL_FAILURE(run_v2_spmv_sell(handle, - rocsparse_operation_transpose, - m, - n, - offs, - cols, - vals, - slice, - hx, - alpha, - beta, - hy0, - expected)); +TEST_F(SellMv, transpose_small_slice_depth10) +{ + ASSERT_NO_FATAL_FAILURE(check_banded_sellmv(handle, + rocsparse_operation_transpose, + 40, + 40, + 32, + 10, + 2.0f, + 0.0f, + std::vector(40, 7.0f))); } // Slice size 1: rocsparse_sellmv_impl dispatches this straight to the CSR @@ -994,7 +1047,7 @@ TEST_F(SellMv, none_small_slice_nonzero_beta) const int64_t m = 32, n = 32, slice = 32; std::vector offs, cols; std::vector vals; - build_bidiagonal_sell(m, slice, offs, cols, vals); + build_banded_sell(m, slice, 2, offs, cols, vals); std::vector hx = make_varying_x(n); std::vector hy0(static_cast(m)); From ca98aebc9be03cde29d70755e90d102bbd8f459c Mon Sep 17 00:00:00 2001 From: kliegeois Date: Fri, 23 Oct 2026 16:32:08 +0000 Subject: [PATCH 08/10] test(rocsparse): make the SELL transpose beta scaling observable (ROCM-33114) Every transpose case used beta = 0 on a zero y with m == n, so dropping the scale_array call before the transpose kernels, or giving it m instead of n, passed. Seed y with 7 in every beta = 0 case, and add a transpose case with beta = 3 and n > m, where y[m..n-1] only get the beta scaling. --- .../unit_test_internal_level2_ell_sell.cpp | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 81b4e0fbd2d4..680a4d922ff5 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -887,7 +887,7 @@ TEST_F(SellMv, none_small_slice) build_banded_sell(m, slice, 2, offs, cols, vals); std::vector hx = make_varying_x(n); - std::vector hy0(static_cast(m), 0.0f); + std::vector hy0(static_cast(m), 7.0f); const float alpha = 2.0f, beta = 0.0f; std::vector expected = host_sellmv( @@ -920,7 +920,7 @@ TEST_F(SellMv, transpose_small_slice_partial_last_slice) build_banded_sell(m, slice, 2, offs, cols, vals); std::vector hx = make_varying_x(m); - std::vector hy0(static_cast(n), 0.0f); + std::vector hy0(static_cast(n), 7.0f); const float alpha = 1.0f, beta = 0.0f; std::vector expected = host_sellmv( @@ -1017,7 +1017,7 @@ TEST_F(SellMv, slice_size_one_csr_fallback) build_bidiagonal_sell_csr_exact(m, offs, cols, vals); std::vector hx = make_varying_x(n); - std::vector hy0(static_cast(m), 0.0f); + std::vector hy0(static_cast(m), 7.0f); const float alpha = 1.0f, beta = 0.0f; std::vector expected = host_sellmv( @@ -1074,3 +1074,18 @@ TEST_F(SellMv, none_small_slice_nonzero_beta) hy0, expected)); } + +// Transpose with beta != 0 and n > m. The bidiagonal columns stop at m - 1, so +// y[m..n-1] only see the beta scaling, which has to cover all n entries of y. +TEST_F(SellMv, transpose_nonzero_beta_wide) +{ + const int64_t m = 40, n = 72; + std::vector hy0(static_cast(n)); + for(int64_t i = 0; i < n; ++i) + { + hy0[static_cast(i)] = static_cast(i + 1); + } + + ASSERT_NO_FATAL_FAILURE( + check_banded_sellmv(handle, rocsparse_operation_transpose, m, n, 32, 2, 2.0f, 3.0f, hy0)); +} From b14d24b7f0a090001de7eb47b4795d6da3ac474e Mon Sep 17 00:00:00 2001 From: kliegeois Date: Thu, 29 Oct 2026 15:16:10 +0000 Subject: [PATCH 09/10] test(rocsparse): always build the ELL triangular solve unit tests (ROCM-33114) ROCSPARSE_WITH_ELL_TRSV was retired with the 5.1 feature flags, so the EllSv tests were compiled out and only the skip stub ran. Drop the guards and the stub. --- .../unit_test_internal_level2_ell_sell.cpp | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index 680a4d922ff5..dbe748919287 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -31,11 +31,7 @@ // (library/src/level2/rocsparse_sptrsv.cpp). clients/tests/test_sptrsv_ell.yaml // only drives it with transA none; this file adds a tiny device-level // check of the numeric solution, plus a unit-diagonal case and an -// explicit-zero-pivot case. Gated the same way the library gates ELL -// TRSV support (BUILD_WITH_ELL_TRSV / ROCSPARSE_WITH_ELL_TRSV): when that -// symbol is not defined the test is skipped with a clear message instead -// of being compiled out, so it still shows up (as skipped) in a -// BUILD_WITH_ELL_TRSV=OFF build. +// explicit-zero-pivot case. // // - Sliced ELL SpMV (sellmv), reached through rocsparse_v2_spmv for // rocsparse_format_sell (library/src/level2/rocsparse_sellmv_impl.cpp, @@ -80,11 +76,9 @@ namespace return c; } -// =========================================================================== -// ellsv : ELL triangular solve via the generic rocsparse_sptrsv descriptor. -// Only built / exercised when the library is built with ELL TRSV support. -// =========================================================================== -#ifdef ROCSPARSE_WITH_ELL_TRSV + // =========================================================================== + // ellsv : ELL triangular solve via the generic rocsparse_sptrsv descriptor. + // =========================================================================== // Host reference for a tiny lower-triangular ELL solve, matching the // semantics of rocsparse::ellsv_device (library/src/level2/ellsv_device.h): @@ -273,10 +267,8 @@ namespace EXPECT_EQ(rocsparse_destroy_sptrsv_descr(descr), rocsparse_status_success); } -#endif // ROCSPARSE_WITH_ELL_TRSV - // =========================================================================== - // sellmv : Sliced ELL SpMV helpers, used regardless of ROCSPARSE_WITH_ELL_TRSV. + // sellmv : Sliced ELL SpMV helpers. // =========================================================================== // Host reference for a sliced-ELL SpMV: y := alpha * op(A) * x + beta * y. @@ -640,8 +632,6 @@ class EllSv : public HandleTest { }; -#ifdef ROCSPARSE_WITH_ELL_TRSV - // Lower triangular, non-unit diagonal, no pivot. 3x3: // [2 ] [y0] [4 ] // [1 4 ] * [y1] = [12] @@ -857,18 +847,6 @@ TEST_F(EllSv, zero_pivot) EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } -#else // !ROCSPARSE_WITH_ELL_TRSV - -TEST_F(EllSv, skipped_ell_trsv_not_built) -{ - GTEST_SKIP() - << "ELL triangular solve support not built " - "(BUILD_WITH_ELL_TRSV=OFF / ROCSPARSE_WITH_ELL_TRSV not defined); " - "rocsparse_sptrsv on rocsparse_format_ell returns rocsparse_status_not_implemented."; -} - -#endif // ROCSPARSE_WITH_ELL_TRSV - // =========================================================================== // sellmv : Sliced ELL SpMV (rocsparse_v2_spmv on rocsparse_format_sell). // =========================================================================== From 28a155dd6e70b9cf993660999b92a3c4fe4b241b Mon Sep 17 00:00:00 2001 From: kliegeois Date: Wed, 4 Nov 2026 02:27:22 +0000 Subject: [PATCH 10/10] test(rocsparse): destroy ELL/SELL test descriptors on every exit (ROCM-33114) run_ellsv created its sptrsv descriptor up front and destroyed it only on the last line, so any failed ASSERT_* in between returned early and leaked it. run_v2_spmv_sell and the EllSv test bodies had the same pattern for their spmat, dnvec and spmv descriptors. Hold them in the level2 RAII guards from unit_test_device_level2_common.hpp (sptrsv_descr_guard, spmv_descr_guard, spmat_guard, dnvec_guard), which the other level-2 unit tests already use. --- .../unit_test_internal_level2_ell_sell.cpp | 69 +++++++------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp index dbe748919287..a25faf04c2f8 100644 --- a/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp +++ b/projects/rocsparse/clients/unittests/unit_test_internal_level2_ell_sell.cpp @@ -44,7 +44,7 @@ // kernels in both directions on both sides of the 128 boundary, with // partial last slices. // -#include "unit_test_utils.hpp" +#include "unit_test_device_level2_common.hpp" #include "rocsparse.h" @@ -162,8 +162,8 @@ namespace int64_t* analysis_pivot, int64_t* solve_pivot) { - rocsparse_sptrsv_descr descr = nullptr; - ASSERT_EQ(rocsparse_create_sptrsv_descr(&descr), rocsparse_status_success); + level2::sptrsv_descr_guard descr; + ASSERT_TRUE(descr.d); const rocsparse_operation trans = rocsparse_operation_none; const rocsparse_sptrsv_alg alg = rocsparse_sptrsv_alg_default; @@ -263,8 +263,6 @@ namespace ASSERT_EQ(pivot_status, pivot == -1 ? rocsparse_status_success : rocsparse_status_zero_pivot); *solve_pivot = pivot; - - EXPECT_EQ(rocsparse_destroy_sptrsv_descr(descr), rocsparse_status_success); } // =========================================================================== @@ -450,8 +448,8 @@ namespace const int64_t nnz = count_nonneg(col_ind); ASSERT_GT(nnz, 0); - rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_sell_descr(&A, + level2::spmat_guard A; + ASSERT_EQ(rocsparse_create_sell_descr(&A.d, m, n, nnz, @@ -481,14 +479,14 @@ namespace device_vector y{hy}; ASSERT_TRUE(x.ptr && y.ptr); - rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, x_size, x.ptr, rocsparse_datatype_f32_r), + level2::dnvec_guard dx, dy; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx.d, x_size, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, y_size, y.ptr, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy.d, y_size, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - rocsparse_spmv_descr spmv_descr = nullptr; - ASSERT_EQ(rocsparse_create_spmv_descr(&spmv_descr), rocsparse_status_success); + level2::spmv_descr_guard spmv_descr; + ASSERT_TRUE(spmv_descr.d); const rocsparse_spmv_alg alg = rocsparse_spmv_alg_sell; const rocsparse_datatype dt = rocsparse_datatype_f32_r; @@ -568,11 +566,6 @@ namespace { EXPECT_EQ(hy_out[i], y_sentinel) << "write past y at index " << i; } - - EXPECT_EQ(rocsparse_destroy_spmv_descr(spmv_descr), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } // Runs build_banded_sell(m, sell_slice_size, depth) with x[i] = i + 1 through @@ -647,8 +640,8 @@ TEST_F(EllSv, lower_non_unit) device_vector d_val{val}; ASSERT_TRUE(d_col.ptr && d_val.ptr); - rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr(&A, + level2::spmat_guard A; + ASSERT_EQ(rocsparse_create_ell_descr(&A.d, m, n, d_col.ptr, @@ -679,10 +672,10 @@ TEST_F(EllSv, lower_non_unit) device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); - rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + level2::dnvec_guard dx, dy; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx.d, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy.d, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; @@ -695,10 +688,6 @@ TEST_F(EllSv, lower_non_unit) = host_ellsv_lower(m, n, width, col_ind, val, hx, alpha, diag_type, &ref_pivot); EXPECT_EQ(ref_pivot, -1); expect_vectors_eq(expected, to_host(y)); - - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } // Lower triangular, unit diagonal. The stored diagonal entries carry a @@ -716,8 +705,8 @@ TEST_F(EllSv, lower_unit_diagonal) device_vector d_val{val}; ASSERT_TRUE(d_col.ptr && d_val.ptr); - rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr(&A, + level2::spmat_guard A; + ASSERT_EQ(rocsparse_create_ell_descr(&A.d, m, n, d_col.ptr, @@ -748,10 +737,10 @@ TEST_F(EllSv, lower_unit_diagonal) device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); - rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + level2::dnvec_guard dx, dy; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx.d, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy.d, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; @@ -765,10 +754,6 @@ TEST_F(EllSv, lower_unit_diagonal) EXPECT_EQ(ref_pivot, -1); // y = {2, 3, 7.25}: the garbage diagonal values must not leak in. expect_vectors_eq(expected, to_host(y)); - - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } // Lower triangular, non-unit diagonal, explicit zero on the diagonal of row 1 @@ -790,8 +775,8 @@ TEST_F(EllSv, zero_pivot) device_vector d_val{val}; ASSERT_TRUE(d_col.ptr && d_val.ptr); - rocsparse_spmat_descr A = nullptr; - ASSERT_EQ(rocsparse_create_ell_descr(&A, + level2::spmat_guard A; + ASSERT_EQ(rocsparse_create_ell_descr(&A.d, m, n, d_col.ptr, @@ -822,10 +807,10 @@ TEST_F(EllSv, zero_pivot) device_vector y{std::vector(static_cast(m), 0.0f)}; ASSERT_TRUE(x.ptr && y.ptr); - rocsparse_dnvec_descr dx = nullptr, dy = nullptr; - ASSERT_EQ(rocsparse_create_dnvec_descr(&dx, n, x.ptr, rocsparse_datatype_f32_r), + level2::dnvec_guard dx, dy; + ASSERT_EQ(rocsparse_create_dnvec_descr(&dx.d, n, x.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); - ASSERT_EQ(rocsparse_create_dnvec_descr(&dy, m, y.ptr, rocsparse_datatype_f32_r), + ASSERT_EQ(rocsparse_create_dnvec_descr(&dy.d, m, y.ptr, rocsparse_datatype_f32_r), rocsparse_status_success); int64_t analysis_pivot = -2, solve_pivot = -2; @@ -841,10 +826,6 @@ TEST_F(EllSv, zero_pivot) EXPECT_EQ(ref_pivot, 1); // y = {2, 8, -1}, using the library's "treat a zero diagonal as 1" rule. expect_vectors_eq(expected, to_host(y)); - - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dx), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_dnvec_descr(dy), rocsparse_status_success); - EXPECT_EQ(rocsparse_destroy_spmat_descr(A), rocsparse_status_success); } // ===========================================================================