Skip to content

Commit ecdc3ae

Browse files
Add view functionality and tests for DArray chunk slicing
1 parent 531f2bc commit ecdc3ae

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

docs/src/darray.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,78 @@ across the workers in the Julia cluster in a relatively even distribution;
211211
future operations on a `DArray` may produce a different distribution from the
212212
one chosen by previous calls.
213213

214+
### DArray Chunk Slicing with `view`
215+
216+
Dagger's `view` function allows you to efficiently create a "view" of a `DArray`'s `Chunk` or a `DTask` that produces a `DArray` `Chunk`. This enables operations on specific parts of your distributed data using standard Julia array slicing, without needing to materialize the entire chunk.
217+
218+
```view(c::Chunk, slices...) & view(c::DTask, slices...)```
219+
220+
These methods create a `view` for a `DArray` `Chunk` object or for a `DTask` that will produce a `DArray` `Chunk`. You specify the desired sub-region using standard Julia array slicing syntax, identical to how you would slice a regular Array.
221+
222+
#### Examples
223+
224+
```julia
225+
julia> A = rand(64, 64)
226+
64×64 Matrix{Float64}:
227+
[...]
228+
229+
julia> DA = DArray(A, Blocks(8,8))
230+
64x64 DMatrix{Float64} with 8x8 partitions of size 8x8:
231+
[...]
232+
233+
julia> chunk = DA.chunks[1,1]
234+
DTask (finished)
235+
236+
julia> view(chunk, :, :) # View the entire 8x8 chunk
237+
ChunkSlice{2}(Dagger.Chunk(...), (Colon(), Colon()))
238+
239+
julia> view(chunk, 1:4, 1:4) # View the top-left 4x4 sub-region of the chunk
240+
ChunkSlice{2}(Dagger.Chunk(...), (1:4, 1:4))
241+
242+
julia> view(chunk, 1, :) # View the first row of the chunk
243+
ChunkSlice{2}(Dagger.Chunk(...), (1, Colon()))
244+
245+
julia> view(chunk, :, 5) # View the fifth column of the chunk
246+
ChunkSlice{2}(Dagger.Chunk(...), (Colon(), 5))
247+
248+
julia> view(chunk, 1:2:7, 2:2:8) # View with stepped ranges
249+
ChunkSlice{2}(Dagger.Chunk(...), (1:2:7, 2:2:8))
250+
```
251+
252+
#### Example Usage: Parallel Row Summation of a DArray using `view`
253+
This example demonstrates how to sum multiple rows of a `DArray` by using `view` to process individual rows within chunks to get Row Sum Vector.
254+
255+
```julia
256+
julia> A = DArray(rand(10,1000), Blocks(2,1000))
257+
10x1000 DMatrix{Float64} with 5x1 partitions of size 2x1000:
258+
[...]
259+
260+
# Helper function to sum a single row and store it in a provided array view
261+
julia> @everywhere function sum_array_row!(row_sum::AbstractArray{Float64}, x::AbstractArray{Float64})
262+
row_sum[1] = sum(x)
263+
end
264+
265+
# Number of rows
266+
julia> nrows = size(A,1)
267+
268+
# Initialize a zero array in the final row sums
269+
julia> row_sums = zeros(nrows)
270+
271+
# Spawn tasks to sum each row in parallel using views
272+
julia> Dagger.spawn_datadeps() do
273+
sz = size(A.chunks,1)
274+
nrows_per_chunk = nrows ÷ sz
275+
for i in 1:sz
276+
for j in 1:nrows_per_chunk
277+
Dagger.@spawn sum_array_row!(Out(view(row_sums, (nrows_per_chunk*(i-1)+j):(nrows_per_chunk*(i-1)+j))), In(Dagger.view(BD.chunks[i,1], j:j, :)))
278+
end
279+
end
280+
281+
# Print the result
282+
julia> println("Row sum Vector: ", row_sums)
283+
Row sum Vector: [499.8765, 500.1234, ..., 499.9876]
284+
```
285+
214286
<!-- -->
215287

216288
### Explicit Processor Mapping of DArray Blocks

