Skip to content

Commit

Permalink
Merge pull request #389 from willow-ahrens/wma/finchlogic
Browse files Browse the repository at this point in the history
High-Level Interface
  • Loading branch information
willow-ahrens authored Mar 7, 2024
2 parents e6544d8 + d41da4d commit d379410
Show file tree
Hide file tree
Showing 47 changed files with 2,105 additions and 953 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Finch"
uuid = "9177782c-1635-4eb9-9bfb-d9dfa25e6bce"
authors = ["Willow Ahrens"]
version = "0.6.14"
version = "0.6.15"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
Expand Down
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ makedocs(;
"Comprehensive Guides" => [
"Calling Finch" => "guides/calling_finch.md",
"Tensor Formats" => "guides/tensor_formats.md",
"Sparse and Structured Utilities" => "guides/sparse_utils.md",
"The Finch Language" => "guides/finch_language.md",
"Dimensionalization" => "guides/dimensionalization.md",
#"Tensor Lifecycles" => "guides/tensor_lifecycles.md",
"Index Sugar" => "guides/index_sugar.md",
"Mask Sugar" => "guides/mask_sugar.md",
"Iteration Protocols" => "guides/iteration_protocols.md",
"Custom Operators" => "guides/custom_operators.md",
"Array API and Fusion" => "guides/array_fusion.md",
#"Parallelization and Architectures" => "guides/parallelization.md",
"FileIO" => "guides/fileio.md",
"Interoperability" => "guides/interoperability.md",
Expand Down
132 changes: 132 additions & 0 deletions docs/src/guides/array_fusion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
```@meta
CurrentModule = Finch
```

# Array API

Finch tensors also support many of the basic array operations one might expect,
including indexing, slicing, and elementwise maps, broadcast, and reduce.
For example:

```jldoctest example1; setup = :(using Finch)
julia> A = fsparse([1, 1, 2, 3], [2, 4, 5, 6], [1.0, 2.0, 3.0])
SparseCOO{2} (0.0) [:,1:6]
├─ [1, 2]: 1.0
├─ [1, 4]: 2.0
└─ [2, 5]: 3.0
julia> A + 0
Dense [:,1:6]
├─ [:, 1]: Dense [1:3]
│ ├─ [1]: 0.0
│ ├─ [2]: 0.0
│ └─ [3]: 0.0
├─ [:, 2]: Dense [1:3]
│ ├─ [1]: 1.0
│ ├─ [2]: 0.0
│ └─ [3]: 0.0
├─ [:, 3]: Dense [1:3]
│ ├─ [1]: 0.0
│ ├─ [2]: 0.0
│ └─ [3]: 0.0
├─ [:, 4]: Dense [1:3]
│ ├─ [1]: 2.0
│ ├─ [2]: 0.0
│ └─ [3]: 0.0
├─ [:, 5]: Dense [1:3]
│ ├─ [1]: 0.0
│ ├─ [2]: 3.0
│ └─ [3]: 0.0
└─ [:, 6]: Dense [1:3]
├─ [1]: 0.0
├─ [2]: 0.0
└─ [3]: 0.0
julia> A + 1
Dense [:,1:6]
├─ [:, 1]: Dense [1:3]
│ ├─ [1]: 1.0
│ ├─ [2]: 1.0
│ └─ [3]: 1.0
├─ [:, 2]: Dense [1:3]
│ ├─ [1]: 2.0
│ ├─ [2]: 1.0
│ └─ [3]: 1.0
├─ [:, 3]: Dense [1:3]
│ ├─ [1]: 1.0
│ ├─ [2]: 1.0
│ └─ [3]: 1.0
├─ [:, 4]: Dense [1:3]
│ ├─ [1]: 3.0
│ ├─ [2]: 1.0
│ └─ [3]: 1.0
├─ [:, 5]: Dense [1:3]
│ ├─ [1]: 1.0
│ ├─ [2]: 4.0
│ └─ [3]: 1.0
└─ [:, 6]: Dense [1:3]
├─ [1]: 1.0
├─ [2]: 1.0
└─ [3]: 1.0
julia> B = A .* 2
Sparse (0.0) [:,1:6]
├─ [:, 2]: Sparse (0.0) [1:3]
│ └─ [1]: 2.0
├─ [:, 4]: Sparse (0.0) [1:3]
│ └─ [1]: 4.0
└─ [:, 5]: Sparse (0.0) [1:3]
└─ [2]: 6.0
julia> B[1:2, 1:2]
Sparse (0.0) [:,1:2]
└─ [:, 2]: Sparse (0.0) [1:2]
└─ [1]: 2.0
julia> map(x -> x^2, B)
Sparse (0.0) [:,1:6]
├─ [:, 2]: Sparse (0.0) [1:3]
│ └─ [1]: 4.0
├─ [:, 4]: Sparse (0.0) [1:3]
│ └─ [1]: 16.0
└─ [:, 5]: Sparse (0.0) [1:3]
└─ [2]: 36.0
```

# Array Fusion

Finch supports array fusion, which allows you to compose multiple array operations
into a single kernel. This can be a significant performance optimization, as it
allows the compiler to optimize the entire operation at once. The two functions
the user needs to know about are `lazy` and `compute`. You can use `lazy` to
mark an array as an input to a fused operation, and call `compute` to execute
the entire operation at once. For example:

```jldoctest example1
julia> C = lazy(A);
julia> D = lazy(B);
julia> E = (C .+ D)/2;
julia> compute(E)
Sparse (0.0) [:,1:6]
├─ [:, 2]: Sparse (0.0) [1:3]
│ └─ [1]: 1.5
├─ [:, 4]: Sparse (0.0) [1:3]
│ └─ [1]: 3.0
└─ [:, 5]: Sparse (0.0) [1:3]
└─ [2]: 4.5
```

In the above example, `E` is a fused operation that adds `C` and `D` together
and then divides the result by 2. The `compute` function examines the entire
operation and decides how to execute it in the most efficient way possible.
In this case, it would likely generate a single kernel that adds the elements of `A` and `B`
together and divides each result by 2, without materializing an intermediate.

```@docs
lazy
compute
```
17 changes: 1 addition & 16 deletions docs/src/guides/optimization_tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ quote
A_lvl_idx = A_lvl_2.idx
A_lvl_2_val = A_lvl_2.lvl.val
B = (((ex.bodies[1]).bodies[2]).body.body.rhs.args[2]).tns.bind
sugar_1 = size(B)
sugar_1 = size((((ex.bodies[1]).bodies[2]).body.body.rhs.args[2]).tns.bind)
B_mode1_stop = sugar_1[1]
B_mode2_stop = sugar_1[2]
B_mode1_stop == A_lvl_2.shape || throw(DimensionMismatch("mismatched dimension limits ($(B_mode1_stop) != $(A_lvl_2.shape))"))
Expand All @@ -253,9 +253,6 @@ quote
result = nothing
C_val = 0
for j_4 = 1:B_mode2_stop
sugar_2 = size(B)
B_mode1_stop = sugar_2[1]
B_mode2_stop = sugar_2[2]
A_lvl_q = (1 - 1) * A_lvl.shape + j_4
A_lvl_2_q = A_lvl_ptr[A_lvl_q]
A_lvl_2_q_stop = A_lvl_ptr[A_lvl_q + 1]
Expand All @@ -278,9 +275,6 @@ quote
C_val = (Main).f(0.0, val) + C_val
end
A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]
sugar_4 = size(B)
B_mode1_stop = sugar_4[1]
B_mode2_stop = sugar_4[2]
val = B[A_lvl_2_i, j_4]
C_val += (Main).f(A_lvl_3_val, val)
A_lvl_2_q += 1
Expand All @@ -293,17 +287,11 @@ quote
C_val += (Main).f(0.0, val)
end
A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]
sugar_6 = size(B)
B_mode1_stop = sugar_6[1]
B_mode2_stop = sugar_6[2]
val = B[phase_stop_3, j_4]
C_val += (Main).f(A_lvl_3_val, val)
A_lvl_2_q += 1
else
for i_10 = i:phase_stop_3
sugar_7 = size(B)
B_mode1_stop = sugar_7[1]
B_mode2_stop = sugar_7[2]
val = B[i_10, j_4]
C_val += (Main).f(0.0, val)
end
Expand All @@ -316,9 +304,6 @@ quote
phase_start_3 = max(1, 1 + A_lvl_2_i1)
if B_mode1_stop >= phase_start_3
for i_12 = phase_start_3:B_mode1_stop
sugar_8 = size(B)
B_mode1_stop = sugar_8[1]
B_mode2_stop = sugar_8[2]
val = B[i_12, j_4]
C_val += (Main).f(0.0, val)
end
Expand Down
91 changes: 91 additions & 0 deletions docs/src/guides/sparse_utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Sparse and Structured Array Utilities

