Skip to content

Generic GPU sparse arrays: storage-parametric formats owned by GPUArrays - #745

Draft
maleadt wants to merge 69 commits into
mainfrom
tb/sparse
Draft

maleadt wants to merge 69 commits into
mainfrom
tb/sparse

Conversation

@maleadt

@maleadt maleadt commented Jun 23, 2026 •

Copy link
Copy Markdown
Member

Breaking v13 redesign: GPUArrays owns immutable sparse vectors, CSR, CSC and row-major
COO, portable operations, high-level vendor integration and algorithm selection.
Backends supply storage/runtime and native executors, so future portable operations
and algorithm choices stay central.

Compressed pointers and coordinates have separate storage/index widths. Raw
constructors borrow; structural mutation is preflighted. Duplicate combination
preserves order; custom combiners must be associative but need not commute.
See the interface contract for the API and unsupported cases.

Coordinated CUDA, AMDGPU, oneAPI, Metal and OpenCL branches preserve vendor solves,
factorizations, BSR and batching where supported. oneAPI has explicit handle,
queue, buffer and prepared-operation lifetimes.
Metal integration is in JuliaGPU/Metal.jl#982.

The handoff records exact tested revisions and limits;
the performance report records timings and temporary memory. Metal was spot-tested
only. NVIDIA dense-allocation churn also reproduces on the v12 baseline and remains
unresolved.

Not release-ready: final coordinated CI, oneAPI artifact audit/publication, compatible
releases and registered resolution remain gates. The single TEMP CI commit pins the
unreleased stack and explicitly develops dependencies on Julia 1.10.

Comment thread docs/src/interface.md Outdated

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird phrasing here, let me think a bit about how to make this more natural

Comment thread docs/src/interface.md Outdated
| `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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we give an example (e.g. CuVector for CUDA)

Comment thread docs/src/interface.md Outdated

### Interface to implement

* **Constructors** — from component arrays (`MyCSR(rowPtr, colVal, nzVal, dims)`),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about to/from dense arrays?

Comment thread lib/JLArrays/src/JLArrays.jl Outdated
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make this an eval block?

Comment thread src/host/sparse.jl Outdated
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 ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really funky to me. Shouldn't Ti also check it's a <: Integer?

Comment thread src/host/sparse.jl Outdated

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"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include Tv and Ti in the error message?

Comment thread src/host/sparse.jl Outdated
@kernel function densify_vector_kernel!(out, iPtr, nzVal)
k = @index(Global, Linear)
if k <= length(nzVal)
@inbounds out[iPtr[k]] = nzVal[k]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's funny how Claude made this a one liner here but not above

@maleadt

maleadt commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

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 similar feels like a genuine simplification though. (tried to get rid of the remaining csc_type/csr_type/... too, but that proved too hard)

@kshyatt

kshyatt commented Jun 23, 2026

Copy link
Copy Markdown
Member

Ah I can eff off until it's ready if you prefer, sorry!

@gdalle

gdalle commented Jun 24, 2026

Copy link
Copy Markdown

I'd love to review this one too once it's ready!

@maleadt

maleadt commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Note to self: add a findnz regression test here to catch issues like JuliaGPU/CUDA.jl#3189

@kshyatt

kshyatt commented Aug 14, 2026

Copy link
Copy Markdown
Member

Can we resurrect this?

@maleadt
maleadt changed the base branch from main to tb/permutedims-empty September 25, 2026 18:23
@maleadt
maleadt added this pull request to stack #797 September 25, 2026 18:23
@maleadt maleadt changed the title Generic sparse arrays: CSC/CSR/COO formats, on-device conversions, SpMM/SpMV Generic GPU sparse arrays: storage-parametric formats owned by GPUArrays Sep 25, 2026
@gdalle

gdalle commented Sep 29, 2026

Copy link
Copy Markdown

This is exciting!!!

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, constructors on them, and host
versions of sortperm, accumulate! and findall. The testsuite builds its
inputs from dense buffers and covers every format for every back-end.
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 and for JLArrays' sparse types.
@maleadt
maleadt removed this pull request from stack #797 October 21, 2026 13:00
@maleadt
maleadt changed the base branch from tb/permutedims-empty to main October 21, 2026 13:00
@maleadt
maleadt force-pushed the tb/sparse branch 4 times, most recently from 4e20de2 to 099665d Compare October 21, 2026 17:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrays The array abstraction: broadcasting, mapreduce, indexing, views, generic linear algebra.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants