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/StructureExample Usage
Major Sparse Formats and Structured ArraysA = Tensor(Dense(SparseList(Element(0.0)), 3, 4)
Background Values Other Than ZeroB = Tensor(SparseList(Element(1.0)), 9)
Broadcasts and Reductionssum(A .* B)
User-Defined Functionsx[] <<min>>= y[i] + z[i]
Multiple Outputsx[] <<min>>= y[i]; z[] <<max>>= y[i]
Multicore Parallelismfor i = parallel(1:100)
Conditionalsif 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
A = sprand(5, 5, 0.5)
y = zeros(5)
@finch begin
    y .= 0
    for i in _, j in _
        y[i] += A[i, j]
    end
end

The @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
end

You 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
end

Calculating 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
end

More 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.@finchMacro
@finch [options...] prgm

Run 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
end

Finch 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, where x and y are finch expressions.
  • arr[inds...] = ex: an array assignment expression, setting arr[inds] to the value of ex.
  • arr[inds...] += ex: an incrementing array expression, adding ex to arr[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 index i, where _ is computed from array access with i in body.
  • if cond body end: a conditional branch that executes only iterations where cond is true.
  • return (tnss...,): at global scope, exit the program and return the tensors tnss with 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 is DefaultAlgebra().
  • 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.
    The default is :safe.

See also: @finch_code

source
Finch.@finch_codeMacro

@finch_code [options...] prgm

Return the code that would be executed in order to run a finch program prgm.

See also: @finch

source

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:

  1. If we want to make tweaks to the Finch implementation, we can directly modify the source code of the resulting function.
  2. When benchmarking Finch functions, we can easily and reliably ensure the benchmarked code is inferrable.
  3. 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_kernel to 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_kernelMacro
@finch_kernel [options...] fname(args...) = prgm

Return 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

source

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()