Peculiar EXC_BAD_ACCESS, involving sparse matrices

Helo all,

Currently, I'm working on an iOS app that performs measurement and shows the results to the user in a graph. I use a Savitzky-Golay filter to filter out noise, so that the graph is nice and smooth. However, the code that calculates the Savitzky-Golay coefficients using sparse matrices crashes sometimes, throwing an EXC_BAD_ACCESS. I tried to find out what the problem is by turning on Address Sanitizer and Thread Sanitizer, but, for some reason, the bad access exception isn't thrown when either of these is on. What else could I try to trace back the problem?

Thanks in advance, CaS

To reproduce the error, run the following:

import SwiftUI
import Accelerate

struct ContentView: View {
    var body: some View {
        VStack {
            Button("Try", action: test)
        }
        .padding()
    }
    
    func test() {
        for windowLength in 3...100 {
            let coeffs = SavitzkyGolay.coefficients(windowLength: windowLength, polynomialOrder: 2)
            print(coeffs)
        }
    }
}

class SavitzkyGolay {
    static func coefficients(windowLength: Int, polynomialOrder: Int, derivativeOrder: Int = 0, delta: Int = 1) -> [Double] {
        let (halfWindow, remainder) = windowLength.quotientAndRemainder(dividingBy: 2)
        var pos = Double(halfWindow)
        
        if remainder == 0 {
            pos -= 0.5
        }
        
        let X = [Double](stride(from: Double(windowLength) - pos - 1, through: -pos, by: -1))
        let P = [Double](stride(from: 0, through: Double(polynomialOrder), by: 1))
        let A = P.map { exponent in
            X.map {
                pow($0, exponent)
            }
        }
        
        var B = [Double](repeating: 0, count: polynomialOrder + 1)
        B[derivativeOrder] = Double(factorial(derivativeOrder)) / pow(Double(delta), Double(derivativeOrder))
        
        return leastSquaresSolution(A: A, B: B)
    }

    static func leastSquaresSolution(A: [[Double]], B: [Double]) -> [Double] {
        let sparseA = A.sparseMatrix()
        var sparseAValuesCopy = sparseA.values
        var xValues = [Double](repeating: 0, count: A.transpose().count)
        var bValues = B
        
        sparseAValuesCopy.withUnsafeMutableBufferPointer { valuesPtr in
            let a = SparseMatrix_Double(
                structure: sparseA.structure,
                data: valuesPtr.baseAddress!
            )
            
            bValues.withUnsafeMutableBufferPointer { bPtr in
                xValues.withUnsafeMutableBufferPointer { xPtr in
                    let b = DenseVector_Double(
                        count: Int32(B.count),
                        data: bPtr.baseAddress!
                    )
                    
                    let x = DenseVector_Double(
                        count: Int32(A.transpose().count),
                        data: xPtr.baseAddress!
                    )
                    
                    #warning("EXC_BAD_ACCESS is thrown below")
                    print("This code is executed...")
                    let status = SparseSolve(SparseLSMR(), a, b, x, SparsePreconditionerDiagScaling)
                    print("...but, if an EXC_BAD_ACCESS is thrown, this code isn't")
                    
                    if status != SparseIterativeConverged {
                        fatalError("Failed to converge. Returned with error \(status).")
                    }
                }
            }
        }
        
        return xValues
    }
}

func factorial(_ n: Int) -> Int {
    n < 2 ? 1 : n * factorial(n - 1)
}

extension Array where Element == [Double] {
    func sparseMatrix() -> (structure: SparseMatrixStructure, values: [Double]) {
        let columns = self.transpose()
        var rowIndices: [Int32] = columns.map { column in
            column.indices.compactMap { indexInColumn in
                if column[indexInColumn] != 0 {
                    return Int32(indexInColumn)
                }
                return nil
            }
        }.reduce([], +)
        
        let sparseColumns = columns.map { column in
            column.compactMap {
                if $0 != 0 {
                    return $0
                }
                return nil
            }
        }
        var counter = 0
        var columnStarts = [Int]()
        for sparseColumn in sparseColumns {
            columnStarts.append(counter)
            counter += sparseColumn.count
        }
        let reducedSparseColumns = sparseColumns.reduce([], +)
        columnStarts.append(reducedSparseColumns.count)
        
        let structure: SparseMatrixStructure = rowIndices.withUnsafeMutableBufferPointer { rowIndicesPtr in
            columnStarts.withUnsafeMutableBufferPointer { columnStartsPtr in
                let attributes = SparseAttributes_t()
                
                return SparseMatrixStructure(
                    rowCount: Int32(self.count),
                    columnCount: Int32(columns.count),
                    columnStarts: columnStartsPtr.baseAddress!,
                    rowIndices: rowIndicesPtr.baseAddress!,
                    attributes: attributes,
                    blockSize: 1
                )
            }
        }
        
        return (structure, reducedSparseColumns)
    }
    
