Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Builtins frontend #22007

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
df2fa13
implement decorators for builtins frontend
illia-bab Aug 16, 2023
e80aa91
implement frontend list class
illia-bab Aug 16, 2023
d6cc29d
implement functions from math module
illia-bab Aug 16, 2023
8c5eca0
implement abs and range frontend functions and use within frontend li…
illia-bab Aug 16, 2023
e552bff
added comments and refactored _infer_return_array to follow the curre…
illia-bab Aug 17, 2023
681cde9
implemented all, any and round builtin functions
illia-bab Aug 17, 2023
3988d55
rm list frontend class as part of design decision
illia-bab Aug 17, 2023
320cd61
modify builtins init file
illia-bab Aug 17, 2023
fe02c23
minor fixes to _infer_return_array
illia-bab Aug 17, 2023
c840c6b
implement angular conversions
illia-bab Aug 18, 2023
7ed9f25
modify init
illia-bab Aug 18, 2023
85d2534
implemented special functions and manually tested
illia-bab Aug 18, 2023
857e3d8
implemented wrappers of hyperbolic funcs and manually tested
illia-bab Aug 18, 2023
e5e20ca
minor change to from_zero_dim_arrays_to_scalar
illia-bab Aug 18, 2023
be5874f
implemented wrappers for trigonometric functions and manually tested
illia-bab Aug 18, 2023
a272b53
added wrappers for exp2, expm1, log1p, log10 functions and manually t…
illia-bab Aug 18, 2023
3cd99a8
compositionally implemented cbrt and manually tested
illia-bab Aug 18, 2023
b4eaff8
upd jax config within decorator when jax is set as a backend
illia-bab Aug 18, 2023
e9892e0
added handling for tuple in from_zero_dim_array_to_scalar
illia-bab Aug 18, 2023
ef7545f
implemented number theoretic and representation functions and manuall…
illia-bab Aug 21, 2023
2dc4e8b
minor fix to from_zero+dim_arrays_to_scalar to deal with scalars
illia-bab Aug 21, 2023
1135bd5
Merge branch 'unifyai:main' into builtins_frontend
illia-bab Aug 21, 2023
bf2aab3
Merge branch 'unifyai:main' into builtins_frontend
illia-bab Aug 22, 2023
d3e8e36
modify inputs_to_ivy_arrays when no args passed and add default behav…
illia-bab Aug 22, 2023
434d8bb
implemented min/max frontends and manually tested
illia-bab Aug 22, 2023
57cfbca
enable ivy array in _infer_return_array and add set/unset default dty…
illia-bab Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ivy/functional/frontends/builtins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import func_wrapper
from .func_wrapper import *
from . import built_in_functions
from .built_in_functions import *
from . import math
90 changes: 90 additions & 0 deletions ivy/functional/frontends/builtins/built_in_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import ivy
import ivy.functional.frontends.builtins as builtins_frontend
from ivy.functional.frontends.builtins.func_wrapper import (
from_zero_dim_arrays_to_scalar,
to_ivy_arrays_and_back,
)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def abs(x):
return ivy.abs(x)


@to_ivy_arrays_and_back
def range(start, /, stop=None, step=1):
if not stop:
return ivy.arange(0, stop=start, step=step)
return ivy.arange(start, stop=stop, step=step)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def all(iterable):
return ivy.all(iterable)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def any(iterable):
return ivy.any(iterable)


@from_zero_dim_arrays_to_scalar
AnnaTz marked this conversation as resolved.
Show resolved Hide resolved
def round(number, ndigits=None):
if not ndigits:
return ivy.round(number)
return ivy.round(number, decimals=ndigits)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def min(*args, default=None, key=None):
# arguments are empty
if len(args) == 0:
# in this case default should be provided
if not default:
raise ValueError("default must be provided for empty input")
return default

# this means we deal with iterable rather than separate arguments
elif len(args) == 1 and not ivy.isscalar(args[0]):
# pass iterable to the same func
return builtins_frontend.min(*args[0], default=default, key=key)

# if keyfunc provided, map all args to it
if key:
mapped_args = ivy.map(key, constant=None, unique={"x": args}, mean=False)
idx = ivy.argmin(mapped_args)
# argmin always returns array, convert it to scalar
idx = ivy.to_scalar(idx)
return args[idx]

return ivy.min(args)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def max(*args, default=None, key=None):
# arguments are empty
if len(args) == 0:
# in this case default should be provided
if not default:
raise ValueError("default must be provided for empty input")
return default

# this means we deal with iterable rather than separate arguments
elif len(args) == 1 and not ivy.isscalar(args[0]):
# pass iterable to the same func
return builtins_frontend.max(*args[0], default=default, key=key)

# if keyfunc provided, map all args to it
if key:
mapped_args = ivy.map(key, constant=None, unique={"x": args}, mean=False)
idx = ivy.argmax(mapped_args)
# argmin always returns array, convert it to scalar
idx = ivy.to_scalar(idx)
return args[idx]

return ivy.max(args)
101 changes: 101 additions & 0 deletions ivy/functional/frontends/builtins/func_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import functools
from typing import Callable, Iterable
from importlib import import_module

import ivy


def _to_ivy_array(x):
# if x is any frontend array including frontend list
# then extract the wrapped ivy array
if hasattr(x, "ivy_array"):
return x.ivy_array

# convert native arrays and lists to ivy arrays
elif isinstance(x, ivy.NativeArray):
Copy link
Contributor

@AnnaTz AnnaTz Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want lists to get converted, we have to change this to else.
I think however that ivy functions can already handle array-likes, in which case you would just need to change the comment, not the condition.

Copy link
Contributor Author

@illia-bab illia-bab Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just forgot to modify the comment.

return ivy.array(x)

return x


def _infer_return_array(x: Iterable) -> Callable:
# get module path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a docstring that explains what this decorator does overall? It is not very obvious when it should be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should always be used within @to_ivy_arrays_and_back decorator to infer the return array type. Let's consider the jax frontend. In this case, if decorator receives jax.Array, its frontend implementation or list the output will always be converted to a frontend implementation of jax.Array. However, in case of builtins the behaviour changes to: almost_any_array -> ivy.Array -> almost_any_array. In other words, we need to track what array to convert back to. So, if our built-in function recieves np.ndarray or its frontend implementation, the output will be converted to np_frontend.ndarray and so on. It also has a default behaviour. By default we convert the output to a frontend implementation of array which corresponds to current backend. This was done to cover the cases, when function accepts scalar arguments, but returns an array. For example, range. But yeah I will add a docstring.

module_path = x.__class__.__module__.split(".")

# if function's input is a scalar, list, or tuple
# convert to current backend's frontend array
if "builtins" in module_path:
cur_backend = ivy.current_backend_str()
module_str = cur_backend if len(cur_backend) != 0 else "numpy"

# in this case we're dealing with frontend array
# module's name is always at index 3 on the path
elif "ivy" in module_path:
module_str = module_path[3]

# native array, e.g. np.ndarray, torch.Tensor etc.
else:
module_str = module_path[0]
# replace jaxlib with jax to construct a valid path
module_str = "jax" if module_str == "jaxlib" else module_str

# import the module and get a corresponding frontend array
frontend_path = "ivy.functional.frontends." + module_str
module = import_module(frontend_path)
frontend_array = getattr(module, "_frontend_array")

return frontend_array


def inputs_to_ivy_arrays(fn: Callable) -> Callable:
@functools.wraps(fn)
def _inputs_to_ivy_arrays_builtins(*args, **kwargs):
ivy_args = ivy.nested_map(args, _to_ivy_array, shallow=False)
ivy_kwargs = ivy.nested_map(kwargs, _to_ivy_array, shallow=False)

# array is the first argument given to a function
frontend_array = _infer_return_array(args[0]) if len(args) != 0 else None

return fn(*ivy_args, **ivy_kwargs), frontend_array

return _inputs_to_ivy_arrays_builtins


def outputs_to_frontend_arrays(fn: Callable) -> Callable:
@functools.wraps(fn)
def _outputs_to_frontend_arrays(*args, **kwargs):
# update config for jax backend
if ivy.current_backend_str() == "jax":
import jax

jax.config.update("jax_enable_x64", True)

