OMEinsum

repository·master·Indexed 42 days ago

https://github.com/under-peter/omeinsum.jl

A high-performance Julia implementation of Einstein summation (einsum) optimized for large-scale tensor network contractions. It features compile-time optimization via the @ein_str and @ein macros, supports CUDA acceleration through CUBLAS and cuTENSOR backends, and provides tools for optimizing contraction order with optimize_code. OMEinsum is compatible with automatic differentiation via Zygote.jl and includes a built-in cost_and_gradient function for efficient reverse-mode AD.

Tokens
5.9K
Snippets
27
Records
31
Agent score
52%

What's inside OMEinsum

  1. Define einsum operations using EinCode or string literals

    master

    You can specify tensor operations using either the EinCode object or the @ein_str string literal. Both methods are equivalent and allow you to define the index mapping for contractions.

    Use EinCode when you want to programmatically construct the index mapping. Use the string literal (via the ein"..." syntax) for a more concise, readable notation.

    using OMEinsum
    
    # Using the string literal
    code1 = ein"ij,jk -> ik"
    
    # Using the EinCode object
    ixs = [[1, 2], [2, 3]]
    iy = [1, 3]
    code2 = EinCode(ixs, iy)
  2. Compare OMEinsum GPU backends

    master

    OMEinsum provides two distinct backends for GPU tensor contractions via CUDA:

    BackendLibraryBest For
    DefaultBackend()CUBLASMatrix-like contractions (GEMM patterns) like ein"ij,jk->ik" or batched matmul.
    CuTensorBackend()cuTENSORGeneral, high-dimensional, or non-GEMM tensor contractions like ein"ijk,jkl->il".

    DefaultBackend works by reshaping tensors into matrix multiplication (GEMM) formats, while CuTensorBackend provides native tensor contraction without reshape/permute overhead.

  3. Understand Einsum notation for tensor contractions

    master

    Einsum notation is a compact string-based way to specify tensor contractions. In this notation:

    • Each character represents an index (subscript).
    • Input tensors are separated by commas (,).
    • The input and output tensors are separated by an arrow (->).

    To perform a contraction, the notation identifies which indices are shared across input tensors (which are summed over) and which indices appear in the output tensor.

    Common Einsum Patterns

    notationmeaning
    ij,jk->ikmatrix-matrix multiplication
    ijl,jkl->iklbatched matrix-matrix multiplication
    ij,j->imatrix-vector multiplication
    ij,ik,il->jklstar contraction
    ii->trace
    ij->isum
    ii->itake the diagonal part of a matrix
    ijkl->ilkjpermute the dimensions of a tensor
    i->iiconstruct a diagonal matrix
    ->iibroadcast a scalar to the diagonal part of a matrix
    ij,ij->ijelement-wise product
    ij,kl->ijklouter product
    // Example of the logic behind einsum notation:
    Let A, B, C, ... be input tensors, O be the output tensor
    for indices in domain_of_unique_indices(einsum_notation)
        O[indices in O] += A[indices in A] * B[indices in B] * ...
    end
  4. Manually specify contraction order with brackets

    master

    Since v0.4, OMEinsum no longer optimizes the contraction order automatically. To ensure efficiency or to help the engine select specific kernels, you must specify the order manually using brackets in the string or macro syntax.

    String style: ein"is, (oij, js) -> os"(x, W, y)

    Macro style: @ein Z[o,s] := x[i,s] * (W[o,i,j] * y[j,s]);

    If you do not provide brackets, the package may fall back to loop_einsum, which is slower. You can use allow_loops(false) to force an error if a fallback is used, helping you identify where manual ordering is needed.

    # Manual ordering via brackets
    Z = ein"is, (oij, js) -> os"(x, W, y);
    
    # Enforce manual ordering by disabling loop fallback
    allow_loops(false);
    Zl = ein"is, oij, js -> os"(x, W, y); # This will throw an error
  5. Set up and use the cuTENSOR backend

    master

    The CuTensorBackend uses NVIDIA's cuTENSOR library for native tensor contractions.

    Prerequisites

    • NVIDIA GPU with compute capability $\ge$ 6.0.
    • CUDA.jl v5.0 or later.
    • cuTENSOR.jl package installed (Pkg.add("cuTENSOR")).

    Critical Requirement: Loading cuTENSOR

    You must load the cuTENSOR package (e.g., using cuTENSOR) alongside or before OMEinsum. This triggers the CuTENSORExt extension. If you set the backend without loading the package, OMEinsum will issue a warning and fall back to DefaultBackend (CUBLAS).

    Usage

    Use set_einsum_backend!(CuTensorBackend()) to enable it globally.

    using CUDA
    using cuTENSOR  # Required to enable the extension
    using OMEinsum
    
    # Enable cuTENSOR globally
    set_einsum_backend!(CuTensorBackend())
    
    # Perform contraction
    A = CUDA.rand(Float32, 64, 64, 64)
    B = CUDA.rand(Float32, 64, 64, 64)
    result = ein"ijk,jkl->il"(A, B)
    
    # Reset to default
    set_einsum_backend!(DefaultBackend())
  6. Configure GPU backends for CUDA acceleration

    master

    OMEinsum supports CUDA GPU acceleration via two distinct backends. Choosing the right one depends on your contraction pattern:

    1. DefaultBackend() (CUBLAS): Best for standard matrix-like (GEMM) contractions.
    2. CuTensorBackend() (cuTENSOR): Best for general tensor contractions and non-GEMM patterns (e.g., tensor networks). It provides native contraction without reshape/permute overhead.

    To switch backends, use set_einsum_backend!().

    Note: Using CuTensorBackend requires the cuTENSOR package (which provides the CuTENSORExt extension).

    using CUDA
    using cuTENSOR
    using OMEinsum
    
    # Switch to cuTENSOR for general tensor contractions
    set_einsum_backend!(CuTensorBackend())
    C = ein"ijk,jkl->il"(A, B)
  7. Optimize contraction order for N-ary operations

    master

    When performing contractions with more than two input tensors, the standard @ein_str macro does not optimize the contraction order. To ensure efficient computation for large N-ary contractions, use one of the following:

    1. optein"..." string literal: Automatically optimizes the contraction order without requiring explicit tensor sizes.
    2. Manual specification: Use ein"(indices),(indices)->output" to manually specify the contraction grouping (e.g., grouping specific tensors together).
    using OMEinsum
    tensors = [randn(100, 100) for _ in 1:4]
    
    # Optimized contraction (automatic)
    optein"ij,jk,kl,lm->im"(tensors...)
    
    # Manually specified contraction order
    ein"(ij,jk),(kl,lm)->im"(tensors...)
  8. Optimize contraction order using `optimize_code`

    master

    When using the @ein_str literal for more than two input tensors, the contraction order is not automatically optimized. To optimize the order, you can construct an EinCode object and pass it to optimize_code.

    This function requires a size_dict (mapping labels to their sizes) and an optimization algorithm. A common choice is TreeSA, a local search algorithm. The result is a binary contraction tree of type SlicedEinsum or NestedEinsum.

    using OMEinsum, OMEinsumContractionOrders, OMEinsumContractionOrders.Graphs
    
    # 1. Construct an EinCode from a graph
    graph = random_regular_graph(20, 3; seed=42)
    code = EinCode([[e.src, e.dst] for e in edges(graph)], Int[])
    
    # 2. Define tensor sizes
    size_dict = uniformsize(code, 2)
    
    # 3. Optimize the contraction order
    optcode = optimize_code(code, size_dict, TreeSA(ntrials=1))
  9. Quick start with OMEinsum.jl

    master

    To use OMEinsum.jl, import the package and use the ein macro to define your tensor contraction operation. You can then use optimize_code to find an efficient contraction order using a specific optimizer (like TreeSA()) and a size hint (via uniformsize). The resulting optimized code object can be called like a function with the input tensors as arguments.

    using OMEinsum
    code = ein"ij,jk,kl,lm->im" # define the einsum operation
    optcode = optimize_code(code, uniformsize(code, 100), TreeSA())  # optimize the contraction order
    optcode(randn(100, 100), randn(100, 100), randn(100, 100), randn(100, 100))  # compute the result
  10. Accelerate einsum operations with CUDA

    master

    You can accelerate tensor contractions by uploading your data to the GPU using CUDA.jl. Once data is on the GPU (e.g., as a CuArray), OMEinsum will perform the contraction on the device. For optimal performance, use CUDA.@sync when benchmarking or executing operations to ensure synchronization.

    using CUDA, OMEinsum
    
    # Prepare data
    A, B = rand(1000, 1000), rand(1000, 1000)
    code = ein"ij,jk->ik"
    
    # Upload to GPU
    cuA, cuB = CuArray(A), CuArray(B)
    
    # Execute on GPU
    result = CUDA.@sync code(cuA, cuB)
  11. Reduce memory usage with `slice_code`

    master

    If the memory usage (space complexity) of a contraction is too high, you can use slicing to reduce it. Slicing is performed using the slice_code function and a slicer object (e.g., TreeSASlicer).

    Slicing involves a trade-off: it reduces space complexity at the cost of increasing time complexity.

    # Assuming optcode and size_dict are already defined
    slicer = TreeSASlicer(score=ScoreFunction(sc_target=2))
    scode = slice_code(optcode, size_dict, slicer)
    
    # scode is a SlicedEinsum object