Finch Language
Writing Finch language directly is a powerful way to express complex array operations. Finch supports complex optimizations under the hood, including conditionals, multiple outputs, and even user-defined types and functions.
Supported Syntax and Structures
| Feature/Structure | Example Usage |
|---|---|
| Major Sparse Formats and Structured Arrays | A = Tensor(Dense(SparseList(Element(0.0)), 3, 4) |
| Background Values Other Than Zero | B = Tensor(SparseList(Element(1.0)), 9) |
| Broadcasts and Reductions | sum(A .* B) |
| User-Defined Functions | x[] <<min>>= y[i] + z[i] |
| Multiple Outputs | x[] <<min>>= y[i]; z[] <<max>>= y[i] |
| Multicore Parallelism | for i = parallel(1:100) |
| Conditionals | if dist[] < best_dist[] |
| Affine Indexing (e.g. Convolution) | A[i + j] |
Quick Start: Examples
To begin, the following program sums the rows of a sparse matrix:
using Finch, SparseArrays
A = sprand(5, 5, 0.5)
y = zeros(5)
@finch begin
y .= 0
for i in _, j in _
y[i] += A[i, j]
end
endThe @finch macro takes a block of code, and compiles it using the sparsity attributes of the arguments. In this case, A is a sparse matrix, so the compiler generates a sparse loop nest. The compiler takes care of applying rules like x * 0 => 0 during compilation to make the code more efficient.
You can call @finch on any loop program, but it will only generate sparse code if the arguments are sparse. For example, the following program calculates the sum of the elements of a dense matrix:
using Finch
A = rand(5, 5)
s = Scalar(0.0)
@finch begin
s .= 0
for i in _, j in _
s[] += A[i, j]
end
endYou can call @finch_code to see the generated code (since A is dense, the code is dense):
julia> @finch_code for i in _, j in _
s[] += A[i, j]
end
quote
s_data = (ex.bodies[1]).body.body.lhs.tns.bind
s_val = s_data.val
A_data = (ex.bodies[1]).body.body.rhs.tns.bind
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)"
for i_3 = 1:A_mode1_stop
for j_3 = 1:A_mode2_stop
val = A_data[i_3, j_3]
s_val = val + s_val
end
end
result = ()
s_data.val = s_val
result
endCalculating Sparse Vector Statistics
Here, we write a Julia program using Finch to compute the minimum, maximum, sum, and variance of a sparse vector. This program efficiently reads the vector once, focusing only on nonzero values.
using Finch
X = Tensor(SparseList(Element(0.0)), fsprand(10, 0.5))
x_min = Scalar(Inf)
x_max = Scalar(-Inf)
x_sum = Scalar(0.0)
x_var = Scalar(0.0)
@finch begin
for i in _
let x = X[i]
x_min[] << min >>= x
x_max[] << max >>= x
x_sum[] += x
x_var[] += x * x
end
end
end;Sparse Matrix-Vector Multiplication
As a more traditional example, what follows is a sparse matrix-vector multiplication using a column-major approach.
x = Tensor(Dense(Element(0.0)), rand(42));
A = Tensor(Dense(SparseList(Element(0.0))), fsprand(42, 42, 0.1));
y = Tensor(Dense(Element(0.0)));
@finch begin
y .= 0
for j in _, i in _
y[i] += A[i, j] * x[j]
end
endMore examples are given in the examples directory.
Usage
General Purpose (@finch)
Most users will want to use the @finch macro, which executes the given program immediately in the given scope. The program will be JIT-compiled on the first call to @finch with the given array argument types. If the array arguments to @finch are type stable, the program will be JIT-compiled when the surrounding function is compiled.
Very often, the best way to inspect Finch compiler behavior is through the @finch_code macro, which prints the generated code instead of executing it.
Finch.@finch — Macro@finch [options...] prgmRun a finch program prgm. The syntax for a finch program is a set of nested loops, statements, and branches over pointwise array assignments. For example, the following program computes the sum of two arrays A = B + C:
@finch begin
A .= 0
for i = _
A[i] = B[i] + C[i]
end
return A
endFinch programs are composed using the following syntax:
arr .= 0: an array declaration initializing arr to zero.arr[inds...]: an array access, the array must be a variable and each index may be another finch expression.x + y,f(x, y): function calls, wherexandyare finch expressions.arr[inds...] = ex: an array assignment expression, settingarr[inds]to the value ofex.arr[inds...] += ex: an incrementing array expression, addingextoarr[inds].*, &, |, are supported.arr[inds...] <<min>>= ex: a incrementing array expression with a custom operator, e.g.<<min>>is the minimum operator.for i = _ body end: a loop over the indexi, where_is computed from array access withiinbody.if cond body end: a conditional branch that executes only iterations wherecondis true.return (tnss...,): at global scope, exit the program and return the tensorstnsswith their new dimensions. By default, any tensor declared in global scope is returned.
Symbols are used to represent variables, and their values are taken from the environment. Loops introduce index variables into the scope of their bodies.
Finch uses the types of the arrays and symbolic analysis to discover program optimizations. If B and C are sparse array types, the program will only run over the nonzeros of either.
Semantically, Finch programs execute every iteration. However, Finch can use sparsity information to reliably skip iterations when possible.
options are optional keyword arguments:
algebra: the algebra to use for the program. The default isDefaultAlgebra().mode: the optimization mode to use for the program. Possible modes are::debug: run the program in debug mode, with bounds checking and better error handling.:safe: run the program in safe mode, with modest checks for performance and correctness.:fast: run the program in fast mode, with no checks or warnings, this mode is for power users.
:safe.
See also: @finch_code
Finch.@finch_code — Macro@finch_code [options...] prgm
Return the code that would be executed in order to run a finch program prgm.
See also: @finch
Ahead Of Time (@finch_kernel)
While @finch is the recommended way to use Finch, it is also possible to run finch ahead-of-time. The @finch_kernel macro generates a function definition ahead-of-time, which can be evaluated and then called later.
There are several reasons one might want to do this:
- If we want to make tweaks to the Finch implementation, we can directly modify the source code of the resulting function.
- When benchmarking Finch functions, we can easily and reliably ensure the benchmarked code is inferrable.
- If we want to use Finch to generate code but don't want to include Finch as a dependency in our project, we can use
@finch_kernelto generate the functions ahead of time and copy and paste the generated code into our project. Consider automating this workflow to keep the kernels up to date!
Finch.@finch_kernel — Macro@finch_kernel [options...] fname(args...) = prgmReturn a definition for a function named fname which executes @finch prgm on the arguments args. args should be a list of variables holding representative argument instances.
See also: @finch
As an example, the following code generates an spmv kernel definition, evaluates the definition, and then calls the kernel several times.
let
A = Tensor(Dense(SparseList(Element(0.0))))
x = Tensor(Dense(Element(0.0)))
y = Tensor(Dense(Element(0.0)))
def = @finch_kernel function spmv(y, A, x)
y .= 0.0
for j in _, i in _
y[i] += A[i, j] * x[j]
end
return y
end
eval(def)
end
function main()
for i in 1:10
A2 = Tensor(Dense(SparseList(Element(0.0))), fsprand(10, 10, 0.1))
x2 = Tensor(Dense(Element(0.0)), rand(10))
y2 = Tensor(Dense(Element(0.0)))
spmv(y2, A2, x2)
end
end
main()