## Sparse Constructors

In addition to the `Tensor` constructor, Finch provides a number of convenience
constructors for common tensor types. For example, the `spzeros` and `sprand` functions
have `fspzeros` and `fsprand` counterparts that return Finch tensors. We can also construct
a sparse COO `Tensor` from a list of indices and values using the `fsparse` function.

```@docs
fsparse
fsparse!
fsprand
fspzeros
ffindnz
```

## Fill Values

Finch tensors support an arbitrary "background" value for sparse arrays. While most arrays use `0` as the background value, this is not always the case. For example, a sparse array of `Int` might use `typemin(Int)` as the background value. The `default` function returns the background value of a tensor. If you ever want to change the background value of an existing array, you can use the `redefault!` function. The `countstored` function returns the number of stored elements in a tensor, and calling `pattern!` on a tensor returns tensor which is true whereever the original tensor stores a value. Note that countstored doesn't always return the number of non-zero elements in a tensor, as it counts the number of stored elements, and stored elements may include the background value. You can call `dropdefaults!` to remove explicitly stored background values from a tensor.

```jldoctest example1; setup = :(using Finch)
julia> A = fsparse([1, 1, 2, 3], [2, 4, 5, 6], [1.0, 2.0, 3.0])
SparseCOO{2} (0.0) [:,1:6]
├─ [1, 2]: 1.0
├─ [1, 4]: 2.0
└─ [2, 5]: 3.0
julia> min.(A, -1)
Dense [:,1:6]
├─ [:, 1]: Dense [1:3]
│ ├─ [1]: -1.0
│ ├─ [2]: -1.0
│ └─ [3]: -1.0
├─ [:, 2]: Dense [1:3]
│ ├─ [1]: -1.0
│ ├─ [2]: -1.0
│ └─ [3]: -1.0
├─ [:, 3]: Dense [1:3]
│ ├─ [1]: -1.0
│ ├─ [2]: -1.0
│ └─ [3]: -1.0
├─ [:, 4]: Dense [1:3]
│ ├─ [1]: -1.0
│ ├─ [2]: -1.0
│ └─ [3]: -1.0
├─ [:, 5]: Dense [1:3]
│ ├─ [1]: -1.0
│ ├─ [2]: -1.0
│ └─ [3]: -1.0
└─ [:, 6]: Dense [1:3]
├─ [1]: -1.0
├─ [2]: -1.0
└─ [3]: -1.0
julia> default(A)
0.0
julia> B = redefault!(A, -Inf)
SparseCOO{2} (-Inf) [:,1:6]
├─ [1, 2]: 1.0
├─ [1, 4]: 2.0
└─ [2, 5]: 3.0
julia> min.(B, -1)
Sparse (-Inf) [:,1:6]
├─ [:, 2]: Sparse (-Inf) [1:3]
│ └─ [1]: -1.0
├─ [:, 4]: Sparse (-Inf) [1:3]
│ └─ [1]: -1.0
└─ [:, 5]: Sparse (-Inf) [1:3]
└─ [2]: -1.0
julia> countstored(A)
3
julia> pattern!(A)
SparseCOO{2} (false) [:,1:6]
├─ [1, 2]: true
├─ [1, 4]: true
└─ [2, 5]: true
```