ret, frontend_array = fn(*args, **kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way inputs_to_ivy_arrays and outputs_to_frontend_arrays have been designed right now they can't be used individually. They would only work when used together in to_ivy_arrays_and_back.
E.g. ret, frontend_array = fn(*args, **kwargs) would cause an error if fn was an unwrapped frontend function because there would not be a frontend_array returned.


if isinstance(ret, (ivy.Array, ivy.NativeArray)):
return frontend_array(ret)
return ret

return _outputs_to_frontend_arrays
AnnaTz marked this conversation as resolved.
Show resolved Hide resolved


def to_ivy_arrays_and_back(fn: Callable) -> Callable:
return outputs_to_frontend_arrays(inputs_to_ivy_arrays(fn))
Copy link
Contributor

@AnnaTz AnnaTz Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Make another wrapper inside here like:
@functools.wraps(fn) 
def _to_ivy_arrays_and_back(*args, **kwargs):
  • Move the frontend_array = _infer_return_array(args[0]) if len(args) != 0 else None there.
  • Call inputs_to_ivy_arrays, and then call outputs_to_frontend_arrays passing frontend_array.
  • You will need to add frontend_array as a new argument like outputs_to_frontend_arrays(fn: Callable, frontend_array: str).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll try this approach!



def from_zero_dim_arrays_to_scalar(fn: Callable) -> Callable:
@functools.wraps(fn)
def _from_zero_dim_arrays_to_scalar(*args, **kwargs):
ret = fn(*args, **kwargs)

if isinstance(ret, tuple):
return ivy.nested_map(
ret, lambda x: ivy.to_scalar(x) if len(x) == 0 else x, shallow=False
)

elif isinstance(ret, (int, float, bool, complex)) or len(ret) > 0:
return ret

return ivy.to_scalar(ret)

return _from_zero_dim_arrays_to_scalar
12 changes: 12 additions & 0 deletions ivy/functional/frontends/builtins/math/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from . import power_and_logarithmic_functions
from .power_and_logarithmic_functions import *
from . import angular_conversion
from .angular_conversion import *
from . import special_functions
from .special_functions import *
from . import hyperbolic_functions
from .hyperbolic_functions import *
from . import trigonometric_functions
from .trigonometric_functions import *
from . import number_theoretic_and_representation_functions
from .number_theoretic_and_representation_functions import *
14 changes: 14 additions & 0 deletions ivy/functional/frontends/builtins/math/angular_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ivy
from ivy.functional.frontends.builtins.func_wrapper import (
from_zero_dim_arrays_to_scalar,
)


@from_zero_dim_arrays_to_scalar
def radians(x):
return ivy.deg2rad(x)


@from_zero_dim_arrays_to_scalar
def degrees(x):
return ivy.rad2deg(x)
34 changes: 34 additions & 0 deletions ivy/functional/frontends/builtins/math/hyperbolic_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ivy
from ivy.functional.frontends.builtins.func_wrapper import (
from_zero_dim_arrays_to_scalar,
)


@from_zero_dim_arrays_to_scalar
def acosh(x):
return ivy.acosh(x)


@from_zero_dim_arrays_to_scalar
def asinh(x):
return ivy.asinh(x)


@from_zero_dim_arrays_to_scalar
def atanh(x):
return ivy.atanh(x)


@from_zero_dim_arrays_to_scalar
def cosh(x):
return ivy.cosh(x)


@from_zero_dim_arrays_to_scalar
def sinh(x):
return ivy.sinh(x)


@from_zero_dim_arrays_to_scalar
def tanh(x):
return ivy.tanh(x)
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import ivy
from ivy.functional.frontends.builtins.func_wrapper import (
to_ivy_arrays_and_back,
from_zero_dim_arrays_to_scalar,
)


@from_zero_dim_arrays_to_scalar
def ceil(x):
return ivy.ceil(x).astype(int)


@from_zero_dim_arrays_to_scalar
def copysign(x, y):
return ivy.copysign(x, y)


@from_zero_dim_arrays_to_scalar
def fabs(x):
return ivy.abs(x).astype(float)


@from_zero_dim_arrays_to_scalar
def floor(x):
return ivy.floor(x).astype(int)


@to_ivy_arrays_and_back
def fmod(x, y):
return ivy.fmod(x, y).astype(float)


@from_zero_dim_arrays_to_scalar
def frexp(x):
return ivy.frexp(x)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def fsum(x):
return ivy.sum(x).astype(float)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def gcd(*integers):
if len(integers) == 0:
return 0
return ivy.gcd(*integers)


@from_zero_dim_arrays_to_scalar
def isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0):
return ivy.isclose(a, b, rtol=rel_tol, atol=abs_tol)


@from_zero_dim_arrays_to_scalar
def isfinite(x):
return ivy.isfinite(x)


@from_zero_dim_arrays_to_scalar
def isinf(x):
return ivy.isinf(x)


@from_zero_dim_arrays_to_scalar
def isnan(x):
return ivy.isnan(x)


@from_zero_dim_arrays_to_scalar
def isqrt(x):
return ivy.floor(ivy.sqrt(x)).astype(int)


@to_ivy_arrays_and_back
@from_zero_dim_arrays_to_scalar
def lcm(*integers):
if len(integers) == 0:
return 1
return ivy.lcm(*integers)


@from_zero_dim_arrays_to_scalar
def ldexp(x, i):
return ivy.ldexp(x, i)


@from_zero_dim_arrays_to_scalar
def modf(x):
return ivy.modf(x)


@from_zero_dim_arrays_to_scalar
def nextafter(x, y):
return ivy.nextafter(x, y)


@from_zero_dim_arrays_to_scalar
def remainder(x, y):
return ivy.remainder(x, y).astype(float)


@from_zero_dim_arrays_to_scalar
def trunc(x):
return ivy.trunc(x)
Loading
Loading