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:
convert all
ivy.Array
instances in the input arguments toivy.NativeArray
instancescall the backend-specific function, passing in these
ivy.NativeArray
instancesconvert all of the
ivy.NativeArray
instances which are returned from the backend function back intoivy.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!