Repository navigation
Conversation
|
|
||
| A sparse array can't share the `AbstractGPUArray` supertype — that is a `DenseArray`, | ||
| whereas a sparse array must be an `AbstractSparseArray` — so GPUArrays keeps a parallel | ||
| sparse hierarchy with its own generic functionality. Integrating a back-end has three |
There was a problem hiding this comment.
Weird phrasing here, let me think a bit about how to make this more natural
| | `AbstractGPUSparseMatrixCSR{Tv,Ti}` | `rowPtr`, `colVal`, `nzVal`, `dims`, `nnz` | | ||
| | `AbstractGPUSparseMatrixCOO{Tv,Ti}` | `rowInd`, `colInd`, `nzVal`, `dims`, `nnz` | | ||
|
|
||
| The pointer/index/value arrays are the back-end's own dense vector type. Provide only the |
There was a problem hiding this comment.
Should we give an example (e.g. CuVector for CUDA)
|
|
||
| ### Interface to implement | ||
|
|
||
| * **Constructors** — from component arrays (`MyCSR(rowPtr, colVal, nzVal, dims)`), |
There was a problem hiding this comment.
What about to/from dense arrays?
| JLArray(x::JLSparseVector) = JLArray(collect(SparseVector(x))) | ||
| JLArray(x::JLSparseMatrixCSC) = JLArray(collect(SparseMatrixCSC(x))) | ||
| JLArray(x::JLSparseMatrixCSR) = JLArray(collect(SparseMatrixCSC(x))) | ||
| JLArray(x::JLSparseVector) = GPUArrays.to_dense(x) |
There was a problem hiding this comment.
should we make this an eval block?
| SparseArrays.SparseMatrixCSC(x::AbstractGPUSparseMatrixCSC) = SparseMatrixCSC(size(x)..., Array(SparseArrays.getcolptr(x)), Array(SparseArrays.rowvals(x)), Array(SparseArrays.nonzeros(x))) | ||
|
|
||
| function check_sparse_target(::Type{ST}, ::Type{Tv}, ::Type{Ti}) where {ST,Tv,Ti} | ||
| Tv isa Type && Ti isa Type || |
There was a problem hiding this comment.
This looks really funky to me. Shouldn't Ti also check it's a <: Integer?
|
|
||
| function check_sparse_target(::Type{ST}, ::Type{Tv}, ::Type{Ti}) where {ST,Tv,Ti} | ||
| Tv isa Type && Ti isa Type || | ||
| throw(ArgumentError("sparse target type $ST must specify value and index eltypes")) |
There was a problem hiding this comment.
Should we include Tv and Ti in the error message?
| @kernel function densify_vector_kernel!(out, iPtr, nzVal) | ||
| k = @index(Global, Linear) | ||
| if k <= length(nzVal) | ||
| @inbounds out[iPtr[k]] = nzVal[k] |
There was a problem hiding this comment.
it's funny how Claude made this a one liner here but not above
|
Yeah this hasn't passed my personal sniff test yet, hence draft, but thanks for the review. FWIW, the initial motivation was Metal support + SpMV/SpMM (which happens to be easier to express in COO), with the rest following out of it. The API simplification, replacing *_array_type / dense_array_type / sparse_array_type with something like |
|
Ah I can eff off until it's ready if you prefer, sorry! |
|
I'd love to review this one too once it's ready! |
|
Note to self: add a |
|
Can we resurrect this? |
|
This is exciting!!! |
9a9126f to
76600f3
Compare
|
Hey @maleadt, need any help to push this one over the finish line? It would be very cool indeed to have sparse linalg on a Metal GPU |
|
I'll spend some tokens on another rebase and update, but I won't have the time to push this over the line right now. It's definitely on the short-term TODO list though. |
GPUArrays now implements the sparse formats itself: GPUSparseVector and GPUSparseMatrixCSR/CSC/COO are immutable structs parametrized on the dense vectors that store their indices and values. Generic code allocates results with similar() on the buffers it already has, so the trait hooks (dense_array_type, sparse_array_type, csc_type, csr_type, coo_type, _sptranspose, _spadjoint) are gone, and a back-end needs no sparse-specific code: it can alias the types for its storage and add a transfer policy for its adaptor. The same struct is used on the device, replacing the GPUSparseDevice* types and the per-back-end adapt rules. The formats are canonical, as in SparseArrays (COO in row-major order). Constructors check host metadata only; check_structure verifies the invariants on the device. similar, copy, copyto!, zero and adapt follow SparseArrays and Adapt semantics: copies and conversions never share buffers, and adapting to a storage type keeps the format and types. Conversions between all formats run on the device (generic_regroup for CSR <-> CSC, generic_expand/generic_compress for CSR <-> COO). Broadcasting and reductions are ported to the new types. Broadcasting no longer reuses the input's pointer buffer for its output, and handles matrices without rows or columns. AbstractGPUSparseMatrixCSR/CSC/COO remain as deprecated aliases of the concrete types for downstream dispatch. JLArrays keeps only aliases and constructors on them. The testsuite builds its inputs from dense buffers and covers every format for every back-end. The raw constructors borrow their buffers, like SparseArrays' raw constructors. Operations that change the structure of an array resize its buffers, and check that they can be resized before modifying anything, so arrays backed by views remain usable for reading and for updating values. dataids covers every buffer, so that copyto! and broadcasts unalias their arguments. Scalar indexing reads the buffers, so it requires allowscalar exactly when the storage does.
GPUArrays now implements the sparse formats generically, parametrized on their storage, so Metal only names them for MtlVector storage (MtlSparseVector, MtlSparseMatrixCSR/CSC/COO) and decides what mtl does with host sparse arrays: matrices become CSR, indices Int32 when they fit, and values follow the dense narrowing rules. Structural adapt(MtlArray, S) keeps the format and types without Metal code. This requires GPUArrays 13 (JuliaGPU/GPUArrays.jl#745).
4cb19d3 to
e2eb984
Compare
Dense GPU arrays convert to every sparse format on the device, through findall in column-major order (or on the dense transpose for row-major formats), and sparse(A; fmt) selects the format like CUDA.jl and AMDGPU.jl do. copy(transpose(A)), copy(adjoint(A)) and permutedims produce the same format by regrouping, and the format constructors accept transposed operands. findnz returns the coordinates in column-major order like SparseArrays, for every format.
sparse(I, J, V, m, n, combine; fmt) and sparsevec(I, V, n, combine) on GPU vectors sort packed coordinate keys, combine the values of repeated coordinates with a per-run fold (reduce_by_key), and compress. As in SparseArrays, repeated entries are folded in input order, with + by default (| for Bool) and explicit zeros kept, so that a call gives the same result on every back-end. Because the device sort need not be stable, a second sort on keys that are unique per entry restores the input order, unless the order cannot change the result (+ and * on integers, max, min, |, &). spdiagm builds on the same path.
The sparse redesign removes the back-end hooks and changes the sparse types, which is breaking for back-ends. JLArrays requires the new version; its own breaking release (0.5, for KernelAbstractions 0.10) is not registered yet and covers the change of its sparse types.
Products hook into LinearAlgebra's storage-level mul!(C, tA, [tB,] A, B, α, β), which Julia 1.13 calls directly and which older versions reach through the forwarded generic_matvecmul!/generic_matmatmul!, like the dense products do. The generic implementations (generic_spmv!, generic_spmm!) gather over the CSR layout of op(A), or the CSC layout of op(B) for dense × sparse, so they are deterministic and work for every element type; Float16 accumulates in Float32. Transposed operands regroup the matrix first. Symmetric and Hermitian operands, which the old code treated as plain matrices, are computed as two passes over the used triangle and its transpose. LinearAlgebra allocates the destination of dense × sparse like the sparse operand, so * allocates a dense result for those.
The broadcast kernels iterate one compressed format, so arguments of other formats are converted to that of the first sparse matrix, COO matrices being read as CSR (which has the same order of entries) and a COO result converted back. map over sparse arrays is a broadcast, as in SparseArrays. SparseArrays implements +, - and map/broadcast of the basic operators for any AbstractSparseVector through scalar loops; GPUSparseVector gets its own methods.
fkeep!, dropzeros[!], droptol!, triu and tril filter the stored entries on the device and recompress; istriu, istril and isdiag check the nonzero values, and issymmetric/ishermitian compare with the transpose, which also accounts for stored zeros on one side. diag looks up each element with a binary search in its slice, reshape and vec reuse the column-major order of CSC, which reshaping preserves, and kron fills each column of the result from a pair of columns without sorting. The istriu/istril workaround for triangular wrappers of sparse matrices (JuliaLang/julia#55547) is carried over from the old code.
A[I, J] with ranges, colons and integers (giving a sparse vector for a row or column) and x[I] for vectors keep the stored entries in the selection and renumber them on the device. Ranges with a positive step preserve the order of the entries, so the result needs no sorting; other index vectors are rejected. The vector methods replace SparseArrays' own, which index through scalar loops.
dot between sparse vectors multiplies them with a broadcast, dot with a dense vector gathers its elements at the stored indices, and dot(x, A, y) is dot(x, A * y). count, any and all combine the stored values with the implicit zeros. float, complex, real and imag convert the stored values, sparse(A; fmt) and sparsevec(A) convert between sparse formats, and reductions of vectors support dims=1. Several of these replace SparseArrays' methods for any sparse vector, which loop over the elements on the host (Vector(x), imag).
generic_spgemm computes A * B for any pair of formats by expansion, sorting and compression: every product of a stored entry of A with one of B is materialized, each thread finding its entry of A by binary search over the running count of products, and the products are summed per coordinate through the assembly path, which keeps the result deterministic. The memory needed grows with the number of products. The sparse-output mul!(C, A, B, α, β) gives C the union of the structures of α*A*B and β*C, resizing its buffers, rather than assuming that an equal number of stored entries means an equal structure. exp is ported from the old code on top of these, with a fix for matrices whose norm is below one, which it scaled up instead of down.
…enation SparseArrays implements these for any AbstractSparseArray with scalar loops, which a GPU sparse array cannot run. == compares values, so stored zeros do not matter. Broadcasting into a sparse array materializes the result and copies it over, so the destination takes the structure of the result. Arithmetic with transposed, Symmetric and Hermitian wrappers, UniformScaling and Diagonal materializes the wrapper and combines the operands on the device, and hcat/vcat of sparse arrays offset the coordinates and reassemble them. blockdiag throws instead of falling back to scalar indexing.
|
@gdalle May be worth testing this branch with your favorite back-end (ideally one that doesn't come with sparse array abstractions out of the box, e.g., Metal.jl or so) and test whether the functionality covers what you need. |
|
Thanks! I'll take a look |
GPUArrays now implements sparse arrays itself. A back-end gets them by naming its storage. Before, GPUArrays only defined abstract types and hooks that every back-end had to implement.
The same code works on
MtlArray,ROCArray,oneArray,CLArrayandJLArray, all running on the device.Design
GPUSparseVectorandGPUSparseMatrixCSR/CSC/COOare concrete structs whose fields are the back-end's dense vectors (GPUSparseMatrixCSR{Tv,Ti,Vi,Vv}). The same struct is passed to kernels, so there are no separate device types. Every operation has a portable KernelAbstractions implementation. A back-end names the types for its storage:To use a vendor library, it overrides LinearAlgebra's storage-level
mul!for those aliases and falls back toGPUArrays.generic_spmv!/generic_spmm!/generic_spgemm, as CUDA.jl already does. The interface docs cover the contract, the supported operations, where semantics differ from SparseArrays, and migration.Supported: products with dense and sparse arrays (including transposes,
Symmetric,Hermitian,DiagonalandI), broadcasting andmap, reductions,norm/opnorm/dot,==, indexing,hcat/vcat,kron,exp,triu/tril/diag/tr,dropzeros!/fkeep!,reshape, conversions between all formats and to and from dense arrays, andsparse/sparsevec/spdiagmassembly. Not implemented:hash,blockdiag, and triangular solves without a vendor library.Breaking
AbstractGPUSparseMatrixCSR/CSC/COOremain as deprecated aliases of the concrete types, so dispatch on them keeps working.dense_array_type,sparse_array_type,csc_type,csr_type,coo_type,_sptranspose,_spadjoint) and theGPUSparseDevice*types are removed.None of the back-ends needs a breaking release: they keep their constructors' defaults (e.g.
Cintindices on CUDA.jl and AMDGPU.jl) and keep old field names as deprecated properties.Back-end PRs
Each branch ends with a
TEMPcommit that points CI at this branch.tb/sparse-v13tb/sparse-v13tb/sparse-v13tb/sparse-v13Validated on an RTX 5080, an AMD iGPU (gfx1036), an Intel Arc A770, an M1 and an M3 Pro, and with PoCL: each back-end's own sparse tests plus GPUArrays' shared sparse testsuite on its arrays.
Fixes #648, #655 and #677. Supersedes #684 and JuliaGPU/Metal.jl#909.
Tested here:
JLArray/sparse(14,730 tests) and a check that the sparse code adds no method ambiguities, on Julia 1.10 and 1.13.