Backend Setting#

The backend framework can either be set by calling ivy.set_backend(backend_name) or it can inferred from the arguments. For the latter, a global variable implicit_backend is located in the file which is initialized as numpy, and is always used to infer the backend in cases where: (a) no backend has been set using the set_backend function and (b) the backend cannot be inferred from the inputs. If the framework can be inferred from the inputs, then this is always used, and the implicit_backend is overwritten with the framework inferred. numpy will always be the default backend unless it is explicitly set or is inferred. When calling this function for setting the backend, the following steps are performed:

  1. store a global copy of the original ivy.__dict__ to ivy_original_dict, if this is not already stored.

  2. import the backend module, for example ivy.functional.backends.torch, if the backend has been passed in as a string. All functions in this unmodified backend module are primary functions, because only primary functions are stored in ivy.functional.backends.backend_name. This backend module does not include any compositional functions.

  3. loop through the original ivy_original_dict (which has all functions, including compositional), and (a) add the primary function from the backend if it exists, (b) else add the compositional function from ivy_original_dict.

  4. wrap the functions where necessary, extending them with shared repeated functionality and writing the function to ivy.__dict__. Wrapping is used in order to avoid excessive code duplication in every backend function implementation. This is explained in more detail in the next section: Function Wrapping.

It’s helpful to look at an example:

x = ivy.array([[2., 3.]])
<module 'ivy.functional.backends.numpy' from '/opt/project/ivy/functional/backends/numpy/'>
y = ivy.multiply(torch.Tensor([3.]), torch.Tensor([4.]))
<module 'ivy.functional.backends.torch' from '/opt/project/ivy/functional/backends/torch/'>
z = ivy.matmul(jax.numpy.array([[2.,3.]]), jax.numpy.array([[5.],[6.]]))
<module 'ivy.functional.backends.jax' from '/opt/project/ivy/functional/backends/jax/'>
<module 'ivy.functional.backends.torch' from '/opt/project/ivy/functional/backends/torch/'>

In the last example above, the moment any backend is set, it will be used over the implicit_backend. However when the current backend is set to the previous using the ivy.previous_backend(), the implicit_backend will be used as a fallback, which will assume the backend from the last run. While the implicit_backend functionality gives more freedom to the user, the recommended way of doing things would be set the backend explicitly. In addition, all the previously set backends can be cleared by calling ivy.unset_backend().

Backend and Frontend Version Support#

Each time a new ivy backend is set, the backend_handler modifies the ivy.__dict__ to support the multiple versions of functions that are not forward compatible. For example, torch.ones_like() in the latest stable version 1.12 has many new arguments dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format compared to the same function at version 0.3.1. None of these new arguments will cause any forward compatibility issues: they weren’t used in old code, and they can now just be used in new code if desired. However, the removal of the out argument does break forward compatibility. Old torch code will raise an Argument Not Found error if being run with new torch versions. However, such forward-breaking changes are in the vast minority.

We currently use a naming convention for such functions and name them as fn_name_v_1p12_and_above which means that this particular implementation of the function is valid for versions 1.12 and above. Similarly, fn_name_v_1p01_to_1p1 means that the function is valid for versions between 1.01 and 1.1 both inclusive. Each time a backend is set, we go through the backend.__dict__ and for all functions for which multiple versions are detected, we simply import and assign the original fn_name to the version specific one. We do so by detecting the version of the backend framework installed on the user’s end.

We follow the same workflow for providing version support to the frontend functions. Again the version is inferred by importing the corresponding framework on the user’s system. If the user’s system doesn’t have the backend framework installed, we default to the latest version.

Round Up

This should have hopefully given you a good feel for how the backend framework is set.

If you have any questions, please feel free to reach out on discord in the backend setting channel or in the backend setting forum!