swiftsparse-matrixaccelerate-framework

Accelerate’s SparseMultiply randomly crashes with EXC_BAD_ACCESS


I am using SparseMultiply of a SparseMatrix_Double and a DenseVector_Double, using the Accelerate framework’s Sparse Solvers, and it is randomly crashing:

enter image description here

Sometimes, it works. Sometimes, it crashes with EXC_BAD_ACCESS. Sometimes, it returns with incorrect results.

I’ve tried turning on various “address sanitizer” and/or various memory-checking options on the “Diagnostics” tab of my scheme settings, and they do not report any issues.

What is going on?

func matrixProductExperiment() {
    // Given sparse matrix A, and dense vector X, calculate product of dense vector Y, i.e., y = Ax:
    //
    //              A                X    =      Y
    //   ( 10.0  1.0      2.5 )  ( 2.20 ) = ( 32.025 )
    //   (  1.0 12.0 -0.3 1.1 )  ( 2.85 ) = ( 38.720 )
    //   (      -0.3  9.5     )  ( 2.79 ) = ( 25.650 )
    //   (  2.5  1.1      6.0 )  ( 2.87 ) = ( 25.855 )

    // We use a format known as Compressed Sparse Column (CSC) to store the data. Further,
    // as the matrix is symmetric, we only need to store half the data. The CSC format
    // stores the matrix as a series of column vectors where only the non-zero entries are
    // specified, stored as the pair of (row index, value), although in separate arrays:

    let rowCount: Int32     = 4
    let columnCount: Int32  = 4
    var matrixValues        = [ 10.0, 1.0, 2.5, 12.0, -0.3, 1.1, 9.5, 6.0 ]
    var rowIndices: [Int32] = [    0,   1,   3,    1,    2,   3,   2,   3 ]
    var columnStarts        = [    0,              3,              6,   7 ]

    // vector X

    var xValues = [ 2.20, 2.85, 2.79, 2.87 ]

    // In this library, this raw information is all wrapper into a flexible data type
    // that allows for more complex use cases in other situations.

    rowIndices.withUnsafeMutableBufferPointer { rowIndicesPointer in
        columnStarts.withUnsafeMutableBufferPointer { columnStartsPointer in
            matrixValues.withUnsafeMutableBufferPointer { valuesPointer in
                xValues.withUnsafeMutableBufferPointer { xPointer in
                    let a = SparseMatrix_Double(
                        // Structure of the matrix, without any values
                        structure: SparseMatrixStructure(
                            rowCount:     rowCount,
                            columnCount:  columnCount,
                            columnStarts: columnStartsPointer.baseAddress!,
                            rowIndices:   rowIndicesPointer.baseAddress!,
                            // Matrix meta-data
                            attributes: SparseAttributes_t(
                                transpose: false,
                                triangle: SparseLowerTriangle,
                                kind: SparseSymmetric,
                                _reserved: 0,
                                _allocatedBySparse: false
                            ),
                            blockSize: 1),
                        // Numerical values of the matrix
                        data: valuesPointer.baseAddress!
                    )

                    let x = DenseVector_Double(count: columnCount, data: xPointer.baseAddress!)

                    let y = [Double](unsafeUninitializedCapacity: Int(rowCount)) { resultBuffer, count in
                        let y = DenseVector_Double(count: rowCount, data: resultBuffer.baseAddress!)
                        SparseMultiply(a, x, y)
                        count = Int(rowCount)
                    }

                    print(y) // [32.025, 38.72, 25.65, 25.855] – Correct
                }
            }
        }
    }
}

Solution

  • The problem is columnStarts. This array must include, after all the actual “column starts”, the number of elements in the array of matrix values (so it can calculate how many elements are in that last column). Note the extra value at the end of the columnStarts array:

    func matrixProductExperiment() {
        …
    
        let rowCount: Int32     = 4
        let columnCount: Int32  = 4
        var matrixValues        = [ 10.0, 1.0, 2.5, 12.0, -0.3, 1.1, 9.5, 6.0 ]
        var rowIndices: [Int32] = [    0,   1,   3,    1,    2,   3,   2,   3 ]
        var columnStarts        = [    0,              3,              6,   7,   8 ]
        assert(columnStarts.last == matrixValues.count, "\(String(describing: columnStarts.last)) ≠ \(matrixValues.count)")
    
        …
    }
    

    If you use SparseConvertFromCoordinate, as shown in the SparseMultiply documentation, you supply the blockCount. But the SparseMatrixStructure initializer has no “block count” parameter, and it extracts this (somewhat unobviously, IMHO) from the last value in columnStarts.

    See Creating Sparse Matrices, which says:

    This array requires an additional, final entry that defines the final column's length.

    To my eye, it was not entirely intuitive that the “starts” array must end with the “count” of the matrix values. And, annoyingly, the SparseMatrixStructure documentation neglects to point this out. But, in retrospect, we can see why we must.

    That is a few hours of my life that I will never get back. The solution may be obvious to many, but for those stumbling with random crashes with the sparse matrices in Accelerate, take a hard look at the columnStarts array. This value does not enjoy meaningful validation and/or error messages. One will receive intermittent/cryptic errors if the count is omitted.

    It was a stupid mistake on my part, but tracking down the problem took a lot of work. Hopefully, this will save some future readers a few hours of their time.