src/memory-spaces.jl

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,56 @@ function will_alias(x_span::MemorySpan, y_span::MemorySpan)
386386
x_end = x_span.ptr + x_span.len - 1
387387
y_end = y_span.ptr + y_span.len - 1
388388
return x_span.ptr <= y_end && y_span.ptr <= x_end
389-
end
389+
end
390+
391+
struct ChunkSlice{N} #<: AbstractAliasing
392+
chunk::Chunk
393+
slices::NTuple{N, Union{Int, AbstractRange{Int}, Colon}}
394+
end
395+
396+
Base.copyto!(dest::ChunkSlice, src::ChunkSlice) = copyto!(view(unwrap(dest.chunk), dest.slices...), view(unwrap(src.chunk), src.slices...))
397+
398+
@inline function view(c::Chunk, slices...)
399+
isa(c.domain, ArrayDomain) || throw(ArgumentError("Chunk must of a DArray (ArrayDomain), got $(typeof(c.domain))"))
400+
nd, sz = ndims(c.domain), size(c.domain)
401+
nd == length(slices) || throw(DimensionMismatch("Expected $nd slices, got $(length(slices))"))
402+
403+
for (i, s) in enumerate(slices)
404+
if s isa Int
405+
1 s sz[i] || throw(ArgumentError("Index $s out of bounds for dimension $i (size $(sz[i]))"))
406+
elseif s isa AbstractRange
407+
isempty(s) && continue
408+
1 first(s) last(s) sz[i] || throw(ArgumentError("Range $s out of bounds for dimension $i (size $(sz[i]))"))
409+
elseif s === Colon()
410+
continue
411+
else
412+
throw(ArgumentError("Invalid slice type $(typeof(s)) at dimension $i, Expected Type of Int, AbstractRange, or Colon"))
413+
end
414+
end
415+
416+
return ChunkSlice(c, slices)
417+
end
418+
419+
view(c::DTask, slices...) = view(fetch(c; raw=true), slices...)
420+
421+
function aliasing(x::ChunkSlice{N}) where N
422+
remotecall_fetch(root_worker_id(x.chunk.processor), x.chunk, x.slices) do x, slices
423+
x = unwrap(x)
424+
v = view(x, slices...)
425+
return aliasing(v)
426+
end
427+
end
428+
429+
function move!(dep_mod, to_space::MemorySpace, from_space::MemorySpace, to::ChunkSlice, from::ChunkSlice)
430+
to_w = root_worker_id(to_space)
431+
remotecall_wait(to_w, dep_mod, to_space, from_space, to, from) do dep_mod, to_space, from_space, to, from
432+
to_raw = unwrap(to.chunk)
433+
from_w = root_worker_id(from_space)
434+
from_raw = to_w == from_w ? unwrap(from.chunk) : remotecall_fetch(unwrap, from_w, from.chunk)
435+
from_view = view(from_raw, from.slices...)
436+
to_view = view(to_raw, to.slices...)
437+
move!(dep_mod, to_space, from_space, to_view, from_view)
438+
end
439+
end
440+
441+
move(from_proc::Processor, to_proc::Processor, slice::ChunkSlice) = view(move(from_proc, to_proc, slice.chunk), slice.slices...)

test/array/allocation.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,44 @@ end
699699
@test A_v == A[1:8, 1:8]
700700
end
701701

702+
@testset "Chunk view of DArray" begin
703+
A = rand(64, 64)
704+
DA = DArray(A, Blocks(8,8))
705+
chunk = DA.chunks[1,1]
706+
707+
@testset "Valid Slices" begin
708+
@test view(chunk, :, :) isa ChunkSlice && view(chunk, 1:8, 1:8) isa ChunkSlice
709+
@test view(chunk, 1:2:7, :) isa ChunkSlice && view(chunk, :, 2:2:8) isa ChunkSlice
710+
@test view(chunk, 1, :) isa ChunkSlice && view(chunk, :, 1) isa ChunkSlice
711+
@test view(chunk, 3:3, 5:5) isa ChunkSlice && view(chunk, 5:7, 1:2:4) isa ChunkSlice
712+
@test view(chunk, 8, 8) isa ChunkSlice
713+
@test view(chunk, 1:0, :) isa ChunkSlice
714+
end
715+
716+
@testset "Dimension Mismatch" begin
717+
@test_throws DimensionMismatch view(chunk, :)
718+
@test_throws DimensionMismatch view(chunk, :, :, :)
719+
end
720+
721+
@testset "Int Slice Out of Bounds" begin
722+
@test_throws ArgumentError view(chunk, 0, :)
723+
@test_throws ArgumentError view(chunk, :, 9)
724+
@test_throws ArgumentError view(chunk, 9, 1)
725+
end
726+
727+
@testset "Range Slice Out of Bounds" begin
728+
@test_throws ArgumentError view(chunk, 0:5, :)
729+
@test_throws ArgumentError view(chunk, 1:8, 5:10)
730+
@test_throws ArgumentError view(chunk, 2:2:10, :)
731+
end
732+
733+
@testset "Invalid Slice Types" begin
734+
@test_throws DimensionMismatch view(chunk, (1:2, :))
735+
@test_throws ArgumentError view(chunk, :, [1, 2])
736+
end
737+
738+
end
739+
702740
@testset "copy/similar" begin
703741
X1 = ones(Blocks(10, 10), 100, 100)
704742
X2 = copy(X1)

0 commit comments

Comments
 (0)