Skip to content

Make whole views of a sparse vector or column behave like the parent for any index type - #1119

Merged
ViralBShah merged 3 commits into
mainfrom
vs/vectorview-index-type
Oct 27, 2026
Merged

ViralBShah merged 3 commits into
mainfrom
vs/vectorview-index-type

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

A whole view of a sparse vector, view(x, :), or of a sparse column, view(A, :, j), should behave like the vector it views. Two things stopped it. view(x, :) of a SparseVector{Tv,Int32} is not a SparseVectorView, so it misses every O(nnz) method on that alias: sum, maximum, norm and dot visit all n elements, rmul! and lmul! take the dense fallback, kron is dense and findnz throws. And for any index type, SparseVector(v) of a whole view, which sparse, convert, vcat, hcat and kron all go through, reads it as a dense vector: O(n), stored zeros dropped, Int indices.

using SparseArrays
x = sparsevec(Int32[2, 4, 5], [1.0, 0.0, 3.0], 6); v = view(x, :)
A = sparse(Int32[1, 3], Int32[1, 1], [1.0, 0.0], 4, 2); c = view(A, :, 1)

Before

SparseVector(v) = sparsevec([2, 5], [1.0, 3.0], 6)
sparse(v) = sparsevec([2, 5], [1.0, 3.0], 6)
vcat(v, v) = sparsevec([2, 5, 8, 11], [1.0, 3.0, 1.0, 3.0], 12)
typeof(kron(v, x)) = Vector{Float64}
typeof(SparseVector(c)) = SparseVector{Float64, Int64}
findnz(v): MethodError: no method matching findnz(::SubArray{Float64, 1, SparseVector{Float64, Int32}, Tuple{Base.Slice{Base.OneTo{Int32}}}, false})

After

SparseVector(v) = sparsevec(Int32[2, 4, 5], [1.0, 0.0, 3.0], 6)
sparse(v) = sparsevec(Int32[2, 4, 5], [1.0, 0.0, 3.0], 6)
vcat(v, v) = sparsevec(Int32[2, 4, 5, 8, 10, 11], [1.0, 0.0, 3.0, 1.0, 0.0, 3.0], 12)
kron(v, x) = sparsevec(Int32[8, 10, 11, 20, 22, 23, 26, 28, 29], [1.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 9.0], 36)
typeof(SparseVector(c)) = SparseVector{Float64, Int32}
findnz(v) = (Int32[2, 4, 5], [1.0, 0.0, 3.0])

For v32 = view(sparsevec(Int32[1, n], [1.0, 2.0], n), :) and v64 likewise, with n = 10^7, the minimum of three runs and the @allocated of a warm call:

f(v) v32, main v32, branch v64, main v64, branch
sum 0.020 s, 0 B 2.0e-8 s, 0 B 3.0e-8 s, 0 B 3.0e-8 s, 0 B
maximum 0.047 s, 0 B 2.9e-8 s, 0 B 3.0e-8 s, 0 B 2.0e-8 s, 0 B
dot(v, v) 0.035 s, 0 B 3.0e-8 s, 0 B 2.1e-8 s, 0 B 2.0e-8 s, 0 B
count(!iszero, v) 0.021 s, 0 B 2.0e-8 s, 0 B 0.021 s, 0 B 2.0e-8 s, 0 B
findnz MethodError 3.0e-8 s, 144 B 3.0e-8 s, 160 B 3.0e-8 s, 160 B
SparseVector 0.027 s, 19 MiB 5.0e-8 s, 144 B 0.023 s, 19 MiB 4.0e-8 s, 160 B
vcat(v, v) 0.057 s, 38 MiB 3.1e-7 s, 880 B 0.052 s, 38 MiB 2.6e-7 s, 960 B