    func transpose() -> Self {
        let columns = self.count
        let rows = self.reduce(0) { Swift.max($0, $1.count) }

        return (0 ..< rows).reduce(into: []) { result, row in
            result.append((0 ..< columns).reduce(into: []) { result, column in
                result.append(row < self[column].count ? self[column][row] : 0)
            })
        }
    }
}
Answered by CalciumSulfide in 793844022

Found an easier solution: using LAPACK's dgels function. Thanks for your help anyway, @Claude31 !

Things get even more interesting, as I just found out that the leastSquaresSolution function always returns an array of zero's, instead of the values it should return, while it does actually return the right values in my real code...

Welcome to the forum.

This code may be readable for you, but without any comment, it is a headache, at least for me, to really understand … So I may miss something in your code.

If I understand what you are doing in

    static func leastSquaresSolution(A: [[Double]], B: [Double]) -> [Double] {

you are computing and return xValues

This should be done by manipulating xPtr. Correct ?

 xValues.withUnsafeMutableBufferPointer { xPtr in

xValues is initialised with 0.0 values.

Where do you update xPtr, which would modify xValues before you return it ?

Hello Claude,

First of all, I am sorry about the absence of comments. It would be a headache for me too if I had not written the code myself, so I should have thought about adding comments earlier... I will add them and send the code again soon.

The xValues form the vector x in the equation Ax = B, where A and B are matrices that are the arguments of the leastSquaresSolution function. Apple describes how the Ax = B equation can be solved with Swift in this article. xValues is indeed modified by updating xPtr. xPtr's baseAddress is passed to the DenseVector_Double that holds x:

                let x = DenseVector_Double(
                    count: Int32(A.transpose().count),
                    data: xPtr.baseAddress!
                )

Then, x is passed to the constant 'status', which should modify x and thereby should modify xPtr and xValues: let status = SparseSolve(SparseLSMR(), a, b, x, SparsePreconditionerDiagScaling)

The SavitzkyGolay class with comments:

class SavitzkyGolay {
    static func coefficients(windowLength: Int, polynomialOrder: Int, derivativeOrder: Int = 0, delta: Int = 1) -> [Double] {
        // windowLength is the number of coefficients returned by this function.
        guard windowLength > 0 else {
            fatalError("windowLength must be positive")
        }
        // polynomialOrder is the order of the polynomial used to smooth the noisy data (the coefficients are calculated independently from the noisy data)
        guard polynomialOrder < windowLength else {
            fatalError("polynomialOrder must be less than windowLength")
        }
        // The derivativeOrder is the order of the derivative to compute. If it is set to zero, the noisy data is smoothed without differentiating.
        guard derivativeOrder <= polynomialOrder else {
            return [Double](repeating: 0, count: windowLength)
        }
        
        let (halfWindow, remainder) = windowLength.quotientAndRemainder(dividingBy: 2)
        var pos = Double(halfWindow)
        
        // pos should not be a round number (because otherwise this function won't work appropriately), so if windowLength is even, which means that halfWindow and thereby pos are round numbers, subtract 0.5 from pos.
        if remainder == 0 {
            pos -= 0.5
        }
        
        // Form the matrix A. The columns of A are powers of the integers from -pos to windowLength - pos - 1. The powers (i.e., rows) range from 0 to polynomialOrder.
        let V = [Double](stride(from: Double(windowLength) - pos - 1, through: -pos, by: -1))
        let P = [Double](stride(from: 0, through: Double(polynomialOrder), by: 1))
        let A = P.map { exponent in
            V.map {
                pow($0, exponent)
            }
        }
        
        // Form the vector B. It determines which order derivative is returned.
        var B = [Double](repeating: 0, count: polynomialOrder + 1)
        // The value assigned to B[derivativeOrder] scales the result to take into account the order of the derivative and the distance between two values of the noisy data.
        B[derivativeOrder] = Double(factorial(derivativeOrder)) / pow(Double(delta), Double(derivativeOrder))
        
        // Return the least-squares solution of Ax = B
        return leastSquaresSolution(A: A, B: B)
    }
}

leastSquaresSolution with comments:

func leastSquaresSolution(A: [[Double]], B: [Double]) -> [Double] {
    // Get the sparse matrix of A. A sparse matrix is a matrix that omits all zero's. sparseMatrix() returns an array of doubles, omitting all zero's, together with information about where in the array the columns start and what the indices of the rows are (for more information, take a look at the sparseMatrix() function).
    let sparseA = A.sparseMatrix()
    
    // Copy some constants and save them in variables, so that they can be pointed to using withUnsafeMutableBufferPointer.
    var sparseAValuesCopy = sparseA.values
    var bValues = B
    
    // The vector X returned by this function has as many values as A has columns. In other words, X has as many values as A's transpose has rows. So X should be initialised to have that amount of values.
    var xValues = [Double](repeating: 0, count: A.transpose().count)
    
    sparseAValuesCopy.withUnsafeMutableBufferPointer { valuesPtr in
        // Construct the matrix A in Ax = B
        let a = SparseMatrix_Double(
            structure: sparseA.structure,
            data: valuesPtr.baseAddress!
        )
        
        bValues.withUnsafeMutableBufferPointer { bPtr in
            xValues.withUnsafeMutableBufferPointer { xPtr in
                // Construct the matrices B and x in Ax = B
                let b = DenseVector_Double(
                    count: Int32(B.count),
                    data: bPtr.baseAddress!
                )
                let x = DenseVector_Double(
                    count: Int32(A.transpose().count),
                    data: xPtr.baseAddress!
                )
                
                // Try to find X with SparseSolve (this is where it seems to go wrong)
                let status = SparseSolve(SparseLSMR(), a, b, x, SparsePreconditionerDiagScaling)
                if status != SparseIterativeConverged {
                    fatalError("Failed to converge. Returned with error \(status).")
                }
            }
        }
    }
    
    // Should return x in Ax = B
    return xValues
}

the sparseMatrix function with comments:

extension Array where Element == [Double] {
    // A sparse matrix is a mattrix where all zero's are ommitted.
    //   Normal matrix:                  Sparse matrix:
    //      0  1  0                            1
    //      1  3  0                         1  3
    //      4  0  2                         4     2
    // Find the sparse matrix of this matrix. Returns an array of doubles containing the values of the sparse matrix (omitting all zero's) and a SparseMatrixStructure containing information about where the columns start and where the rows start. The values array of the sparse matrix above would be [1, 4, 1, 3, 2].
    func sparseMatrix() -> (structure: SparseMatrixStructure, values: [Double]) {
        let columns = self.transpose()
        // Get the row indices of the matrix. The row indices of the sparse matrix above would be
        // [1, 2                                   column 0
        // 0, 1,                                   column 1
        // 2]                                      column 2
        var rowIndices: [Int32] = columns.map { column in
            column.indices.compactMap { indexInColumn in
                if column[indexInColumn] != 0 {
                    return Int32(indexInColumn)
                }
                return nil
            }
        }.reduce([], +)
        
        // Get the column starts. For the sparse matrix above, the column starts would be
        // [1,                                     column 0
        // 3,                                      column 1
        // 5,                                      column 2
        // 5]                                      the total amount of values in the sparse matrix
        let sparseColumns = columns.map { column in
            column.compactMap {
                if $0 != 0 {
                    return $0
                }
                return nil
            }
        }
        var counter = 0
        var columnStarts = [Int]()
        for sparseColumn in sparseColumns {
            columnStarts.append(counter)
            counter += sparseColumn.count
        }
        // Get the reducedSparseColumns (the `values`). For the matrix above, these would be [1, 4, 1, 3, 2].
        let reducedSparseColumns = sparseColumns.reduce([], +)
        columnStarts.append(reducedSparseColumns.count)
        
        // Wrap the information about the sparse matrix in a SparseMatrixStructure
        let structure: SparseMatrixStructure = rowIndices.withUnsafeMutableBufferPointer { rowIndicesPtr in
            columnStarts.withUnsafeMutableBufferPointer { columnStartsPtr in
                let attributes = SparseAttributes_t()
                
                return SparseMatrixStructure(
                    rowCount: Int32(self.count),
                    columnCount: Int32(columns.count),
                    columnStarts: columnStartsPtr.baseAddress!,
                    rowIndices: rowIndicesPtr.baseAddress!,
                    attributes: attributes,
                    blockSize: 1
                )
            }
        }
        
        // Return the structure and the values of the sparse matrix.
        return (structure, reducedSparseColumns)
    }
    
    // swaps rows and columns
    func transpose() -> Self {
        let columns = self.count
        let rows = self.reduce(0) { Swift.max($0, $1.count) }

        return (0 ..< rows).reduce(into: []) { result, row in
            result.append((0 ..< columns).reduce(into: []) { result, column in
                result.append(row < self[column].count ? self[column][row] : 0)
            })
        }
    }
}

Much better with the comments.

However, I still do not catch where xValues is modified (initialised as zeros).

It should be in this part, correct ?

        bValues.withUnsafeMutableBufferPointer { bPtr in
            xValues.withUnsafeMutableBufferPointer { xPtr in
                // Construct the matrices B and x in Ax = B
                let b = DenseVector_Double(
                    count: Int32(B.count),
                    data: bPtr.baseAddress!
                )
                let x = DenseVector_Double(
                    count: Int32(A.transpose().count),
                    data: xPtr.baseAddress!
                )
                
                // Try to find X with SparseSolve (this is where it seems to go wrong)
                let status = SparseSolve(SparseLSMR(), a, b, x, SparsePreconditionerDiagScaling)
                if status != SparseIterativeConverged {
                    fatalError("Failed to converge. Returned with error \(status).")
                }
            }
        }

through the change of xPtr. Correct ?

But where is xPtr modified ?

Thank you for your reaction!

The code you mentioned is derived from this article, where the xPtr isn’t changed manually. If I understood it correctly, the SparseSolve function changes x, which in turn changes xPtr and xValues.

The article does not give complete code, so you probably had to complete for your own code.

If I understood it correctly, the SparseSolve function changes x, which in turn changes xPtr and xValues.

x is passed as a parameter (and const) to the function, so it is not modified by SparseSolve.

To be clear, I did not analyse nor understand the details of how SparseSolve works, but I looked at the doc for SparseSolve (however I did not find one that returns a status), and it seems to be a bit different:

let xValues = [Double](unsafeUninitializedCapacity: n) {
    buffer, count in
    
    bValues.withUnsafeMutableBufferPointer { bPtr in
        
        let b = DenseVector_Double(count: 3,
                                   data: bPtr.baseAddress!)
        let x = DenseVector_Double(count: 3,
                                   data: buffer.baseAddress!)
        
        SparseSolve(factorization, b, x)
        
        count = n
    }
}
On return, xValues contains the values [1.0, 2.0, 3.0].

Here, xValues is changed in the completion handler.

Could you try to replicate this ?

Running that code works fine with the matrix they used in the example, but as soon as I try it out with other matrices, the function crashes with this error: 'Matrix is structurally singular'. Which, according to Google, means that the matrix is not invertible. So seemingly QR-factorization (the factorization used in the example) only works with matrices that are invertible. I tried out the other methods of factorization, but they all have their limitations; none of them seem suitable for what I'm trying to achieve.

However, the other leastSquaresSolution-method that I initially wrote also returns zero's if I initialize it with nan's instead of zero's. So xPtr must somehow be modified by the SparseSolve function (although I also have no idea how that's possible). Moreover, exactly the same code (literally copy-pasted) returns the right values when I call it in my real project, except that it sometimes throws an EXC_BAD_ACCESS. This stuff is far more complicated than I had expected it to be :)

as soon as I try it out with other matrices, the function crashes with this error: 'Matrix is structurally singular'.

Could you show examples of Matrix that cause crash ? It would be surprising that any other matrix be singular. Unless it is not the original matrix but some derived one.

 

However, the other leastSquaresSolution-method that I initially wrote also returns zero's

Did you post this method, to see the difference ?

Accepted Answer

Found an easier solution: using LAPACK's dgels function. Thanks for your help anyway, @Claude31 !

Peculiar EXC_BAD_ACCESS, involving sparse matrices
 
 
Q