Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #212 +/- ##
==========================================
+ Coverage 94.42% 95.57% +1.14%
==========================================
Files 8 9 +1
Lines 323 429 +106
==========================================
+ Hits 305 410 +105
- Misses 18 19 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I still have to review this in details (unlikely to have much time before this evening), but in general very happy about moving in this direction! |
|
Does this change the default backend? |
|
Currently it does not. Making it the default would be a breaking change. |
861d7e0 to
2d3e931
Compare
2d3e931 to
3df638c
Compare
`static_size_linear_index` now computes `i + n1 * ((j - 1) + n2 * (k - 1))` instead of the sum of strided terms `1 + (i - 1) + n1 * (j - 1) + (n1 * n2) * (k - 1)`, which needs fewer integer operations. In the flux differencing volume integral of Trixi.jl (2D, 4096 elements, 1 thread), this is 3.5% faster with a `Vector` backend (2553 to 2464 us) and 2.5% faster with a `MemoryRef` backend (#212). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5z6TVTAFfLGSFafUoLzuo
3df638c to
5e9682f
Compare
* Compute the linear index of Cartesian indexing in Horner form `static_size_linear_index` now computes `i + n1 * ((j - 1) + n2 * (k - 1))` instead of the sum of strided terms `1 + (i - 1) + n1 * (j - 1) + (n1 * n2) * (k - 1)`, which needs fewer integer operations. In the flux differencing volume integral of Trixi.jl (2D, 4096 elements, 1 thread), this is 3.5% faster with a `Vector` backend (2553 to 2464 us) and 2.5% faster with a `MemoryRef` backend (#212). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5z6TVTAFfLGSFafUoLzuo * Add tests and a CHANGELOG entry for the Horner-form linear index - Test Cartesian `getindex`/`setindex!` against `LinearIndices` for 1-5 dimensions with odd sizes, size-1 dimensions, `Memory`/`Vector` backends, a static first dimension, and out-of-bounds indices (ported from #206). - FileCheck test: all elements `u[v, i, j, k]` along the first dimension share one offset, with no multiplication after the first load. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5e9682f to
0a2d3db
Compare
| * Static dimensions: `FixedSizeArray{T,N,Mem}` is now an alias for `FixedSizeArrayDims{T,N,NTuple{N,Int},Mem}`, where the new public (but not exported) type `FixedSizeArrays.FixedSizeArrayDims{T,N,D,Mem}` is an `N`-dimensional array over a memory backend `Mem<:DenseVector{T}` whose dimensions are a tuple of type `D` of `Int`s, `Val`s and `StaticInt`s from [Static.jl](https://github.com/SciML/Static.jl) (e.g. `(Val(5), Val(4), n)` or `(static(5), static(4), n)`), so that any dimension can be a compile-time constant while the others are set at runtime. Linear indices, strides and sizes are computed at compile time as far as the dimensions are static. An array with static dimensions can be constructed with `FixedSizeArrays.wrap(FixedSizeArrayDims, parent, dims)`, with `collect_as`, or with the constructors of `FixedSizeArrayDims` (`undef` or converting from another array); the dimensions given to a `FixedSizeArray` constructor are converted to `Int`s. It supports the strided arrays interface, `similar`, `copy`, `fill!`, `copyto!`, broadcasting (returning `FixedSizeArray`s), and the package extensions with MPI.jl, Adapt.jl (adapting the memory backend for any memory backend), and the new package extension with [StaticArrayInterface.jl](https://github.com/JuliaArrays/StaticArrayInterface.jl), which provides static sizes and strides to e.g. StrideArraysCore.jl, LoopVectorization.jl and Polyester.jl. `Val` dimensions need no other package, while `StaticInt` dimensions are available with the new package extension with Static.jl, which is a weak dependency. | ||
| * `FixedSizeArrays.wrap` now accepts at most one dimension given as `:`, which is computed from the length of the parent. | ||
| * With StaticArrayInterface.jl, `StaticArrayInterface.device` of a `FixedSizeArray` is now the one of its memory backend, e.g. a GPU memory backend is not treated as a CPU pointer by LoopVectorization.jl. | ||
| * On Julia v1.11 and later, the memory backend of a `FixedSizeArray` can be a `GenericMemoryRef` (e.g. `FixedSizeArray{T,N,MemoryRef{T}}`, also with static dimensions, e.g. `FixedSizeArrayDims{T,N,D,MemoryRef{T}}`), a reference to an element in a `Memory`, which can be at an offset and does not need to have exactly the length of the array. `FixedSizeArrays.wrap(FixedSizeArray, ref, size)` wraps a reference, `FixedSizeArrays.wrap(FixedSizeArray{T,N,MemoryRef{T}} where {T,N}, v::Vector, size)` the reference of a `Vector`. Unlike a `Vector` backend, the data pointer is a field of the immutable `FixedSizeArray` and need not be reloaded from a mutable object at every access. |
There was a problem hiding this comment.
Reminder: move this entry to a different version section, when we're ready to move on with this PR
giordano
left a comment
There was a problem hiding this comment.
Caveat: generic code that recurses into
parent(A)expecting anAbstractArrayneeds a method for this backend (e.g.KernelAbstractions.get_backend). As for a wrappedVector, the array keeps the old memory alive afterresize!of the vector, so it stays memory/GC safe but may no longer alias it; wrap again after resizing (documented inwrap).
This is probably the more problematic point of this PR, there's quite a bit of workarounds to make this kinda work.
We should also have FileCheck tests, including ensuring that memory loads are hoisted in loops with conditional access
| Base.@propagate_inbounds function copyto_memoryref!(dst, doff::Int, src, soff::Int, n::Int) | ||
| n < 0 && throw_copyto_negative(n) | ||
| iszero(n) && return dst | ||
| @boundscheck checkbounds(dst, doff:(doff + n - 1)) | ||
| @boundscheck checkbounds(src, soff:(soff + n - 1)) | ||
| dr = maybe_memoryref(dst) | ||
| sr = maybe_memoryref(src) | ||
| unsafe_copyto!(element_ref(dr, doff), element_ref(sr, soff), n) | ||
| dst | ||
| end |
There was a problem hiding this comment.
This can write past the end of dst inside @inbounds:
julia> using FixedSizeArrays
julia> mem = Memory{Float64}(zeros(6));
julia> a = FixedSizeArrays.wrap(FixedSizeArray, memoryref(mem), (2,)); # the first 2 elements of `mem`
julia> b = FixedSizeArray{Float64,1,MemoryRef{Float64}}(ones(4));
julia> f!(a, b) = @inbounds copyto!(a, b)
f! (generic function with 1 method)
julia> f!(a, b)
2-element FixedSizeVector{Float64, MemoryRef{Float64}}:
1.0
1.0
julia> mem
6-element Memory{Float64}:
1.0
1.0
1.0
1.0
0.0
0.0| function wrap_backend(::Type{<:(FixedSizeArrayDims{T, N, D, <:MemRef{T}} where {T, N, D})}, parent::Vector) | ||
| getfield(parent, :ref) | ||
| end |
There was a problem hiding this comment.
Should there be a method to wrap a Memory (with memoryrefnew(mem))?
| storage_array_type(::Type{R}) where {R <: GenericMemoryRef} = memory_type(R) | ||
|
|
||
|
|
||
| storage_dataids(ref::GenericMemoryRef) = Base.dataids(getfield(ref, :mem)) |
There was a problem hiding this comment.
Should this be aware of the offsets, so that non-overlapping slices of the same memory don't alias?
| function adapt_backend(to, ref::MemoryRef, a::FixedSizeArrayDims) | ||
| v = Base.wrap(Array, ref, length(a)) | ||
| adapted = adapt(to, v) | ||
| adapted === v ? ref : adapted | ||
| end |
There was a problem hiding this comment.
This means that the backend type may not be preserved?
| # support for the storage type is used: since its first element is the first element of | ||
| # the array, the pointer to it is the pointer to the array, and the result of `cconvert`, | ||
| # which `ccall` keeps rooted, keeps the memory alive. | ||
| Base.cconvert(::Type{MPIPtr}, a::FixedSizeArrayDims) = Base.cconvert(MPIPtr, underlying_storage(a)) |
There was a problem hiding this comment.
Do we still need this method at all with MPI.jl@0.20.28?
| # use the fast path only if one side has a `GenericMemoryRef` backend and both sides are | ||
| # backed by a memory reference with the same element type and memory kind | ||
| function copyto_fast_path(dst, src) | ||
| dr = maybe_memoryref(dst) | ||
| sr = maybe_memoryref(src) | ||
| (dr isa MemoryRef) && (sr isa MemoryRef) && (eltype(dr) === eltype(sr)) |
There was a problem hiding this comment.
The check contradicts the comment that this should apply to GenericMemoryRef?
Rebased onto the static dimensions of #213 (`FixedSizeArrayDims`), so that arrays with static dimensions can have a `GenericMemoryRef` backend too, e.g. `FixedSizeArrayDims{T,N,D,MemoryRef{T}}`: - `wrap` of a reference accepts static dimensions and a `:` dimension, which is computed from the elements of the memory from the reference on. - `wrap` of a `Vector` with a type asking for a reference backend (e.g. `FixedSizeArrayDims{T,N,D,MemoryRef{T}} where {T,N,D}`) wraps the reference of the `Vector`. - `similar`, `copy`, `fill!`, `isassigned` and Cartesian indexing with static strides go through the storage hooks. - StaticArrayInterface.jl: the `device` of a reference backend is the one of its memory, so that LoopVectorization.jl accepts these arrays. - Adapt.jl: for static dimensions, a reference backend is adapted as a `Vector` sharing its elements, and kept if adapting does not change it. - Tests: the per-backend test sets with `MemoryRef` as in the original PR (except the `wrap` and static dimensions test sets, which assume a parent of exactly the length of the array), plus a new test set for static dimensions with a reference backend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0a2d3db to
19b939f
Compare
Requested by Valentin
Before: wrapping a
Vectorwithout copying (wrap(FixedSizeArray, v, dims)) givesFixedSizeArray{T,N,Vector{T}}, whose every access loads the data pointer and theMemoryfrom the mutableVector. In loops with conditional accesses LLVM cannot hoist these loads; under Enzyme.jl this made the augmented forward pass cache the (shadow)Memoryon every inner iteration (e.g. 9.3 MB vs 1.9 MB of GC-allocated tape for a Trixi.jl flux-differencing VJP in 3D). Wrappingv.ref.meminstead is not an option, since the memory can be longer than the vector (afterresize!) and the vector can start at an offset.After: on Julia v1.11 and later, the memory backend can be a
GenericMemoryRef(e.g.FixedSizeArray{T,N,MemoryRef{T}}). The array stores the reference directly and its ownsizegives the length, so the data pointer is a field of the immutableFixedSizeArray.How:
MemisUnion{DenseVector{T}, GenericMemoryRef{<:Any,T}}on v1.11+ (unchanged on v1.10). Storage access goes through small internal hooks; the reference implementations are insrc/memoryref.jland usememoryrefnew(ref, i, false)+memoryrefget/memoryrefset!/memoryref_isassignedwith the default access order (soAtomicMemoryworks). Bounds are checked against theFixedSizeArray's size.wrap(FixedSizeArray, ref, size)requires at leastprod(size)elements fromrefon;wrap(FixedSizeArray{T,N,MemoryRef{T}} where {T,N}, v::Vector, size)wrapsv.ref(exact length).parentreturns the reference andparentindicesare1:lengthrelative to it;dataidsare those of the memory, so the array and the wrappedVectorreportmightalias.similar, broadcasting,copyandcollect_asallocate a newMemoryand keep the reference backend;copyto!between references usesunsafe_copyto!; the Random and MPI extensions use a view of the memory.parent(A)expecting anAbstractArrayneeds a method for this backend (e.g.KernelAbstractions.get_backend). As for a wrappedVector, the array keeps the old memory alive afterresize!of the vector, so it stays memory/GC safe but may no longer alias it; wrap again after resizing (documented inwrap).GenericMemoryRefbackend" (858 tests: offsets afterpopfirst!, references shorter than their memory, empty arrays,Stringelements, aliasing with theVector, the resize contract, GC with write barriers, overlappingcopyto!, broadcasting into an aliased destination, MPI pointers,AtomicMemory, inference and no allocations), plusMemoryRefin the per-backend test loops. The full test suite passes locally on Julia 1.11.9 and 1.12.7.In Trixi.jl (local prototype), backing the solution arrays with this backend reduces the GC-allocated Enzyme tape of an
rhs!VJP from 0.67 MB to 11 KB (2D) and from 1.86 MB to 26 KB (3D) compared toArray, with identical gradients and bit-identical AMR results. Differentiating it with Enzyme.jl needs EnzymeAD/Enzyme.jl#3835 (make_zeroof aMemoryRef).🤖 Generated with Claude Code