Arraymancer

repository·master·Indexed 23 days ago

https://github.com/mratsim/arraymancer

A Nim-based library for high-performance tensor operations. It provides support for CPU, CudaTensor, and ClTensor backends, featuring Autograd for neural networks, strided parallel iteration via OpenMP, and a comprehensive set of linear algebra operators. Key features include implicit and explicit broadcasting, universal functions (ufuncs), and flexible tensor initialization and manipulation.

Tokens
6K
Snippets
23
Records
39
Agent score
80%

What's inside arraymancer

  1. How tensor printing works for high-dimensional arrays

    master

    Arraymancer supports pretty-printing tensors of arbitrary dimensionality. The representation follows a pattern based on whether the dimension rank is odd or even:

    • Odd dimensions (e.g., rank 1, 3, 5): Represented as horizontal stacks of the pretty-printed (N-1) dimension.
    • Even dimensions (e.g., rank 2, 4): Represented as vertical stacks of the pretty-printed (N-1) dimension.

    To improve readability, separators (| and -) are used between stacks, and indices for each dimension are printed at the top (for odd dimensions) or left (for even dimensions) of the layer.

    import arraymancer
    
    let t1 = toSeq(1..144).toTensor().reshape(2,3,4,3,2)
    echo t1
  2. Leverage OpenMP for parallel execution

    master

    Most core operations in Arraymancer are parallelized using OpenMP. This includes:

    • Linear algebra functions
    • Universal functions
    • map, reduce, and fold based operations (including their _inline variants)

    Using the _inline variants (like map_inline or apply_inline) ensures that these parallelized operations are also fused, preventing the overhead of multiple parallel passes and intermediate memory allocations.

  3. Implement the tensor interface for strided parallel iteration

    master

    To use the forEach macro for strided parallel iteration, your tensor type must implement a specific interface. The implementation is generic and works on any type that provides the following routines or fields:

    • rank: Returns an int representing the number of dimensions.
    • size: Returns an int representing the total number of elements.
    • shape: Returns an array, seq, or indexable container supporting [] (read-only).
    • strides: Returns an array, seq, or indexable container supporting [] (read-only).
    • unsafe_raw_offset: Returns a pointer UncheckedArray[T] or a type with [] indexing. This must return the address of the start of the raw data, including any tensor offset (e.g., the address of x[0, 0, 0, ...]). This requires mutable access for var tensors.
    • is_C_contiguous: A routine used by the forEach macro to dispatch between forEachContiguous and forEachStrided implementations.

    Important Semantics:

    • The input data storage backend must be shallow copied on assignment (reference semantics).
    • The macro works on aliases to ensure that if a tensor is a result of another routine (like a slice), that routine is only called once (e.g., x[0..<2, _] will not slice x multiple times).
  4. Copying tensors and memory sharing

    master
    Warning: By default, assigning one tensor to another (e.g., let b = a) does not perform a deep copy. Instead, both variables will share the same underlying data. Modifying one will modify the other. To create a completely independent copy, you must explicitly use the clone function.
  5. Optimize performance with YOLO (You Only Loop Once) via inline operations

    master

    Arraymancer uses the YOLO™ (You Only Loop Once) paradigm to avoid the performance penalties of multiple passes over data. Standard broadcasting (e.g., using /. and +.) can result in multiple implicit loops and the creation of temporary tensors, leading to $O(n)$ complexity increases (e.g., $O(4n)$ for a 4-step operation).

    To achieve maximum efficiency, use the inline versions of mapping and applying functions. These constructs fuse multiple operations into a single loop, reducing memory bandwidth bottlenecks and avoiding intermediate allocations.

  6. Create universal functions with makeUniversal and makeUniversalLocal

    master

    Arraymancer allows you to turn unary functions (functions that operate on a single element) into universal functions that can be applied to entire tensors, similar to NumPy's ufuncs.

    There are two primary ways to do this:

    • makeUniversal(func): Creates a universal function from a unary function and exports it so it can be imported elsewhere.
    • makeUniversalLocal(func): Creates a universal function but does not export it.

    For example, many functions from the standard math module can be generalized to tensors using makeUniversal(sin).

  7. Understand Tensor properties

    master

    Tensors in Arraymancer have several key properties that define their structure and memory layout:

    • rank: The number of dimensions (0 for scalar, 1 for vector, 2 for matrices, N for N-dimensional arrays).
    • shape: A sequence representing the dimensions along each axis.
    • strides: A sequence of numbers indicating the steps required to reach the next item along a specific dimension in memory.
    • offset: The index of the first element of the tensor.

    Note: Scalars (rank 0) cannot be stored directly as tensors.

    import arraymancer
    
    let d = [[1, 2, 3], [4, 5, 6]].toTensor()
    
    echo d.rank     # 2
    echo d.shape    # @[2, 3]
    echo d.strides  # @[3, 1]
    echo d.offset   # 0
  8. Slicing syntax in Arraymancer

    master

    Arraymancer supports a wide range of slicing syntaxes for selecting dimension subsets, whole dimensions, stepping, reversing, and counting from the end.

    Key syntax patterns:

    • Standard slice: start..end (inclusive of both ends).
    • Exclusive slice: start..<end (excludes the end index).
    • Span slice: _ (equivalent to _.._) selects all items in a dimension.
    • Partial span slice: start.._ (from start to the end) or _..end (from the beginning to end).
    • Counting from end: ^n (e.g., ^3 is the 3rd element from the end). Note that ^1 points to the last element.
    • Stepping: start..end|step (e.g., |2 takes every second element).
    • Negative steps: start..end|-step (requires start > end).
    • Reversing: _|-1 is the easiest way to reverse a tensor dimension.
  9. Use boolean masks for selection and mutation

    master

    Boolean masks allow you to select or mutate elements based on a condition.

    • Selection: Using a boolean mask (e.g., foo[foo >. 27]) returns a flat, 1-D tensor containing only the elements that satisfy the condition.
    • Mutation: Using a boolean mask for assignment (e.g., foo[condition] = value) performs an in-place mutation on the original tensor, preserving its original shape.
    import arraymancer
    
    # Selection (returns 1D tensor)
    echo foo[foo >. 27]
    
    # Mutation (in-place, preserves shape)
    foo[foo >. 27] = -arange(9)
  10. Mutate slices in Arraymancer

    master

    You can mutate slices of a tensor using several types of values:

    • Single value: Assigns the value to all elements in the slice.
    • Nested array or seq: Assigns elements from the nested structure to the slice.
    • Tensor or tensor slice: Assigns elements from another tensor or slice to the target slice.

    Warning on In-place Mutation: Slice mutations happen on the same memory in real-time. If the source (RHS) and target (LHS) of a mutation overlap, the results might be unexpected because the mutation happens incrementally. To avoid side effects from overlapping memory, consider making a copy of the source tensor first.

    import arraymancer
    
    var foo = vandermonde(arange(1, 6), arange(1, 6)).asType(int)
    
    # Mutation with a single value
    foo[1..2, 3..4] = 999
    
    # Mutation with nested array or nested seq
    foo[0..1,0..1] = [[111, 222], [333, 444]]
    
    # Mutation with a tensor or tensor slice
    foo[^2..^1,2..4] = foo[^1..^2|-1, 4..2|-1]
  11. Perform implicit broadcasting with dot-prefixed operators

    master

    Arraymancer supports implicit broadcasting through element-wise operations that begin with a dot. These operations automatically handle broadcasting between operands of different shapes.

    Supported implicit broadcasting operators:

    • +.: Element-wise addition
    • -.: Element-wise subtraction
    • *.: Element-wise matrix multiplication (Hadamard product)
    • ./: Element-wise division or integer-division

    Note: For in-place versions (+.=, -.=, *.=, ./=), only the right-hand operand is broadcastable.

    let j = [0, 10, 20, 30].toTensor.reshape(4,1)
    let k = [0, 1, 2].toTensor.reshape(1,3)
    
    echo j +. k
    # Result is a Tensor[int] of shape "[4, 3]"
    # |0       1       2|
    # |10     11      12|
    # |20     21      22|
    # |30     31      32|