```@docs
redefault!
pattern!
countstored
dropdefaults
dropdefaults!
```
5 changes: 1 addition & 4 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,13 @@ quote
s = (ex.bodies[1]).body.body.lhs.tns.bind
s_val = s.val
A = (ex.bodies[1]).body.body.rhs.tns.bind
sugar_1 = size(A)
sugar_1 = size((ex.bodies[1]).body.body.rhs.tns.bind)
A_mode1_stop = sugar_1[1]
A_mode2_stop = sugar_1[2]
@warn "Performance Warning: non-concordant traversal of A[i, j] (hint: most arrays prefer column major or first index fast, run in fast mode to ignore this warning)"
result = nothing
for i_3 = 1:A_mode1_stop
for j_3 = 1:A_mode2_stop
sugar_3 = size(A)
A_mode1_stop = sugar_3[1]
A_mode2_stop = sugar_3[2]
val = A[i_3, j_3]
s_val = val + s_val
end
Expand Down
2 changes: 1 addition & 1 deletion docs/src/interactive.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
{
"output_type": "execute_result",
"data": {
"text/plain": "quote\n y = (ex.bodies[1]).body.body.lhs.tns.bind\n A_lvl = ((ex.bodies[1]).body.body.rhs.args[1]).tns.bind.lvl\n A_lvl_2 = A_lvl.lvl\n A_lvl_ptr = A_lvl_2.ptr\n A_lvl_idx = A_lvl_2.idx\n A_lvl_2_val = A_lvl_2.lvl.val\n x = ((ex.bodies[1]).body.body.rhs.args[2]).tns.bind\n sugar_1 = size(y)\n y_mode1_stop = sugar_1[1]\n A_lvl_2.shape == y_mode1_stop || throw(DimensionMismatch(\"mismatched dimension limits ($(A_lvl_2.shape) != $(y_mode1_stop))\"))\n sugar_2 = size(x)\n x_mode1_stop = sugar_2[1]\n x_mode1_stop == A_lvl.shape || throw(DimensionMismatch(\"mismatched dimension limits ($(x_mode1_stop) != $(A_lvl.shape))\"))\n result = nothing\n for j_4 = 1:x_mode1_stop\n sugar_3 = size(x)\n x_mode1_stop = sugar_3[1]\n val = x[j_4]\n A_lvl_q = (1 - 1) * A_lvl.shape + j_4\n A_lvl_2_q = A_lvl_ptr[A_lvl_q]\n A_lvl_2_q_stop = A_lvl_ptr[A_lvl_q + 1]\n if A_lvl_2_q < A_lvl_2_q_stop\n A_lvl_2_i1 = A_lvl_idx[A_lvl_2_q_stop - 1]\n else\n A_lvl_2_i1 = 0\n end\n phase_stop = min(A_lvl_2.shape, A_lvl_2_i1)\n if phase_stop >= 1\n if A_lvl_idx[A_lvl_2_q] < 1\n A_lvl_2_q = Finch.scansearch(A_lvl_idx, 1, A_lvl_2_q, A_lvl_2_q_stop - 1)\n end\n while true\n A_lvl_2_i = A_lvl_idx[A_lvl_2_q]\n if A_lvl_2_i < phase_stop\n A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]\n y[A_lvl_2_i] = val * A_lvl_3_val + y[A_lvl_2_i]\n A_lvl_2_q += 1\n else\n phase_stop_3 = min(A_lvl_2_i, phase_stop)\n if A_lvl_2_i == phase_stop_3\n A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]\n y[phase_stop_3] = val * A_lvl_3_val + y[phase_stop_3]\n A_lvl_2_q += 1\n end\n break\n end\n end\n end\n end\n result = ()\n result\nend"
"text/plain": "quote\n y = (ex.bodies[1]).body.body.lhs.tns.bind\n sugar_1 = size((ex.bodies[1]).body.body.lhs.tns.bind)\n y_mode1_stop = sugar_1[1]\n A_lvl = ((ex.bodies[1]).body.body.rhs.args[1]).tns.bind.lvl\n A_lvl_2 = A_lvl.lvl\n A_lvl_ptr = A_lvl_2.ptr\n A_lvl_idx = A_lvl_2.idx\n A_lvl_2_val = A_lvl_2.lvl.val\n x = ((ex.bodies[1]).body.body.rhs.args[2]).tns.bind\n sugar_2 = size(((ex.bodies[1]).body.body.rhs.args[2]).tns.bind)\n x_mode1_stop = sugar_2[1]\n A_lvl_2.shape == y_mode1_stop || throw(DimensionMismatch(\"mismatched dimension limits ($(A_lvl_2.shape) != $(y_mode1_stop))\"))\n x_mode1_stop == A_lvl.shape || throw(DimensionMismatch(\"mismatched dimension limits ($(x_mode1_stop) != $(A_lvl.shape))\"))\n result = nothing\n for j_4 = 1:x_mode1_stop\n val = x[j_4]\n A_lvl_q = (1 - 1) * A_lvl.shape + j_4\n A_lvl_2_q = A_lvl_ptr[A_lvl_q]\n A_lvl_2_q_stop = A_lvl_ptr[A_lvl_q + 1]\n if A_lvl_2_q < A_lvl_2_q_stop\n A_lvl_2_i1 = A_lvl_idx[A_lvl_2_q_stop - 1]\n else\n A_lvl_2_i1 = 0\n end\n phase_stop = min(A_lvl_2.shape, A_lvl_2_i1)\n if phase_stop >= 1\n if A_lvl_idx[A_lvl_2_q] < 1\n A_lvl_2_q = Finch.scansearch(A_lvl_idx, 1, A_lvl_2_q, A_lvl_2_q_stop - 1)\n end\n while true\n A_lvl_2_i = A_lvl_idx[A_lvl_2_q]\n if A_lvl_2_i < phase_stop\n A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]\n y[A_lvl_2_i] = val * A_lvl_3_val + y[A_lvl_2_i]\n A_lvl_2_q += 1\n else\n phase_stop_3 = min(A_lvl_2_i, phase_stop)\n if A_lvl_2_i == phase_stop_3\n A_lvl_3_val = A_lvl_2_val[A_lvl_2_q]\n y[phase_stop_3] = val * A_lvl_3_val + y[phase_stop_3]\n A_lvl_2_q += 1\n end\n break\n end\n end\n end\n end\n result = ()\n result\nend"
},
"metadata": {},
"execution_count": 1
Expand Down
15 changes: 10 additions & 5 deletions src/Finch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export diagmask, lotrimask, uptrimask, bandmask, chunkmask
export scale, products, offset, permissive, protocolize, swizzle, toeplitz, window
export PlusOneVector

export choose, minby, maxby, overwrite, initwrite, d
export lazy, compute

export choose, minby, maxby, overwrite, initwrite, filterop, d

export default, AsArray

Expand Down Expand Up @@ -140,24 +142,27 @@ include("tensors/combinators/swizzle.jl")
include("tensors/combinators/scale.jl")
include("tensors/combinators/product.jl")

include("traits.jl")

export fsparse, fsparse!, fsprand, fspzeros, ffindnz, fread, fwrite, countstored

export bspread, bspwrite
export ftnsread, ftnswrite, fttread, fttwrite

export moveto, postype

include("FinchLogic/FinchLogic.jl")
using .FinchLogic
include("interface/traits.jl")
include("interface/abstractarrays.jl")
include("interface/abstractunitranges.jl")
include("interface/broadcast.jl")
include("interface/index.jl")
include("interface/mapreduce.jl")
include("interface/compare.jl")
include("interface/copy.jl")
include("interface/fsparse.jl")
include("interface/fileio/fileio.jl")
include("interface/compute.jl")
include("interface/lazy.jl")
include("interface/eager.jl")


@static if !isdefined(Base, :get_extension)
function __init__()
Expand Down
Loading

2 comments on commit d379410

@willow-ahrens
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/102495

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.6.15 -m "<description of version>" d379410a50c8baff87557d2b12b0670ceb4cc1f9
git push origin v0.6.15

Please sign in to comment.