A SparseVector stores its length as Ti, so axes(x32) is (OneTo{Int32}(n),) and view(x32, :) slices a Slice{OneTo{Int32}}, while SparseVectorView named Slice{OneTo{Int}} only. A matrix stores its size as Int, so SparseColumnView already matched for every index type. The fix widens SparseVectorView to <:Tuple{Base.Slice{<:Base.OneTo}}; no method on the alias assumes Int axes, since they read the parent through nonzeros, nonzeroinds and length. The SparseVector copy constructors now take SparseVectorOrView rather than AbstractCompressedVector, so a whole view or column view is copied from its stored entries with the parent's index type, and sparsevec of one copies it through them. count on a SparseVector already counted f over the stored values plus f of one zero times the number of unstored entries; that method (_simple_count) now also covers the whole and range views, and takes the zero from _densezero.

This touches dispatch.

Potential breakage, in each case towards what the parent already gives:

  • For a whole view or column view of any index type, SparseVector(v), sparse(v), convert(SparseVector, v) and sparse vcat, hcat, hvcat and kron that take one keep its stored zeros, so nnz of the result can be larger than before.
  • They also keep the parent's index type instead of Int.
  • For a whole view v of a SparseVector with a non-Int index type: nonzeros(v) is the parent's Vector rather than a SubArray of it; nonzeroinds(v) is the parent's own index vector rather than a fresh copy, so writing to it changes the parent; findfirst, findlast, findnext and findprev return Ti rather than Int; diff(v) keeps a stored zero; kron(v, x) and kron(v, A) are sparse rather than dense; and A * v with an Int-indexed A is a SparseVector{Tv,Int} rather than a SparseVector{Tv,Ti}.

length keeps returning Ti, as #176 (JuliaLang/julia#45937) decided. Range views such as view(x, 2:4) still convert through the dense path, and spdiagm(0 => view(x, :)) still stores every diagonal entry; both are left for separate PRs.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.68%. Comparing base (e4a42b4) to head (f3c058b).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1119      +/-   ##
==========================================
- Coverage   94.73%   94.68%   -0.05%     
==========================================
  Files          16       16              
  Lines       10325    10433     +108     
==========================================
+ Hits         9781     9879      +98     
- Misses        544      554      +10     

☔ 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.

…s views dispatch to the sparse methods

`length` and `size` of a `SparseVector` and a `FixedSparseVector` return an
`Int` when every value of the index type fits in one, and `Ti` otherwise, so
the whole view of a narrow-indexed vector is a `SparseVectorView`. The alias is
widened to any `Slice{<:OneTo}` for the wider index types, and `count` over
sparse vector views uses the stored entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah
ViralBShah force-pushed the vs/vectorview-index-type branch from 1ab862c to 803d0d2 Compare October 26, 2026 15:02
@ViralBShah ViralBShah changed the title Dispatch view(x, :) of a non-Int-indexed SparseVector to the SparseVectorView methods Return Int from length and axes of a SparseVector, so that its views dispatch to the sparse methods Oct 26, 2026
Revert the `Int` length: #176 (JuliaLang/julia#45937) decided that `length`
returns the index type. The widened `SparseVectorView` alias and the O(nnz)
`count` on sparse vector views stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah ViralBShah changed the title Return Int from length and axes of a SparseVector, so that its views dispatch to the sparse methods Dispatch view(x, :) of a non-Int-indexed SparseVector and count of a sparse vector view to the O(nnz) methods Oct 26, 2026
…n `SparseVector`

`SparseVector(v)`, and through it `sparse`, `convert`, concatenation and
`kron`, read a whole view as a dense vector: O(n), stored zeros dropped and
`Int` indices. The copy constructors now take `SparseVectorOrView`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah ViralBShah changed the title Dispatch view(x, :) of a non-Int-indexed SparseVector and count of a sparse vector view to the O(nnz) methods Make whole views of a sparse vector or column behave like the parent for any index type Oct 27, 2026
@ViralBShah
ViralBShah merged commit 61f75e0 into main Oct 27, 2026
12 checks passed
@ViralBShah
ViralBShah deleted the vs/vectorview-index-type branch October 27, 2026 03:20
ViralBShah added a commit that referenced this pull request Oct 27, 2026
A range view of a sparse vector, `view(x, 2:6)`, is converted to a
`SparseVector` by reading it as a dense vector, so `SparseVector`,
`sparse`, `sparsevec`, `convert` and sparse `vcat`, `hcat` and `hvcat`
take O(n), drop stored zeros and give `Int` indices, and `kron` of a
range view is dense.

```julia
using SparseArrays
x = sparsevec(Int32[2, 4, 5], [1.0, 0.0, 3.0], 7); v = view(x, 2:6)
```

**Before**
```
SparseVector(v) = sparsevec([1, 4], [1.0, 3.0], 5)
vcat(v, v) = sparsevec([1, 4, 6, 9], [1.0, 3.0, 1.0, 3.0], 10)
typeof(kron(v, x)) = Vector{Float64}
```

**After**
```
SparseVector(v) = sparsevec(Int32[1, 3, 4], [1.0, 0.0, 3.0], 5)
vcat(v, v) = sparsevec(Int32[1, 3, 4, 6, 8, 9], [1.0, 0.0, 3.0, 1.0, 0.0, 3.0], 10)
typeof(kron(v, x)) = SparseVector{Float64, Int32}
```

For `w = view(sparsevec([1, n], [1.0, 2.0], n), 1:n)` with `n = 10^7`
and `y = sparsevec([1], [1.0], 2)`, the minimum of three runs and the
`@allocated` of a warm call:

| `f(w)`            | main               | branch             |
|-------------------|--------------------|--------------------|
| `SparseVector(w)` | 0.030 s, 19.1 MiB  | 2.3e-7 s, 240 B    |
| `vcat(w, w)`      | 0.056 s, 38.2 MiB  | 3.8e-7 s, 1.1 KiB  |
| `kron(w, y)`      | 0.067 s, 152.6 MiB | 3.8e-7 s, 976 B    |

The `SparseVector` copy constructors and `sparsevec`, which #1119
widened to whole views, also take `SparseVectorPartialView`, whose
`nonzeroinds` and `nonzeros` give its stored entries relative to the
range. `convert` and the concatenations reach them through
`_makesparse`. `kron` takes range views in `_SparseKronGroup` and in its
vector methods, so they go through the same conversion. The
`_sparsevector` helper that sparse `map` uses for views now calls
`SparseVector` instead of repeating it.

This touches dispatch.

Potential breakage: for a range view, these conversions and
concatenations keep its stored zeros and the parent's index type, and
`kron` of one is sparse rather than dense, as for `x[2:6]`.

Products with a range view are in #1135.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah ViralBShah mentioned this pull request Oct 27, 2026
30 tasks done
ViralBShah added a commit that referenced this pull request Oct 27, 2026
Backports of the merged PRs labelled `backport 1.14`, cherry-picked with
`-x` in the order they were merged on `main`. `release-1.14` starts at
b0aa455 (#1104), the commit JuliaLang/julia's `release-1.14` pins, and
every PR merged on `main` since then is labelled, so this branch's tree
is identical to `main`'s at 9bfad27.

- [x] #1105
- [x] #1011
- [x] #1107
- [x] #1108
- [x] #1110
- [x] #1111
- [x] #1114
- [x] #1112
- [x] #1115
- [x] #1117
- [x] #1118
- [x] #1109
- [x] #1113
- [x] #1122
- [x] #1123
- [x] #1120
- [x] #1124
- [x] #1125
- [x] #1116
- [x] #1121
- [x] #1126
- [x] #1128
- [x] #1130
- [x] #1131
- [x] #1119
- [x] #1133
- [x] #1127
- [x] #1135
- [x] #1134
- [x] #1137

#1137 removes the first-call compile time that #1128 added, which the
TTFX check flagged on this PR.

#1132 merged into the branch of #1131 and is part of its commit.

Not bug fixes: #1011 adds a deprecation, #1109 changes the `sprand`
value stream for a value function, #1113 changes the result shape of
`SparseMatrixCSC` of a vector view, and #1119, #1133, #1134 and #1135
add methods for views. They are included because they are labelled and
1.14 is not released yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
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