Skip to content

Keep the stored zeros of sparse arguments in hcat, vcat and cat with dense arrays - #1102

Merged
ViralBShah merged 3 commits into
mainfrom
vs/concat-keep-stored-zeros
Oct 16, 2026
Merged

ViralBShah merged 3 commits into
mainfrom
vs/concat-keep-stored-zeros

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Concatenating sparse arrays alone keeps their stored zeros, but as soon as a dense array, a vector, a number, I or a Diagonal joins in, hcat, vcat and cat drop them. hvcat already keeps them in both cases.

Before

julia> A = sparse([1, 2], [1, 2], [1.0, 0.0]); D = [1.0 2; 3 4];

julia> nnz([A A]), nnz([A sparse(D)]), nnz([A D; D A])
(4, 6, 12)

julia> nnz([A D]), nnz([D A]), nnz([A; D]), nnz(hcat(A, ones(2))), nnz([A I]), nnz([A Diagonal([1.0, 2])]), nnz(cat(A, A; dims=(1, 2)))
(5, 5, 5, 3, 3, 3, 2)

After

julia> nnz([A D]), nnz([D A]), nnz([A; D]), nnz(hcat(A, ones(2))), nnz([A I]), nnz([A Diagonal([1.0, 2])]), nnz(cat(A, A; dims=(1, 2)))
(6, 6, 6, 4, 4, 4, 4)

Time (median over three processes of the minimum of 200 samples) and allocations, with S = sprand(2000, 2000, 0.001) after Random.seed!(1), D = rand(2000, 50), v = rand(2000) and Dg = Diagonal(rand(2000)):

main time allocs branch time allocs
[S I] 32.0 µs 55 26.0 µs 38
[S; I] 30.9 µs 55 21.5 µs 53
[S I; I S] 213.2 µs 16080 81.1 µs 94
[S S] 4.8 µs 15 4.7 µs 15
[S D] 243.3 µs 52 152.0 µs 17
[S; D'] 430.8 µs 90 332.2 µs 54
[S v] 86.0 µs 68 13.5 µs 27
[S Dg] 34.2 µs 54 18.8 µs 18
cat(S, S; dims=(1, 2)) 34.1 µs 45 9.0 µs 27
hcat(S, fill(1.0, 2000)) 85.3 µs 71 14.0 µs 30

The mixed path converts every argument to a sparse array and then hands them to Base's generic typed_hcat, typed_vcat or __cat, which allocate a sparse destination and write each block into it with setindex!. Sparse setindex! does not store a zero, so the stored zeros of the sparse arguments were lost; a dense argument loses its zeros in the conversion, as sparse(D) does, which is unchanged. _sparse_cat_t_shaped now assembles a matrix result from the stored entries of the blocks with _hvcat_csc (horizontal or vertical) or _blockdiag (dims=(1, 2)), and _sparse_typed_hcat/_sparse_typed_vcat route through it, so sparse_hcat, sparse_vcat and the UniformScaling concatenations take the same path. A number still fills its whole block, as in dense cat (#383). The result keeps the element and index type it had: the index type of the first array, widened to Int by a leading number, not the promotion that concatenating sparse matrices alone does. A vector result (vcat of vectors and numbers) already kept stored zeros and still goes through Base. _hvcat_csc picks a block by an index known only at run time, which boxed the block once per column and block row when the blocks were of different types, as with the Bool block that I becomes; it now converts such blocks to the result type first, which also removes the per-column allocations that [S I; I S] had on main. Since cat(...; dims=(1, 2)) now reaches _blockdiag, it reads only the first nnz entries of each block's row and value arrays, which may be longer, and it and _hvcat_csc check that the stored entries fit the index type, as the setindex! path did.

Left out: typed concatenation such as Float64[A D] or Float64[A A] goes through Base's typed_hcat/typed_vcat without reaching SparseArrays, and still drops stored zeros, also with sparse arguments only.

Authored with Claude Code.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.83%. Comparing base (11b874d) to head (1023a80).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1102   +/-   ##
=======================================
  Coverage   94.82%   94.83%           
=======================================
  Files          16       16           
  Lines       10234    10252   +18     
=======================================
+ Hits         9704     9722   +18     
  Misses        530      530           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ViralBShah and others added 3 commits October 16, 2026 03:24
… arrays

`hcat`, `vcat` and `cat` that mix a sparse argument with a dense array, a
vector, a number, `I` or a `Diagonal` built the result through Base's
generic concatenation, which writes each block with `setindex!` and so
drops the stored zeros of the sparse arguments. Assemble a matrix result
from the stored entries of the blocks instead, with the kernels that
`hvcat` and `blockdiag` use, as concatenating sparse matrices alone does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_hvcat_csc` picks a block by an index known only at run time, which
boxes the block once per column and block row when the blocks are of
different types, as with the `Bool` block that `I` becomes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah
ViralBShah force-pushed the vs/concat-keep-stored-zeros branch from b7f9491 to 1023a80 Compare October 16, 2026 03:25
@ViralBShah
ViralBShah enabled auto-merge (squash) October 16, 2026 03:26
@ViralBShah
ViralBShah merged commit a49c313 into main Oct 16, 2026
10 checks passed
@ViralBShah
ViralBShah deleted the vs/concat-keep-stored-zeros branch October 16, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant