Getting Started

Tensor Formats

Creating Tensors

You can create Finch tensors using the Tensor constructor, which closely follows the Array constructor syntax. The first argument specifies the storage format.

julia> A = Tensor(CSCFormat(), 4, 3);

julia> B = Tensor(COOFormat(2), A);

Some pre-defined formats include:

SignatureDescription
DenseFormat(N, z = 0.0, T = typeof(z))A dense format with a fill value of z.
CSFFormat(N, z = 0.0, T = typeof(z))An N-dimensional CSC format for sparse tensors.
CSCFormat(z = 0.0, T = typeof(z))A 2D CSC format storing matrices as dense lists.
DCSFFormat(N, z = 0.0, T = typeof(z))A DCSF format storing tensors as nested lists.
HashFormat(N, z = 0.0, T = typeof(z))A hash-table-based format for sparse data.
ByteMapFormat(N, z = 0.0, T = typeof(z))A byte-map-based format for compact storage.
DCSCFormat(z = 0.0, T = typeof(z))A 2D DCSC format storing matrices as lists.
COOFormat(N, T = Float64, z = zero(T))An N-dimensional COO format for coordinate lists.

It is also possible to build custom formats using the interface, as described in the Tensor Formats section.

High-Level Array API

Basic Array Operations

Finch tensors support indexing, slicing, mapping, broadcasting, and reducing. Many functions in the Julia standard array library are supported.

julia> A = Tensor(CSCFormat(), [0 1; 2 3]);

julia> B = A .+ 1
2×2 Tensor{DenseLevel{Int64, DenseLevel{Int64, ElementLevel{1.0, Float64, Int64, Vector{Float64}}}}}:
 1.0  2.0
 3.0  4.0

julia> C = max.(A, B)
2×2 Tensor{DenseLevel{Int64, DenseLevel{Int64, ElementLevel{1.0, Float64, Int64, Vector{Float64}}}}}:
 1.0  2.0
 3.0  4.0

julia> D = sum(C; dims=2)
2 Tensor{DenseLevel{Int64, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}:
 3.0
 7.0

julia> E = B[1, :]
2 Tensor{DenseLevel{Int64, ElementLevel{1.0, Float64, Int64, Vector{Float64}}}}:
 1.0
 2.0

For situations which are difficult to express in the julia standard library, Finch also supports an @einsum syntax:

julia> @einsum F[i, j, k] *= A[i, j] * B[j, k]
2×2×2 Tensor{DenseLevel{Int64, DenseLevel{Int64, SparseDictLevel{Int64, Vector{Int64}, Vector{Int64}, Vector{Int64}, Dict{Tuple{Int64, Int64}, Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}}:
[:, :, 1] =
 0.0  3.0
 2.0  9.0

[:, :, 2] =
 0.0   4.0
 4.0  12.0

julia> @einsum G[j, k] << max >>= A[i, j] + B[j, k]
2×2 Tensor{DenseLevel{Int64, DenseLevel{Int64, ElementLevel{-Inf, Float64, Int64, Vector{Float64}}}}}:
 3.0  4.0
 6.0  7.0

The @einsum macro is a powerful tool for expressing complex array operations concisely.

Array Fusion

To get the full benefits of a sparse compiler, it is critical to fuse certain operations together. For this, Finch exposes two functions, lazy and compute. The lazy function creates a lazy tensor, which is a symbolic representation of the computation. The compute function evaluates the computation. For convenience, you may wish to use the fused function, which automatically fuses the computations it contains.

julia> A = fsparse([1, 1, 2, 3], [2, 4, 5, 6], [1.0, 2.0, 3.0])
3×6 Tensor{SparseCOOLevel{2, Tuple{Int64, Int64}, Vector{Int64}, Tuple{Vector{Int64}, Vector{Int64}}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}:
 0.0  1.0  0.0  2.0  0.0  0.0
 0.0  0.0  0.0  0.0  3.0  0.0
 0.0  0.0  0.0  0.0  0.0  0.0

julia> B = A .* 2
3×6 Tensor{SparseDictLevel{Int64, Vector{Int64}, Vector{Int64}, Vector{Int64}, Dict{Tuple{Int64, Int64}, Int64}, Vector{Int64}, SparseDictLevel{Int64, Vector{Int64}, Vector{Int64}, Vector{Int64}, Dict{Tuple{Int64, Int64}, Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  2.0  0.0  4.0  0.0  0.0
 0.0  0.0  0.0  0.0  6.0  0.0
 0.0  0.0  0.0  0.0  0.0  0.0

julia> C = lazy(A)
?×?-LazyTensor{Float64}

julia> D = lazy(B)
?×?-LazyTensor{Float64}

julia> E = (C .+ D) ./ 2
?×?-LazyTensor{Float64}

julia> compute(E)
3×6 Tensor{SparseDictLevel{Int64, Vector{Int64}, Vector{Int64}, Vector{Int64}, Dict{Tuple{Int64, Int64}, Int64}, Vector{Int64}, SparseDictLevel{Int64, Vector{Int64}, Vector{Int64}, Vector{Int64}, Dict{Tuple{Int64, Int64}, Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  1.5  0.0  3.0  0.0  0.0
 0.0  0.0  0.0  0.0  4.5  0.0
 0.0  0.0  0.0  0.0  0.0  0.0

The lazy and compute functions allow the compiler to fuse operations together, resulting in asymptotically more efficient code.

julia> using BenchmarkTools

julia> A = fsprand(1000, 1000, 100);
       B = Tensor(rand(1000, 1000));
       C = Tensor(rand(1000, 1000));

julia> @btime A .* (B * C);
  145.940 ms (859 allocations: 7.69 MiB)

julia> @btime compute(lazy(A) .* (lazy(B) * lazy(C)));
  694.666 μs (712 allocations: 60.86 KiB)

Different optimizers can be used with compute, such as the state-of-the-art Galley optimizer, which can adapt to the sparsity patterns of the inputs.

julia> A = fsprand(1000, 1000, 0.1);
       B = fsprand(1000, 1000, 0.1);
       C = fsprand(1000, 1000, 0.0001);

julia> A = lazy(A);
       B = lazy(B);
       C = lazy(C);

julia> @btime compute(sum(A * B * C));
  282.503 ms (1018 allocations: 184.43 MiB)

julia> @btime compute(sum(A * B * C), ctx=galley_scheduler());
  152.792 μs (672 allocations: 28.81 KiB)

Sparse and Structured Utilities

Sparse Constructors

fsparse constructs a tensor from lists of nonzero coordinates. For example,

julia> A = fsparse([1, 2, 3], [2, 3, 4], [1.0, 2.0, 3.0])
3×4 Tensor{SparseCOOLevel{2, Tuple{Int64, Int64}, Vector{Int64}, Tuple{Vector{Int64}, Vector{Int64}}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}:
 0.0  1.0  0.0  0.0
 0.0  0.0  2.0  0.0
 0.0  0.0  0.0  3.0

The inverse of fsparse is ffindnz, which returns a list of nonzero coordinates in a tensor.

julia> ffindnz(A)
([1, 2, 3], [2, 3, 4], [1.0, 2.0, 3.0])

Random Sparse Tensors

The fsprand constructs a random sparse tensor with a specified sparsity or number of nonzeros:

julia> A = fsprand(5, 5, 0.1)

Fill Values

Fill values represent the background value of a sparse tensor. Usually, this value is zero, but some applications may choose to use other fill values as fits their application. Only values which are not equal to the fill value are stored

julia> t = Tensor(Dense(SparseList(Element(0.0))), 3, 3)
3×3 Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> fill_value(t)
0.0

julia> set_fill_value!(t, -1.0)
3×3 Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{-1.0, Float64, Int64, Vector{Float64}}}}}:
 -1.0  -1.0  -1.0
 -1.0  -1.0  -1.0
 -1.0  -1.0  -1.0

julia> countstored(t)
0

julia> countstored(dropfills(t))
0

Empty Tensors

The Tensor constructor initializes tensors to their fill value when given a list of dimensions, but you can also use fspzeros for an empty COO Tensor, for consistency with MATLAB.

julia> A = fspzeros(3, 3)
3×3 Tensor{SparseCOOLevel{2, Tuple{Int64, Int64}, Vector{Int64}, Tuple{Vector{Int64}, Vector{Int64}}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> B = Tensor(CSCFormat(1.0), 3, 3)
3×3 Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{1.0, Float64, Int64, Vector{Float64}}}}}:
 1.0  1.0  1.0
 1.0  1.0  1.0
 1.0  1.0  1.0

Converting Between Formats

You can convert between tensor formats with the Tensor constructor. Simply construct a new Tensor in the desired format and

julia> A = Tensor(CSCFormat(), [0 0 2 1; 0 0 1 0; 1 0 0 0])
3×4 Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  0.0  2.0  1.0
 0.0  0.0  1.0  0.0
 1.0  0.0  0.0  0.0

julia> B = Tensor(DCSCFormat(), A)
3×4 Tensor{SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  0.0  2.0  1.0
 0.0  0.0  1.0  0.0
 1.0  0.0  0.0  0.0

Storage Order

By default, tensors in Finch are column-major. However, you can use the swizzle function to transpose them lazily. To convert to a transposed format, use the dropfills! function.

julia> A = Tensor(CSCFormat(), [0 0 2 1; 0 0 1 0; 1 0 0 0])
3×4 Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}:
 0.0  0.0  2.0  1.0
 0.0  0.0  1.0  0.0
 1.0  0.0  0.0  0.0

julia> swizzle(A, 2, 1)
4×3 Finch.SwizzleArray{(2, 1), Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}}:
 0.0  0.0  1.0
 0.0  0.0  0.0
 2.0  1.0  0.0
 1.0  0.0  0.0

julia> dropfills!(swizzle(Tensor(CSCFormat()), 2, 1), A)
3×4 Finch.SwizzleArray{(2, 1), Tensor{DenseLevel{Int64, SparseListLevel{Int64, Vector{Int64}, Vector{Int64}, ElementLevel{0.0, Float64, Int64, Vector{Float64}}}}}}:
 0.0  0.0  2.0  1.0
 0.0  0.0  1.0  0.0
 1.0  0.0  0.0  0.0

File I/O

Reading and Writing Files

Finch supports multiple formats, such as .bsp, .mtx, and .tns. Use fread and fwrite to read and write tensors.

julia> fwrite("tensor.bsp", A)

julia> B = fread("tensor.bsp")