Arrays#

There are two types of arrays in Ivy, there is the ivy.NativeArray and also the ivy.Array.

Native Array#

The ivy.NativeArray is simply a placeholder class for a backend-specific array class, such as np.ndarray, tf.Tensor, torch.Tensor or jaxlib.xla_extension.DeviceArray.

When no framework is set, this is an empty class. When a framework is set, this is overwritten with the backend-specific array class.

Ivy Array#

The ivy.Array is a simple wrapper class, which wraps around the ivy.NativeArray, storing it in self._data.

All functions in the Ivy functional API which accept at least one array argument in the input are implemented as instance methods in the ivy.Array class. The only exceptions to this are functions in the nest module and the meta module, which have no instance method implementations.

The organization of these instance methods follows the same organizational structure as the files in the functional API. The ivy.Array class inherits from many category-specific array classes, such as ArrayWithElementwise, each of which implements the category-specific instance methods.

Each instance method simply calls the functional API function internally, but passes in self._data as the first array argument. ivy.Array.add is a good example. However, it’s important to bear in mind that this is not necessarily the first argument, although in most cases it will be. We also do not set the out argument to self for instance methods. If the only array argument is the out argument, then we do not implement this instance method. For example, we do not implement an instance method for ivy.zeros.

Given the simple set of rules which underpin how these instance methods should all be implemented, if a source-code implementation is not found, then this instance method is added programmatically. This serves as a helpful backup in cases where some methods are accidentally missed out.

The benefit of the source code implementations is that this makes the code much more readable, with important methods not being entirely absent from the code. It also enables other helpful perks, such as auto-completions in the IDE etc.

Most special methods also simply wrap a corresponding function in the functional API, as is the case in the Array API Standard. Examples include __add__, __sub__, __mul__ and __truediv__ which directly call ivy.add(), ivy.subtract(), ivy.multiply() and ivy.divide() respectively. However, for some special methods such as __setitem__, there are substantial differences between the backend frameworks which must be addressed in the ivy.Array implementation.

Array Handling#

When calling backend-specific functions such as torch.sin(), we must pass in ivy.NativeArray instances. For example, torch.sin() will throw an error if we try to pass in an ivy.Array instance. It must be provided with a torch.Tensor, and this is reflected in the backend type hints.

However, all Ivy functions must return ivy.Array instances, which is reflected in the Ivy type hints. The reason we always return ivy.Array instances from Ivy functions is to ensure that any subsequent Ivy code is fully framework-agnostic, with all operators performed on the returned array being handled by the special methods of the ivy.Array class, and not the special methods of the backend ivy.NativeArray class.

For example, calling any of (+, -, *, / etc.) on the array will result in (__add__(), __sub__(), __mul__(), __truediv__() etc.) being called on the array class.

For most special methods, calling them on the ivy.NativeArray would not be a problem because all backends are generally quite consistent, but as explained above, for some functions such as __setitem__ there are substantial differences which must be addressed in the ivy.Array implementation in order to guarantee unified behaviour.

Given that all Ivy functions return ivy.Array instances, all Ivy functions must also support ivy.Array instances in the input, otherwise it would be impossible to chain functions together!

Therefore, most functions in Ivy must adopt the following pipeline:

  1. convert all ivy.Array instances in the input arguments to ivy.NativeArray instances

  2. call the backend-specific function, passing in these ivy.NativeArray instances

  3. convert all of the ivy.NativeArray instances which are returned from the backend function back into ivy.Array instances, and return

Given the repeating nature of these steps, this is all entirely handled in the inputs_to_native_arrays and outputs_to_ivy_arrays wrappers, as explained in the Function Wrapping section.

All Ivy functions also accept ivy.NativeArray instances in the input. This is for a couple of reasons. Firstly, ivy.Array instances must be converted to ivy.NativeArray instances anyway, and so supporting them in the input is not a problem. Secondly, this makes it easier to combine backend-specific code with Ivy code, without needing to explicitly wrap any arrays before calling sections of Ivy code.

Therefore, all input arrays to Ivy functions have type Union[ivy.Array, ivy.NativeArray], whereas the output arrays have type ivy.Array. This is further explained in the Function Arguments section.

However, ivy.NativeArray instances are not permitted for the out argument, which is used in most functions. This is because the out argument dictates the array to which the result should be written, and so it effectively serves the same purpose as the function return. This is further explained in the Inplace Updates section.

As a final point, extra attention is required for compositional functions, as these do not directly defer to a backend implementation. If the first line of code in a compositional function performs operations on the input array, then this will call the special methods on an ivy.NativeArray and not on an ivy.Array. For the reasons explained above, this would be a problem.

Therefore, all compositional functions have a separate piece of wrapped logic to ensure that all ivy.NativeArray instances are converted to ivy.Array instances before entering into the compositional function.

Integrating custom classes with Ivy#

Ivy’s functional API and its functions can easily be integrated with non-Ivy classes. Whether these classes are ones that inherit from Ivy or completely standalone custom classes, using Ivy’s __ivy_array_function__, Ivy’s functions can handle inputs of those types.

To make use of that feature, the class must contain an implementation for these functions and it must contain an implementation for the function __ivy_array_function__. If a non-Ivy class is passed to an Ivy function, a call to this class’s __ivy_array_function__ is made which directs Ivy’s function to handle that input type correctly. This allows users to define custom implementations for any of the functions that can be found in Ivy’s functional API which would further make it easy to integrate those classes with other Ivy projects.

Note This functionality is inspired by NumPy’s __ivy_array_function__ and PyTorch’s __torch_function__.

As an example, consider the following class MyArray with the following definition:

class MyArray:
        def __init__(self, data=None):
                self.data = data

Running any of Ivy’s functions using a MyArray object as input will throw an IvyBackendException since Ivy’s functions do not support this class type as input. This is where __ivy_array_function__ comes into play. Let’s add the method to our MyArray class to see how it works.

There are different ways to do so. One way is to use a global dict HANDLED_FUNCTIONS which will map Ivy’s functions to the custom variant functions:

HANDLED_FUNCTIONS = {}
class MyArray:
    def __init__(self, data=None):
            self.data = data
    def __ivy_array_function__(self, func, types, args, kwargs):
            if func not in HANDLED_FUNCTIONS:
                    return NotImplemented
            if not all(issubclass(t, (MyArray, ivy.Array, ivy.NativeArray)) for t in types):
                    return NotImplemented
            return HANDLED_FUNCTIONS[func](*args, **kwargs)

__ivy_array_function__ accepts four parameters: func representing a reference to the array API function being overridden, types a list of the types of objects implementing __ivy_array_function__, args a tuple of arguments supplied to the function, and kwargs being a dictionary of keyword arguments passed to the function. While this class contains an implementation for __ivy_array_function__, it is still not enough as it is necessary to implement any needed Ivy functions with the new MyArray class as input(s) for the code to run successfully. We will define a decorator function implements that can be used to add functions to HANDLED_FUNCTIONS:

def implements(ivy_function):
    def decorator(func):
        HANDLED_FUNCTIONS[ivy_function] = func
        return func
    return decorator

Lastly, we need to apply that decorator to the override function. Let’s consider for example a function that overrides ivy.abs:

@implements(ivy.abs)
def my_abs(my_array, ivy_array):
    my_array.data = abs(my_array.data)

Now that we have added the function to HANDLED_FUNCTIONS, we can now use ivy.abs with MyArray objects:

X = MyArray(-3)
X = ivy.abs(X)

Of course ivy.abs is an example of a function that is easy to override since it only requires one operand. The same approach can be used to override functions with multiple operands, including arrays or array-like objects that define __ivy_array_function__.

It is relevant to mention again that any function not stored inside the dict HANDLED_FUNCTIONS will not work and it is also important to notice that the operands passed to the function must match that of the function stored in the dict. For instance my_abs takes only one parameter which is a MyArray object. So, passing any other operands to the function will result in an exception IvyBackendException being thrown. Lastly, for a custom class to be covered completely with Ivy’s functional API, it is necessary to create an implementation for all the relevant functions within the API that will be used by this custom class. That can be all the functions in the API or only a subset of them.

Round Up

This should have hopefully given you a good feel for the different types of arrays, and how these are handled in Ivy.

If you have any questions, please feel free to reach out on discord in the arrays thread!