Current section

Files

Jump to
exla lib exla defn.ex
Raw

lib/exla/defn.ex

defmodule EXLA.Defn do
@moduledoc false
require Logger
alias Nx.Defn.{Composite, Expr, Tree}
alias Nx.Tensor, as: T
@doc false
def __stream__(key, input, acc, vars, fun, [args], options) do
{run_options, compile_options} = Keyword.pop(options, :run_options, [])
{client_name, compile_options} =
Keyword.pop_lazy(compile_options, :client, &EXLA.Client.default_name/0)
client = EXLA.Client.fetch!(client_name)
# The input vars should not be converted to buffers as they come from infeed
input_vars = Nx.Defn.Composite.flatten_list([input])
acc_vars = Nx.Defn.Composite.flatten_list([acc])
used_fun = &stream_used_inputs(&1, length(input_vars), length(acc_vars))
comp_fun =
&to_stream_computation(client, key, input_vars, acc_vars, &1, &2, &3, &4, compile_options)
{executable, used_inputs, {output, acc_output}, hooks, extra, debug?} =
compile(client, {:stream, key}, vars, fun, compile_options, used_fun, comp_fun)
{input_shape, input_indexes, output_shapes} = extra
# Execution of streams requires the coordination of
# multiple processes which is outlined below.
# First, we get a lock on the executable, because we want
# to avoid transfer to the device unless we know we are
# ready to use the device.
{time, lock} =
:timer.tc(fn ->
EXLA.Defn.Lock.lock(run_key(executable))
end)
if debug? do
Logger.debug("EXLA device #{executable.device_id} lock in #{us_to_ms(time)}ms")
end
{time, streams} =
:timer.tc(fn ->
buffers =
args
|> EXLA.Defn.Buffers.filter_by_indexes(used_inputs)
|> EXLA.Defn.Buffers.from_nx!()
# Now that we have transferred to device, we spawn a runner process
# to execute the stream. We use a runner instead of a task to avoid
# leaking messages in the inbox. We also don't use a supervisor
# to keep them linked, which is safe because the agent is not used
# outside the scope of the current process.
#
# Finally, note the runner cannot start immediately, we need to
# setup the outfeed reader and register the on_unlock callback
# that cancels the stream atomically. This is done inside
# EXLA.Defn.Stream.run.
{:ok, runner} =
EXLA.Defn.Runner.start_link(lock, fn ->
EXLA.Executable.run(executable, [buffers], run_options)
end)
# The outfeed reader will redirect all outputs with flag 1 to the current
# process. Once flag 0 is emitted, we know the stream is done.
hooks = Map.put(hooks, 1, {output_shapes, {self(), lock}})
{:ok, outfeed} = EXLA.Defn.Outfeed.start_child(executable, hooks, Process.group_leader())
stream =
EXLA.Defn.Stream.run(
executable,
lock,
runner,
outfeed,
input,
input_shape,
input_indexes,
output,
output_shapes,
acc_output
)
[stream]
end)
if debug? do
Logger.debug("EXLA stream start on device #{executable.device_id} in #{us_to_ms(time)}ms")
end
streams
end
defp stream_used_inputs(used, input_length, acc_length) do
total = input_length + acc_length
{inputs, acc_and_rest} = Enum.split_while(used, &(&1 < input_length))
{inputs,
Enum.to_list(input_length..(total - 1)//1) ++ Enum.drop_while(acc_and_rest, &(&1 < total))}
end
defp to_stream_computation(
client,
key,
input_vars,
acc_vars,
expr,
input_indexes,
used_shapes,
used_hooks,
options
) do
%{platform: platform} = client
inspected_key = inspect(key)
builder = EXLA.Builder.new(inspected_key)
input_shape =
input_vars
|> EXLA.Defn.Buffers.filter_by_indexes(input_indexes)
|> Enum.map(&nx_to_shape!/1)
|> EXLA.Shape.make_tuple_shape()
# Drop all accumulator entries from used_shapes as we will handle it separately.
used_shapes = Enum.drop(used_shapes, length(acc_vars))
# The stream loop will be a three element tuple:
#
# The result of calling infeed.
# The looping accumulator.
# The looping constants.
#
# The input will be read as part of the infeed.
acc_shapes = Enum.map(acc_vars, &nx_to_shape!/1)
acc_shape = EXLA.Shape.make_tuple_shape(acc_shapes)
constant_shape = EXLA.Shape.make_tuple_shape(Enum.map(used_shapes, &elem(&1, 1)))
flag_shape = EXLA.Shape.make_shape({:pred, 8}, {})
token_shape = EXLA.Shape.make_token_shape()
infeed_shape = EXLA.Shape.make_tuple_shape([flag_shape, token_shape])
arg_shape = EXLA.Shape.make_tuple_shape([infeed_shape, acc_shape, constant_shape])
pred_b = EXLA.Builder.new(builder, "while-pred-" <> inspected_key)
param = EXLA.Op.parameter(pred_b, 0, arg_shape, "arg")
infeed = EXLA.Op.get_tuple_element(param, 0)
flag = EXLA.Op.get_tuple_element(infeed, 0)
pred_op = EXLA.Op.equal(flag, EXLA.Op.constant_r0(pred_b, 1, {:pred, 8}))
pred = EXLA.Builder.build(pred_op)
body_b = EXLA.Builder.new(builder, "while-body-" <> inspected_key)
param = EXLA.Op.parameter(body_b, 0, arg_shape, "arg")
infeed = EXLA.Op.get_tuple_element(param, 0)
acc = EXLA.Op.get_tuple_element(param, 1)
constant = EXLA.Op.get_tuple_element(param, 2)
# The first infeed call is a flag.
# Call infeed again to get the actual input.
token = EXLA.Op.get_tuple_element(infeed, 1)
%EXLA.Shape{dtype: {:tuple, shapes}} = input_shape
# EXLA on host does not support tuples, so we emit multiple infeed operations.
{infeeds, token} =
if platform == :host do
Enum.map_reduce(shapes, token, fn shape, token ->
infeed = EXLA.Op.infeed(token, shape)
{EXLA.Op.get_tuple_element(infeed, 0), EXLA.Op.get_tuple_element(infeed, 1)}
end)
else
infeed = EXLA.Op.infeed(token, input_shape)
input = EXLA.Op.get_tuple_element(infeed, 0)
token = EXLA.Op.get_tuple_element(infeed, 1)
{Enum.with_index(shapes, fn _shape, i -> EXLA.Op.get_tuple_element(input, i) end), token}
end
{output, acc, cache} =
case expr do
{output_expr, acc_expr} ->
input_params = Enum.zip_with(infeeds, input_indexes, fn infeed, i -> {i, infeed} end)
# Accs start after inputs
counter = length(input_vars)
{acc_params, _counter} =
Enum.map_reduce(acc_vars, counter, fn _shape, i ->
{{i, EXLA.Op.get_tuple_element(acc, i - counter)}, i + 1}
end)
constant_params =
Enum.with_index(used_shapes, fn {pos, _shape}, index ->
{pos, EXLA.Op.get_tuple_element(constant, index)}
end)
state = %{
precision: Keyword.get(options, :precision, :highest),
builder: body_b,
params: Map.new(input_params ++ acc_params ++ constant_params),
scope_ids: Tree.scope_ids(expr)
}
{output, cache} = recur_flatten(output_expr, state, new_cache(token, used_hooks))
{acc, cache} = recur_flatten(acc_expr, state, cache)
{output, acc, cache}
_ ->
raise "expected the function given to Nx.stream/3 to return a two-element tuple, got: " <>
inspect(expr)
end
# Emit the output flag of 1 to signal loop output
{token, _, outfeed_hooks} = get_hooks(cache)
{token, output_shapes} = outfeed_flat_tuple(body_b, 1, output, token)
body_tuple = EXLA.Op.tuple(body_b, [EXLA.Op.infeed(token, flag_shape), acc, constant])
body = EXLA.Builder.build(body_tuple)
# Now we build the call to while, converting parameters to tuples.
{acc_params, counter} =
Enum.map_reduce(acc_shapes, 0, fn shape, i ->
{EXLA.Op.parameter(builder, i, shape, "p#{i}"), i + 1}
end)
{constant_params, _} =
Enum.map_reduce(used_shapes, counter, fn {_pos, shape}, i ->
{EXLA.Op.parameter(builder, i, shape, "p#{i}"), i + 1}
end)
token = EXLA.Op.create_token(builder)
init =
EXLA.Op.tuple(builder, [
EXLA.Op.infeed(token, flag_shape),
EXLA.Op.tuple(builder, acc_params),
EXLA.Op.tuple(builder, constant_params)
])
while = EXLA.Op.while(pred, body, init)
infeed = EXLA.Op.get_tuple_element(while, 0)
acc = EXLA.Op.get_tuple_element(while, 1)
token = EXLA.Op.get_tuple_element(infeed, 1)
close_outfeed(builder, token)
{EXLA.Builder.build(acc), {input_shape, input_indexes, output_shapes}, outfeed_hooks}
end
@doc false
def __jit__(key, vars, fun, args_list, options) do
__compile__(key, vars, fun, options).(args_list)
end
@doc false
def __compile__(key, vars, fun, options) do
{run_options, compile_options} = Keyword.pop(options, :run_options, [])
{client_name, compile_options} =
Keyword.pop_lazy(compile_options, :client, &EXLA.Client.default_name/0)
client = EXLA.Client.fetch!(client_name)
callback = &to_root_computation(key, &1, &2, &3, &4, compile_options)
{executable, used_inputs, outputs, hooks, :ok, debug?} =
compile(client, key, vars, fun, compile_options, &{[], &1}, callback)
fn [args] ->
{time, lock} =
:timer.tc(fn ->
EXLA.Defn.Lock.lock(run_key(executable))
end)
if debug? do
Logger.debug("EXLA device #{executable.device_id} lock in #{us_to_ms(time)}ms")
end
{time, res} =
:timer.tc(fn ->
maybe_outfeed(lock, executable, args, used_inputs, outputs, hooks, run_options)
end)
if debug? do
Logger.debug("EXLA execution on device #{executable.device_id} in #{us_to_ms(time)}ms")
end
res
end
end
defp to_root_computation(key, expr, [] = _out_inputs, used_shapes, used_hooks, options) do
builder = EXLA.Builder.new(inspect(key))
params =
Enum.with_index(used_shapes, fn {pos, shape}, i ->
{pos, EXLA.Op.parameter(builder, i, shape, "p#{i}")}
end)
state = %{
precision: Keyword.get(options, :precision, :highest),
builder: builder,
params: Map.new(params),
scope_ids: Tree.scope_ids(expr)
}
token = EXLA.Op.create_token(builder)
{res, cache} = recur_flatten(expr, state, new_cache(token, used_hooks))
{token, used_hooks, outfeed_hooks} = get_hooks(cache)
close_outfeed(builder, used_hooks, token)
{EXLA.Builder.build(res), :ok, outfeed_hooks}
end
defp maybe_outfeed(lock, executable, args, used_inputs, outputs, hooks, run_options)
when hooks == %{} do
try do
buffers =
args
|> EXLA.Defn.Buffers.filter_by_indexes(used_inputs)
|> EXLA.Defn.Buffers.from_nx!()
EXLA.Executable.run(executable, [buffers], run_options)
else
[result] -> [EXLA.Defn.Buffers.to_nx!(result, outputs)]
after
EXLA.Defn.Lock.unlock(lock)
end
end
defp maybe_outfeed(lock, executable, args, used_inputs, outputs, hooks, run_options) do
buffers =
args
|> EXLA.Defn.Buffers.filter_by_indexes(used_inputs)
|> EXLA.Defn.Buffers.from_nx!()
{:ok, runner} =
EXLA.Defn.Runner.start_link(lock, fn ->
EXLA.Executable.run(executable, [buffers], run_options)
end)
{:ok, outfeed} = EXLA.Defn.Outfeed.start_child(executable, hooks, Process.group_leader())
_ = EXLA.Defn.Lock.transfer(lock, fn -> send(runner, lock) end, outfeed)
ref = Process.monitor(outfeed)
receive do
{:DOWN, ^ref, _, _, _} ->
[result] = EXLA.Defn.Runner.read(runner)
[EXLA.Defn.Buffers.to_nx!(result, outputs)]
end
end
defp run_key(%{client: %{ref: ref}, device_id: device_id}), do: [ref | device_id]
## Compile
defp compile(client, key, vars, fun, options, to_used, to_computation) do
{{expr_cache_fun, comp_cache_fun}, options} =
case Keyword.pop(options, :cache, true) do
{true, options} ->
Keyword.pop(options, EXLA, {&EXLA.Defn.LockedCache.run/2, &EXLA.Defn.LockedCache.run/2})
{false, options} ->
cache_fun = fn _key, fun -> fun.() end
{{cache_fun, cache_fun}, options}
end
{debug?, options} = Keyword.pop(options, :debug, false)
{args_key, reverse_args_triplet} =
Enum.map_reduce(vars, [], fn var, acc ->
Nx.Defn.Composite.traverse(var, acc, fn
%T{type: type, shape: shape, names: names}, acc ->
triplet = {type, shape, names}
{triplet, [triplet | acc]}
end)
end)
{eval_time, {expr, {ref, used_inputs, defined_hooks, outputs}}} =
:timer.tc(fn ->
expr_cache_fun.({key, args_key}, fn ->
expr = fun.(vars)
{expr, used_inputs_and_hooks(expr)}
end)
end)
if debug? do
hit_or_miss = if expr, do: "miss", else: "hit"
Logger.debug(
"EXLA defn evaluation #{inspect(key)} cache #{hit_or_miss} in #{us_to_ms(eval_time)}ms"
)
end
# Hooks with default callbacks or user callbacks are part of the cache key
{hooks, options} = Keyword.pop(options, :hooks, %{})
used_hooks = Enum.sort(for {k, v} <- defined_hooks, v != nil or Map.has_key?(hooks, k), do: k)
{out_inputs, in_inputs} = to_used.(used_inputs)
comp_key = {ref, client.name, used_hooks, options}
{comp_time, {evaled, {executable, extra, outfeed_hooks}}} =
:timer.tc(fn ->
comp_cache_fun.(comp_key, fn ->
shapes =
reverse_args_triplet
|> Enum.reverse()
|> EXLA.Defn.Buffers.filter_by_indexes(in_inputs)
|> Enum.map(fn {type, shape, _names} -> EXLA.Shape.make_shape(type, shape) end)
inputs_and_shapes = Enum.zip(in_inputs, shapes)
{computation, extra, hooks} =
to_computation.(expr || fun.(vars), out_inputs, inputs_and_shapes, used_hooks)
executable = EXLA.Computation.compile(computation, client, shapes, options)
{:ok, {executable, extra, hooks}}
end)
end)
# Now finally compute the hooks to give to outfeed
hooks =
for {flag, {key, template, shapes}} <- outfeed_hooks,
do: {flag, {shapes, compile_hook(key, hooks, defined_hooks, template)}},
into: %{}
if debug? do
hit_or_miss = if evaled, do: "miss", else: "hit"
Logger.debug(
"EXLA compilation #{inspect(key)} cache #{hit_or_miss} in #{us_to_ms(comp_time)}ms"
)
end
if expr || evaled do
measurements = %{
eval_time: eval_time,
compile_time: comp_time,
total_time: eval_time + comp_time
}
:telemetry.execute([:exla, :compilation], measurements, %{key: key})
end
{executable, in_inputs, outputs, hooks, extra, debug?}
end
defp us_to_ms(time), do: Float.round(time / 1000, 1)
defp compile_hook(key, hooks, defined_hooks, template) do
{hooks[key] || Map.fetch!(defined_hooks, key), template}
end
defp used_inputs_and_hooks(expr) do
{_, used_inputs, used_hooks} =
Composite.reduce(expr, {%{}, %{}, %{}}, &used_inputs_and_hooks/2)
{make_ref(), used_inputs |> Map.keys() |> Enum.sort(), used_hooks, Nx.to_template(expr)}
end
defp used_inputs_and_hooks(%T{data: %Expr{id: id} = expr} = t, {seen, inputs, hooks}) do
case seen do
%{^id => true} ->
{seen, inputs, hooks}
%{} ->
acc = {Map.put(seen, id, true), used_inputs(expr, inputs), used_hooks(expr, hooks)}
t
|> Tree.apply_args(acc, &{&1, used_inputs_and_hooks(&1, &2)})
|> elem(1)
end
end
defp used_inputs(%Expr{op: :parameter, args: [i], context: :root}, inputs),
do: Map.put(inputs, i, true)
defp used_inputs(_, inputs),
do: inputs
defp used_hooks(%Expr{op: :token, args: [token]}, hooks),
do: Enum.reduce(token.hooks, hooks, &Map.put(&2, &1.name, &1.callback))
defp used_hooks(_, hooks),
do: hooks
## Operator handling
defp recur_flatten(composite, state, cache) do
{acc, cache} =
Composite.reduce(composite, {[], cache}, fn %T{} = expr, {acc, cache} ->
{expr, cache} = recur_operator(expr, state, cache)
{[expr | acc], cache}
end)
{EXLA.Op.tuple(state.builder, Enum.reverse(acc)), cache}
end
defp recur_operator(%T{data: %Expr{id: id, op: op}} = expr, state, cache) do
case cache do
%{^id => res} ->
{res, cache}
%{} ->
{res, cache} = cached_recur_operator(op, expr, state, cache)
{res, Map.put(cache, id, res)}
end
end
defp cached_recur_operator(:while, %T{data: %Expr{args: args}}, state, cache) do
[initial, arg, pred, body] = args
{initial, cache} =
recur_composite({get_token(cache), initial}, &cast_pred_to_u8/1, state, cache)
{pred, cache} = while_computation(:while_pred, arg, pred, {:pred, 8}, & &1, state, cache)
{body, cache} =
while_computation(:while_body, arg, body, :with_token, &cast_pred_to_u8/1, state, cache)
while = EXLA.Op.while(pred, body, initial)
token = EXLA.Op.get_tuple_element(while, 0)
{EXLA.Op.get_tuple_element(while, 1), update_token(cache, token)}
end
defp cached_recur_operator(:cond, %T{data: %Expr{args: args}} = t, state, cache) do
[clauses, last] = args
{cond, cache} =
case clauses do
[{pred, on_true}] ->
to_if(pred, on_true, last, state, cache)
_ ->
# We convert cond into a nested tree of conds in order to compile it to ifs
%T{data: %Expr{args: [[{pred, on_true}], on_false]}} =
clauses
|> Enum.reverse()
|> Enum.reduce(last, fn {pred, on_true}, on_false ->
update_in(t.data, fn data ->
%{data | args: [[{pred, on_true}], on_false], id: make_ref()}
end)
end)
to_if(pred, on_true, on_false, state, cache)
end
if get_token(cache) do
token = EXLA.Op.get_tuple_element(cond, 0)
{EXLA.Op.get_tuple_element(cond, 1), update_token(cache, token)}
else
{cond, cache}
end
end
defp cached_recur_operator(:parameter, %T{data: %Expr{args: [i]}}, state, cache) do
{Map.fetch!(state.params, i), cache}
end
defp cached_recur_operator(:fun, %T{data: %Expr{args: args}, type: type}, state, cache) do
[args, expr, {_, name, _}] = args
{fun_computation(name, args, expr, type, state), cache}
end
defp cached_recur_operator(:optional, %T{data: %Expr{args: args}}, state, cache) do
[call, expr] = args
%{data: %{args: args, op: op}} = call
key = computation_key(op, args)
{call_args, cache} = Enum.map_reduce(args, cache, &recur_operator(&1, state, &2))
{call_body, cache} =
case cache do
%{^key => computation} ->
{computation, cache}
%{} ->
{computation, cache} = token_computation("optional", call_args, expr, state, cache)
{computation, Map.put(cache, key, computation)}
end
result = EXLA.Op.call(state.builder, [get_token(cache) | call_args], call_body)
token = EXLA.Op.get_tuple_element(result, 0)
{EXLA.Op.get_tuple_element(result, 1), update_token(cache, token)}
end
defp cached_recur_operator(:attach_token, %T{data: %Expr{args: [token, expr]}}, state, cache) do
{op, cache} = recur_operator(expr, state, cache)
{_, cache} = recur_operator(token, state, cache)
{op, cache}
end
defp cached_recur_operator(:token, %T{data: %Expr{args: [token]}}, state, cache) do
cache =
List.foldr(token.hooks, cache, fn %{name: name, expr: expr}, cache ->
# First traverse the child because if it has hooks,
# we need to handle them first
{tuple, cache} = recur_flatten(expr, state, cache)
{token, used_hooks, outfeed_hooks} = get_hooks(cache)
# Now, if we have a callback for this function, generate the outfeed code
cond do
name in used_hooks ->
# The hook at position 0 is used to shutdown the outfeed.
# The hook at position 1 is used to control streams.
# We may need to introduce other defaults, which require bumping this.
flag = map_size(outfeed_hooks) + 2
{token, shapes} = outfeed_flat_tuple(state.builder, flag, tuple, token)
outfeed_hooks = Map.put(outfeed_hooks, flag, {name, Nx.to_template(expr), shapes})
put_hooks(cache, token, used_hooks, outfeed_hooks)
token ->
cache
true ->
raise "hooks are not supported inside #{state.builder.name}"
end
end)
{EXLA.Op.tuple(state.builder, []), cache}
end
defp cached_recur_operator(op, expr, state, cache) do
{args, cache} = Tree.apply_args(expr, cache, &recur_operator(&1, state, &2))
{to_operator(op, args, expr, state), cache}
end
## to_operator creation
defp to_operator(:constant, [constant], ans, state) do
op = to_constant(state.builder, constant, ans.type)
if ans.shape == {} do
op
else
EXLA.Op.broadcast_in_dim(op, ans.shape, {})
end
end
defp to_operator(:tensor, [tensor], _ans, state) do
case tensor.shape do
{} ->
to_constant(state.builder, Nx.to_number(tensor), tensor.type)
shape ->
shape = EXLA.Shape.make_shape(tensor.type, shape)
EXLA.Op.constant_from_binary(state.builder, Nx.to_binary(tensor), shape)
end
end
defp to_operator(:random_uniform, [min, max], %{type: type, shape: shape}, _state) do
if match?({int, size} when int in [:s, :u] and size < 32, type) do
raise ArgumentError,
"Nx.random_uniform/4 for EXLA requires signed and unsigned tensors to be " <>
"at least of size 32, got: #{elem(type, 1)}"
end
min = to_type(min, type)
max = to_type(max, type)
shape = EXLA.Shape.make_shape(type, shape)
EXLA.Op.rng_uniform(min, max, shape)
end
defp to_operator(:random_normal, [mu, sigma], %{type: type, shape: shape}, _state) do
mu = to_type(mu, type)
sigma = to_type(sigma, type)
shape = EXLA.Shape.make_shape(type, shape)
EXLA.Op.rng_normal(mu, sigma, shape)
end
defp to_operator(:iota, [axis], %{type: type, shape: shape}, state) do
shape = EXLA.Shape.make_shape(type, shape)
EXLA.Lib.iota(state.builder, shape, axis)
end
defp to_operator(:eye, [], %{type: type, shape: shape}, state) do
iota_type = Nx.Type.merge_number({:u, 8}, Tuple.product(shape))
iota_shape = EXLA.Shape.make_shape(iota_type, shape)
rank = tuple_size(shape)
i0 = EXLA.Op.iota(state.builder, iota_shape, rank - 2)
i1 = EXLA.Op.iota(state.builder, iota_shape, rank - 1)
to_type(EXLA.Op.equal(i0, i1), type)
end
## to_operator shape
defp to_operator(:reshape, [op], %{shape: shape}, _state) do
EXLA.Op.reshape(op, shape)
end
defp to_operator(:pad, [op, value, padding_config], %{type: type}, _state) do
EXLA.Op.pad(to_type(op, type), to_type(value, type), padding_config)
end
defp to_operator(:broadcast, [op, _shape, axes], ans, _state) do
EXLA.Op.broadcast_in_dim(op, ans.shape, List.to_tuple(axes))
end
defp to_operator(:transpose, [op, axes], _ans, _state) do
EXLA.Op.transpose(op, List.to_tuple(axes))
end
defp to_operator(:squeeze, [op, _axes], ans, _state) do
EXLA.Op.reshape(op, ans.shape)
end
## to_operator others
defp to_operator(:metadata, [op, _metadata], _ans, _state) do
op
end
defp to_operator(:elem, [op, index], _ans, _state) do
EXLA.Op.get_tuple_element(op, index)
end
defp to_operator(
:dot,
[left, contract_axes1, batch_axes1, right, contract_axes2, batch_axes2],
%{type: type},
state
) do
precision = state.precision
EXLA.Op.dot_general(
to_type(left, type),
to_type(right, type),
{contract_axes1, batch_axes1, contract_axes2, batch_axes2},
precision
)
end
defp to_operator(
:conv,
[operand, kernel, opts],
ans,
state
) do
padding = opts[:padding]
strides = opts[:strides]
input_dilation = opts[:input_dilation]
kernel_dilation = opts[:kernel_dilation]
feature_groups = opts[:feature_group_size]
batch_groups = opts[:batch_group_size]
%{type: output_type} = ans
# Build general conv dims
input_permutation = List.to_tuple(opts[:input_permutation])
[out_features, in_features | spatial_features] = opts[:kernel_permutation]
kernel_permutation = List.to_tuple([in_features, out_features | spatial_features])
output_permutation =
opts[:output_permutation]
|> List.to_tuple()
conv_dim_nos = {input_permutation, kernel_permutation, output_permutation}
# Ensure both types are floating
operand = to_type(operand, output_type)
kernel = to_type(kernel, output_type)
EXLA.Op.conv_general_dilated(
operand,
kernel,
strides,
padding,
input_dilation,
kernel_dilation,
conv_dim_nos,
feature_groups,
batch_groups,
state.precision
)
end
defp to_operator(:select, [pred, on_true, on_false], %{type: type, shape: shape}, _state) do
pred = to_type(pred, {:pred, 8})
on_true =
on_true
|> to_type(type)
|> EXLA.Op.broadcast_in_dim(shape, broadcast_axes(op_shape(on_true), shape))
on_false =
on_false
|> to_type(type)
|> EXLA.Op.broadcast_in_dim(shape, broadcast_axes(op_shape(on_false), shape))
EXLA.Op.select(pred, on_true, on_false)
end
defp to_operator(:triangular_solve, [a, b, opts], %{type: type}, _state) do
left_side = Keyword.fetch!(opts, :left_side)
lower = Keyword.fetch!(opts, :lower)
transform = Keyword.fetch!(opts, :transform_a)
case EXLA.Op.get_shape(b).dims do
{_} = b_shape ->
b =
b
|> to_type(type)
|> EXLA.Op.reshape(Tuple.append(b_shape, 1))
to_type(a, type)
|> EXLA.Op.triangular_solve(b, left_side, lower, false, transform)
|> EXLA.Op.reshape(b_shape)
_ ->
to_type(a, type)
|> EXLA.Op.triangular_solve(to_type(b, type), left_side, lower, false, transform)
end
end
defp to_operator(:lu, [{_, _, _}, _tensor, _opts], _ans, _state) do
raise ArgumentError, "XLA does not currently support the LU operation"
end
defp to_operator(:qr, [{%{type: type}, %{type: type}}, tensor, opts], _ans, state) do
{q, r} = EXLA.Op.qr(to_type(tensor, type), opts[:mode] != :reduced)
EXLA.Op.tuple(state.builder, [q, r])
end
defp to_operator(
:svd,
[{%{type: type}, %{type: type}, %{type: type}}, tensor, _opts],
_ans,
state
) do
{u, s, vt} = EXLA.Op.svd(to_type(tensor, type), state.precision)
EXLA.Op.tuple(state.builder, [u, s, vt])
end
## to_operator element-wise
defp to_operator(:negate, [op], _ans, _state), do: EXLA.Op.negate(op)
defp to_operator(:abs, [op], _ans, _state), do: EXLA.Op.abs(op)
defp to_operator(:sign, [op], %{type: type}, state) do
case type do
{:u, _} -> EXLA.Op.min(op, EXLA.Op.constant_r0(state.builder, 1, type))
_ -> EXLA.Op.sign(op)
end
end
defp to_operator(:right_shift, [left, right], %{type: type}, _state) do
dims = broadcast_axes(op_shape(left), op_shape(right))
op =
if match?({:u, _}, type),
do: :right_shift_logical,
else: :right_shift_arithmetic
apply(EXLA.Op, op, [to_type(left, type), to_type(right, type), dims])
end
@bin_op [:add, :subtract, :multiply, :min, :max, :remainder, :power, :divide, :atan2] ++
[:bitwise_and, :bitwise_or, :bitwise_xor, :left_shift]
defp to_operator(op, [left, right], %{type: type}, _state) when op in @bin_op do
dims = broadcast_axes(op_shape(left), op_shape(right))
apply(EXLA.Op, op, [to_type(left, type), to_type(right, type), dims])
end
defp to_operator(:quotient, [left, right], %{type: type}, _state) do
dims = broadcast_axes(op_shape(left), op_shape(right))
apply(EXLA.Op, :divide, [to_type(left, type), to_type(right, type), dims])
end
@bin_comp_op [:equal, :not_equal, :greater, :less, :greater_equal, :less_equal]
defp to_operator(op, [left, right], _ans, _state) when op in @bin_comp_op do
# The answer type is always {:u, 8} but we need cast the inputs
# to the same type which is not necessarily the answer type.
left_shape = EXLA.Op.get_shape(left)
right_shape = EXLA.Op.get_shape(right)
type = merge_type(left_shape.dtype, right_shape.dtype)
dims = broadcast_axes(left_shape.dims, right_shape.dims)
apply(EXLA.Op, op, [to_type(left, type), to_type(right, type), dims])
end
@bin_pred_op [logical_and: :bitwise_and, logical_or: :bitwise_or, logical_xor: :bitwise_xor]
for {logical, bitwise} <- @bin_pred_op do
defp to_operator(unquote(logical), [left, right], _ans, _state) do
type = {:pred, 8}
dims = broadcast_axes(op_shape(left), op_shape(right))
apply(EXLA.Op, unquote(bitwise), [to_type(left, type), to_type(right, type), dims])
end
end
@unary_op [:exp, :expm1, :log, :log1p, :sigmoid, :cos, :sin, :tanh, :sqrt, :rsqrt, :cbrt] ++
[:bitwise_not, :count_leading_zeros, :population_count, :cosh, :sinh, :acos] ++
[:asin, :atan, :floor, :ceil, :round, :acosh, :asinh, :atanh, :erf] ++
[:erfc, :erf_inv, :conjugate]
defp to_operator(op, [arg], %{type: type}, _state) when op in @unary_op do
apply(EXLA.Op, op, [to_type(arg, type)])
end
defp to_operator(:fft, args, out, state), do: fft(&EXLA.Op.fft/2, args, out, state)
defp to_operator(:ifft, args, out, state), do: fft(&EXLA.Op.ifft/2, args, out, state)
defp to_operator(:is_nan, [arg], out, state),
do: EXLA.Op.is_nan(arg, op_type(arg), out.shape, Nx.axes(out), state)
defp to_operator(:is_infinity, [arg], out, state),
do: EXLA.Op.is_infinity(arg, op_type(arg), out.shape, Nx.axes(out), state)
# These operations do the type conversion implicitly, and so
# we cannot mess with the output type (e.g. the to_type conversion)
# because it will throw an error
@complex_op [:real, :imag]
defp to_operator(op, [arg], %{type: type}, _state) when op in @complex_op do
maybe_cast_arg =
if Nx.Type.integer?(op_type(arg)) do
to_type(arg, type)
else
arg
end
apply(EXLA.Op, op, [maybe_cast_arg])
end
@unary_lib_op [:tan]
defp to_operator(op, [arg], %{type: type}, _state) when op in @unary_lib_op do
apply(EXLA.Lib, op, [to_type(arg, type)])
end
defp to_operator(:as_type, [arg], %{type: type}, _state) do
to_type(arg, type)
end
defp to_operator(:bitcast, [arg], %{type: type}, _state) do
if op_type(arg) == type do
arg
else
EXLA.Op.bitcast_convert_type(arg, type)
end
end
## to_operator reduction
defp to_operator(:all, [arg, opts], %{shape: shape}, state) do
to_aggregate(:bitwise_and, {:pred, 8}, shape, arg, 1, opts, state)
end
defp to_operator(:any, [arg, opts], %{shape: shape}, state) do
to_aggregate(:bitwise_or, {:pred, 8}, shape, arg, 0, opts, state)
end
defp to_operator(:sum, [arg, opts], %{type: type, shape: shape}, state) do
to_aggregate(:add, type, shape, arg, 0, opts, state)
end
defp to_operator(:product, [arg, opts], %{type: type, shape: shape}, state) do
to_aggregate(:multiply, type, shape, arg, 1, opts, state)
end
defp to_operator(:reduce_max, [arg, opts], %{type: type, shape: shape}, state) do
min_number = EXLA.Lib.min_number(state.builder, type)
to_aggregate(:max, type, shape, arg, min_number, opts, state)
end
defp to_operator(:reduce_min, [arg, opts], %{type: type, shape: shape}, state) do
max_number = EXLA.Lib.max_number(state.builder, type)
to_aggregate(:min, type, shape, arg, max_number, opts, state)
end
defp to_operator(:reduce, [arg, acc, opts, fun], %{type: type, shape: shape}, _state) do
arg = to_type(arg, type)
keep_axes = opts[:keep_axes]
result = EXLA.Op.reduce(arg, to_type(acc, type), fun, reduce_axes(arg, opts[:axes]))
if keep_axes do
EXLA.Op.reshape(result, shape)
else
result
end
end
defp to_operator(:window_sum, [arg, window_dims, opts], %{type: type}, state) do
to_window_aggregate(:add, type, arg, 0, window_dims, opts, state)
end
defp to_operator(:window_max, [arg, window_dims, opts], %{type: type}, state) do
min_number = EXLA.Lib.min_number(state.builder, type)
to_window_aggregate(:max, type, arg, min_number, window_dims, opts, state)
end
defp to_operator(:window_min, [arg, window_dims, opts], %{type: type}, state) do
max_number = EXLA.Lib.max_number(state.builder, type)
to_window_aggregate(:min, type, arg, max_number, window_dims, opts, state)
end
defp to_operator(:window_product, [arg, window_dims, opts], %{type: type}, state) do
to_window_aggregate(:multiply, type, arg, 1, window_dims, opts, state)
end
defp to_operator(
:window_reduce,
[arg, acc, window_dimensions, opts, fun],
%{type: type},
_state
) do
padding_config = opts[:padding]
strides = opts[:strides]
window_dilations = opts[:window_dilations]
arg = to_type(arg, type)
EXLA.Op.window_reduce(
arg,
to_type(acc, type),
fun,
window_dimensions,
strides,
window_dilations,
padding_config
)
end
defp to_operator(
:window_scatter_max,
[arg, source, init_value, window_dimensions, opts],
%{type: type},
state
) do
padding_config = opts[:padding]
strides = opts[:strides]
arg = to_type(arg, type)
source = to_type(source, type)
init_value = to_type(init_value, type)
args = [%{type: type, shape: {}}, %{type: type, shape: {}}]
select_fn = op_computation(:greater, args, state)
scatter_fn = op_computation(:add, args, state)
EXLA.Op.select_and_scatter(
arg,
select_fn,
window_dimensions,
strides,
padding_config,
source,
init_value,
scatter_fn
)
end
defp to_operator(
:window_scatter_min,
[arg, source, init_value, window_dimensions, opts],
%{type: type},
state
) do
padding_config = opts[:padding]
strides = opts[:strides]
arg = to_type(arg, type)
source = to_type(source, type)
init_value = to_type(init_value, type)
args = [%{type: type, shape: {}}, %{type: type, shape: {}}]
select_fn = op_computation(:less, args, state)
scatter_fn = op_computation(:add, args, state)
EXLA.Op.select_and_scatter(
arg,
select_fn,
window_dimensions,
strides,
padding_config,
source,
init_value,
scatter_fn
)
end
defp to_operator(
:indexed_add,
tensors,
%{type: type} = out,
state
) do
args = [%{type: type, shape: {}}, %{type: type, shape: {}}]
scatter_fn = op_computation(:add, args, state)
scatter(scatter_fn, tensors, out)
end
defp to_operator(:indexed_put, tensors, out, state) do
# Build update computation
subbuilder = subbuilder(state.builder, "scatter_reduction")
param_shape = EXLA.Shape.make_shape(out.type, {})
_left = EXLA.Op.parameter(subbuilder, 0, param_shape, "left")
right = EXLA.Op.parameter(subbuilder, 1, param_shape, "right")
scatter_fn = EXLA.Builder.build(right)
scatter(scatter_fn, tensors, out)
end
defp to_operator(:map, [arg, _opts, fun], %{shape: shape, type: type}, _state) do
arg = to_type(arg, type)
EXLA.Op.map(arg, fun, Nx.axes(shape))
end
@reduction_op [:argmax, :argmin, :reduce_max, :reduce_min]
defp to_operator(op, [arg, opts], ans, state)
when op in @reduction_op do
apply(EXLA.Lib, op, [state.builder, arg, [type: ans.type] ++ opts])
end
defp to_operator(:clip, [operand, min, max], ans, _state) do
min = to_type(min, ans.type)
max = to_type(max, ans.type)
operand = to_type(operand, ans.type)
EXLA.Op.clamp(operand, min, max)
end
defp to_operator(:slice, [tensor, start_indices, lengths, strides], ans, _state) do
all_static? = Enum.all?(start_indices, &is_integer/1)
if all_static? do
limit_indices = Enum.zip_with(start_indices, lengths, fn i, len -> i + len end)
EXLA.Op.slice(tensor, start_indices, limit_indices, strides)
else
zeros = List.duplicate(0, tuple_size(ans.shape))
slice = EXLA.Op.dynamic_slice(tensor, start_indices, lengths)
EXLA.Op.slice(slice, zeros, lengths, strides)
end
end
defp to_operator(:put_slice, [tensor, start_indices, slice], ans, _state) do
tensor = to_type(tensor, ans.type)
slice = to_type(slice, ans.type)
EXLA.Op.dynamic_update_slice(tensor, slice, start_indices)
end
defp to_operator(:take, [tensor, indices, axis], _ans, _state) do
tensor_rank = tensor |> op_shape() |> tuple_size()
indices_rank = indices |> op_shape() |> tuple_size()
result_rank = tensor_rank - 1 + indices_rank
index_vector_dim = indices_rank
slice_sizes = tensor |> op_shape() |> put_elem(axis, 1) |> Tuple.to_list()
offset_dims = result_rank |> axes_for_rank() |> delete_slice(axis, indices_rank)
collapsed_slice_dims = [axis]
start_index_map = [axis]
EXLA.Op.gather(
tensor,
indices,
index_vector_dim,
slice_sizes,
offset_dims,
collapsed_slice_dims,
start_index_map
)
end
defp to_operator(:take_along_axis, [tensor, indices, axis], _ans, state) do
indices_shape = op_shape(indices)
indices_rank = tuple_size(indices_shape)
axes_range = 0..(indices_rank - 1)//1
index_vector_dim = indices_rank
slice_sizes = List.duplicate(1, indices_rank)
offset_dims = []
collapsed_slice_dims = Enum.to_list(axes_range)
start_index_map = Enum.to_list(axes_range)
indices_exla_shape = EXLA.Op.get_shape(indices)
iotas =
Enum.map(axes_range, fn axis ->
EXLA.Op.iota(state.builder, indices_exla_shape, axis)
end)
new_axis_shape = Tuple.append(indices_shape, 1)
indices =
iotas
|> List.replace_at(axis, indices)
|> Enum.map(&EXLA.Op.reshape(&1, new_axis_shape))
|> EXLA.Op.concatenate(indices_rank)
EXLA.Op.gather(
tensor,
indices,
index_vector_dim,
slice_sizes,
offset_dims,
collapsed_slice_dims,
start_index_map
)
end
defp to_operator(:gather, [tensor, indices], _ans, _state) do
tensor_rank = tensor |> op_shape() |> tuple_size()
indices_rank = indices |> op_shape() |> tuple_size()
index_vector_dim = indices_rank - 1
slice_sizes = List.duplicate(1, tensor_rank)
offset_dims = []
collapsed_slice_dims = axes_for_rank(tensor_rank)
start_index_map = axes_for_rank(tensor_rank)
EXLA.Op.gather(
tensor,
indices,
index_vector_dim,
slice_sizes,
offset_dims,
collapsed_slice_dims,
start_index_map
)
end
defp to_operator(:reverse, [tensor, axes], _ans, _state) do
EXLA.Op.reverse(tensor, axes)
end
defp to_operator(:concatenate, [tensors, axis], ans, _state) do
tensors =
tensors
|> Enum.map(&to_type(&1, ans.type))
EXLA.Op.concatenate(tensors, axis)
end
defp to_operator(:cholesky, [tensor], ans, state) do
tensor = to_type(tensor, ans.type)
cholesky = EXLA.Op.cholesky(tensor)
zeros =
state.builder
|> to_constant(0.0, ans.type)
|> EXLA.Op.broadcast_in_dim(ans.shape, broadcast_axes({}, ans.shape))
iota_shape = EXLA.Shape.make_shape({:s, 64}, ans.shape)
iota_one = EXLA.Op.iota(state.builder, iota_shape, 1)
iota_zero = EXLA.Op.iota(state.builder, iota_shape, 0)
EXLA.Op.select(EXLA.Op.less_equal(iota_one, iota_zero), cholesky, zeros)
end
defp to_operator(:sort, [tensor, opts], ans, state) do
dimension = opts[:axis]
op =
case opts[:direction] do
:asc -> :less
:desc -> :greater
end
args = [%{type: ans.type, shape: {}}, %{type: ans.type, shape: {}}]
comp = op_computation(op, args, state)
EXLA.Op.sort(tensor, comp, dimension)
end
defp to_operator(:argsort, [tensor, opts], ans, state) do
dimension = opts[:axis]
op =
case opts[:direction] do
:asc -> :less
:desc -> :greater
end
args = [
%{type: op_type(tensor), shape: {}},
%{type: op_type(tensor), shape: {}},
%{type: ans.type, shape: {}},
%{type: ans.type, shape: {}}
]
comp = op_computation(op, args, state, fn [arg1, arg2 | _] -> [arg1, arg2] end)
EXLA.Lib.argsort(state.builder, tensor, dimension, comp, ans.type)
end
defp fft(exla_op, [tensor, opts], %{type: type}, state) do
n = opts[:length]
output_type = Nx.Type.to_complex(type)
tensor = to_type(tensor, output_type)
shape = op_shape(tensor)
m = elem(shape, tuple_size(shape) - 1)
tensor =
cond do
m == n ->
tensor
m > n ->
lengths =
shape
|> Tuple.insert_at(tuple_size(shape), n)
|> Tuple.delete_at(tuple_size(shape) - 1)
|> Tuple.to_list()
starts = List.duplicate(0, tuple_size(shape))
strides = List.duplicate(1, tuple_size(shape))
EXLA.Op.slice(tensor, starts, lengths, strides)
m < n ->
zero = EXLA.Op.constant_r0(state.builder, Complex.new(0), output_type)
padding_config =
{0, 0, 0}
|> List.duplicate(tuple_size(shape))
|> List.replace_at(tuple_size(shape) - 1, {0, n - m, 0})
EXLA.Op.pad(tensor, zero, padding_config)
end
apply(exla_op, [tensor, n])
end
defp scatter(scatter_fn, [target, indices, updates], %{type: type}) do
target = to_type(target, type)
updates = to_type(updates, type)
rank = target |> op_shape() |> tuple_size()
# indices_rank is guaranteed to be 2 by Nx.Shape
indices_rank = 2
rank_diff = rank - indices_rank + 1
indices_shape = op_shape(indices)
indices_shape =
[List.duplicate(1, rank_diff) | Tuple.to_list(indices_shape)]
|> List.flatten()
|> List.to_tuple()
indices = EXLA.Op.reshape(indices, indices_shape)
# If indices has shape {x, y}, updates is guaranteed by Nx.Shape to
# have shape {x}, so if we reshaped indices to {..., x, y}, we need to
# reshape updates to {..., x}
updates_shape = Tuple.delete_at(indices_shape, tuple_size(indices_shape) - 1)
updates = EXLA.Op.reshape(updates, updates_shape)
axes = axes_for_rank(rank)
EXLA.Op.scatter(
target,
indices,
updates,
scatter_fn,
rank,
[],
axes,
axes
)
end
## Cache and hook helpers helpers
defp no_token_cache(),
do: %{__MODULE__ => {nil, [], %{}}}
defp new_cache(token, used),
do: %{__MODULE__ => {token, used, %{}}}
defp update_outfeed(%{__MODULE__ => {token, used, _}} = cache, %{__MODULE__ => {_, _, outfeed}}),
do: %{cache | __MODULE__ => {token, used, outfeed}}
defp reset_token(%{__MODULE__ => {_, used, outfeed}}, token),
do: %{__MODULE__ => {token, used, outfeed}}
defp update_token(%{__MODULE__ => {_token, used, outfeed}} = cache, token),
do: %{cache | __MODULE__ => {token, used, outfeed}}
defp get_token(%{__MODULE__ => {token, _, _}}),
do: token
defp get_hooks(%{__MODULE__ => value}),
do: value
defp put_hooks(cache, token, used_hooks, outfeed_hooks),
do: %{cache | __MODULE__ => {token, used_hooks, outfeed_hooks}}
## Outfeed
defp outfeed_flat_tuple(builder, flag, tuple, token) do
token = EXLA.Op.outfeed(EXLA.Op.constant_r0(builder, flag, {:u, 16}), token)
%EXLA.Shape{dims: {size}, dtype: {:tuple, shapes}} = EXLA.Op.get_shape(tuple)
token =
Enum.reduce(1..size//1, token, fn pos, token ->
EXLA.Op.outfeed(EXLA.Op.get_tuple_element(tuple, pos - 1), token)
end)
{token, shapes}
end
defp close_outfeed(_builder, [], _token), do: :ok
defp close_outfeed(builder, _, token), do: close_outfeed(builder, token)
defp close_outfeed(builder, token) do
EXLA.Op.outfeed(EXLA.Op.constant_r0(builder, 0, {:u, 16}), token)
end
## Computation helpers
defp op_computation(op, args, state, prepare_args \\ & &1) do
subbuilder = subbuilder(state.builder, Atom.to_string(op))
args =
Enum.with_index(args, fn arg, i ->
fun_shape = computation_arg_shape(arg)
EXLA.Op.parameter(subbuilder, i, fun_shape, "p#{i}")
end)
EXLA.Builder.build(apply(EXLA.Op, op, prepare_args.(args)))
end
defp fun_computation(name, args, expr, type, state) do
subbuilder = subbuilder(state.builder, Atom.to_string(name))
arg_params =
Enum.with_index(args, fn arg, i ->
fun_shape = computation_arg_shape(arg)
{arg, EXLA.Op.parameter(subbuilder, i, fun_shape, "p#{i}")}
end)
params = Enum.flat_map(arg_params, &computation_arg_param/1)
state = %{
state
| builder: subbuilder,
params: Map.new(params),
scope_ids: Tree.scope_ids(expr)
}
{res, _} = recur_composite(expr, state, no_token_cache())
EXLA.Builder.build(to_type(res, type))
end
defp while_computation(name, arg, expr, type, transform, state, cache) do
subbuilder = subbuilder(state.builder, Atom.to_string(name))
arg_shape = computation_arg_shape(arg)
tuple_shape = EXLA.Shape.make_tuple_shape([EXLA.Shape.make_token_shape(), arg_shape])
param = EXLA.Op.parameter(subbuilder, 0, tuple_shape, "p0")
arg_token = EXLA.Op.get_tuple_element(param, 0)
arg_param = EXLA.Op.get_tuple_element(param, 1)
params = computation_arg_param({arg, arg_param})
state = %{
state
| builder: subbuilder,
params: Map.new(params),
scope_ids: Tree.scope_ids(expr)
}
{res, comp_cache} = recur_composite(expr, transform, state, reset_token(cache, arg_token))
res =
if type == :with_token do
EXLA.Op.tuple(subbuilder, [arg_token, res])
else
to_type(res, type)
end
{EXLA.Builder.build(res), update_outfeed(cache, comp_cache)}
end
defp token_computation(name, arg, expr, state, cache) do
subbuilder = subbuilder(state.builder, name)
arg_token = EXLA.Op.parameter(subbuilder, 0, EXLA.Shape.make_token_shape(), "p0")
params =
arg
|> Enum.map(&EXLA.Op.get_shape/1)
|> Enum.with_index(fn arg_shape, idx ->
{idx, EXLA.Op.parameter(subbuilder, idx + 1, arg_shape, "p#{idx + 1}")}
end)
state = %{
state
| builder: subbuilder,
params: Map.new(params),
scope_ids: Tree.scope_ids(expr)
}
{res, comp_cache} = recur_composite(expr, state, reset_token(cache, arg_token))
res = EXLA.Op.tuple(subbuilder, [arg_token, res])
{EXLA.Builder.build(res), update_outfeed(cache, comp_cache)}
end
defp computation_key(op, args) do
keys =
Enum.map(args, fn
%Nx.Tensor{shape: shape, names: names, type: type} -> {type, shape, names}
opts -> opts
end)
{op, keys}
end
defp computation_arg_shape(%{type: type, shape: shape}) do
EXLA.Shape.make_shape(type, shape)
end
defp computation_arg_shape(tuple) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> Enum.map(&computation_arg_shape/1)
|> EXLA.Shape.make_tuple_shape()
end
defp computation_arg_param({tuple, param}) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> Enum.with_index(fn arg, i -> {arg, EXLA.Op.get_tuple_element(param, i)} end)
|> Enum.flat_map(&computation_arg_param/1)
end
defp computation_arg_param({%T{data: %Expr{op: :parameter, args: [pos]}}, param}) do
[{pos, param}]
end
defp recur_composite(composite, state, cache) do
recur_composite(composite, & &1, state, cache)
end
defp recur_composite(tuple, transform, state, cache) when is_tuple(tuple) do
list = Tuple.to_list(tuple)
if expr = full_tuple(list) do
recur_composite(expr, transform, state, cache)
else
{elements, cache} = Enum.map_reduce(list, cache, &recur_composite(&1, transform, state, &2))
{EXLA.Op.tuple(state.builder, elements), cache}
end
end
defp recur_composite(%EXLA.Op{} = op, transform, _state, cache) do
{transform.(op), cache}
end
defp recur_composite(expr, transform, state, cache) do
{op, cache} = recur_operator(expr, state, cache)
{transform.(op), cache}
end
# If each element of the tuple is just a reference to the parent expression,
# discard the tuple elements and return the parent expression.
defp full_tuple(list) do
with [%T{data: %Expr{op: :elem, args: args}} | rest] <- list,
[%T{data: %Expr{id: id}} = expr, 0] <- args,
true <- rest |> Enum.with_index(1) |> Enum.all?(&full_tuple?(&1, id)) do
expr
else
_ -> nil
end
end
defp full_tuple?({arg, index}, id) do
match?(%T{data: %Expr{op: :elem, args: [%T{data: %Expr{id: ^id}}, ^index]}}, arg)
end
## Aggregation
defp to_aggregate(op, type, shape, arg, initial, opts, state) do
arg = to_type(arg, type)
acc =
case initial do
%EXLA.Op{} = initial -> initial
initial when is_number(initial) -> EXLA.Op.constant_r0(state.builder, initial, type)
end
args = [%{type: type, shape: {}}, %{type: type, shape: {}}]
# We reverse the argument order because :nan + :infinity
# returns :nan but :infinity + :nan returns :infinity.
# So we want to keep the current value as first argument
# to preserve such properties.
comp = op_computation(op, args, state, &Enum.reverse/1)
keep_axes = opts[:keep_axes]
result = EXLA.Op.reduce(arg, acc, comp, reduce_axes(arg, opts[:axes]))
if keep_axes do
EXLA.Op.reshape(result, shape)
else
result
end
end
defp to_window_aggregate(op, type, arg, initial, window_dimensions, opts, state) do
arg = to_type(arg, type)
acc =
case initial do
%EXLA.Op{} = initial ->
initial
initial when is_number(initial) ->
EXLA.Op.constant_r0(state.builder, initial, type)
end
args = [%{type: type, shape: {}}, %{type: type, shape: {}}]
# We reverse the argument order because :nan + :infinity
# returns :nan but :infinity + :nan returns :infinity.
# So we want to keep the current value as first argument
# to preserve such properties.
comp = op_computation(op, args, state, &Enum.reverse/1)
strides = opts[:strides]
padding = opts[:padding]
window_dilations = opts[:window_dilations]
EXLA.Op.window_reduce(arg, acc, comp, window_dimensions, strides, window_dilations, padding)
end
## Cond
defp to_if(pred, on_true, on_false, state, cache) do
{pred_op, cache} = recur_operator(pred, state, cache)
pred_op = to_type(pred_op, {:pred, 8})
true_ids = Tree.scope_ids(on_true)
false_ids = Tree.scope_ids(on_false)
{true_args, true_comp, cache} = to_if_branch(true, on_true, true_ids, false_ids, state, cache)
{false_args, false_comp, cache} =
to_if_branch(false, on_false, false_ids, true_ids, state, cache)
{EXLA.Op.conditional(pred_op, true_args, true_comp, false_args, false_comp), cache}
end
defp collect_arg?(_id, :parameter, _args, _shared_ids),
do: true
# We never pass reference to tuples around, only through their elements,
# so if a tuple is in a predicate, then it all must be in a predicate.
defp collect_arg?(_id, :elem, [%T{data: %Expr{id: tuple_id}}, _pos], {parent_ids, sibling_ids})
when is_map_key(parent_ids, tuple_id) or is_map_key(sibling_ids, tuple_id),
do: true
defp collect_arg?(id, _op, _args, {parent_ids, sibling_ids}),
do: is_map_key(parent_ids, id) or is_map_key(sibling_ids, id)
defp collect_args(%T{data: %Expr{id: id, op: op, args: args}} = expr, {cache, ids}, shared_ids) do
cond do
op == :constant ->
{expr, {cache, ids}}
collect_arg?(id, op, args, shared_ids) ->
case ids do
%{^id => {_, _, new}} ->
{new, {cache, ids}}
%{} ->
i = map_size(ids)
param = Expr.parameter(expr, i)
{param, {Map.put(cache, id, param), Map.put(ids, id, {i, expr, param})}}
end
expr = Map.get(cache, id) ->
{expr, {cache, ids}}
true ->
{args, {cache, ids}} =
Tree.apply_args(expr, :scope, {cache, ids}, &collect_args(&1, &2, shared_ids))
expr = put_in(expr.data.args, args)
{expr, {Map.put(cache, id, expr), ids}}
end
end
defp to_if_branch(bool, expr, current_ids, other_ids, %{scope_ids: ids} = state, cache) do
{expr, {_, ids_args}} =
Composite.traverse(expr, {%{}, %{}}, &collect_args(&1, &2, {ids, other_ids}))
sorted_ids_args = Enum.sort_by(ids_args, fn {_id, {i, _old, _new}} -> i end)
{args, cache} =
Enum.map_reduce(sorted_ids_args, cache, fn {_, {_, old, _}}, cache ->
recur_operator(old, state, cache)
end)
subbuilder = subbuilder(state.builder, "if-#{Atom.to_string(bool)}")
{args, comp, comp_cache} =
if_branch_computation(subbuilder, args, cache, fn params, comp_cache ->
comp_state = %{
state
| builder: subbuilder,
params: Map.new(params),
scope_ids: current_ids
}
recur_composite(expr, &cast_pred_to_u8/1, comp_state, comp_cache)
end)
args = EXLA.Op.tuple(state.builder, args)
{args, comp, update_outfeed(cache, comp_cache)}
end
defp if_branch_computation(subbuilder, args, cache, fun) do
shapes = Enum.map(args, &EXLA.Op.get_shape/1)
if token = get_token(cache) do
tuple_shape = EXLA.Shape.make_tuple_shape([EXLA.Shape.make_token_shape() | shapes])
param = EXLA.Op.parameter(subbuilder, 0, tuple_shape, "p")
params = Enum.with_index(args, fn _, i -> {i, EXLA.Op.get_tuple_element(param, i + 1)} end)
comp_token = EXLA.Op.get_tuple_element(param, 0)
comp_cache = reset_token(cache, comp_token)
{res, comp_cache} = fun.(params, comp_cache)
comp = EXLA.Builder.build(EXLA.Op.tuple(subbuilder, [get_token(comp_cache), res]))
{[token | args], comp, comp_cache}
else
tuple_shape = EXLA.Shape.make_tuple_shape(shapes)
param = EXLA.Op.parameter(subbuilder, 0, tuple_shape, "p")
params = Enum.with_index(args, fn _, i -> {i, EXLA.Op.get_tuple_element(param, i)} end)
{res, comp_cache} = fun.(params, cache)
{args, EXLA.Builder.build(res), comp_cache}
end
end
## Axes helpers
defp broadcast_axes(left, right) do
{min, max} = if left <= right, do: {left, right}, else: {right, left}
min_size = tuple_size(min)
max_size = tuple_size(max)
# To reproduce Nx broadcast, we simply match the lower dimensions to the highest ones.
List.to_tuple(count_up(min_size, max_size - min_size))
end
defp reduce_axes(op, axes) do
if axes do
axes
|> Enum.sort()
|> List.to_tuple()
else
List.to_tuple(Nx.axes(op_shape(op)))
end
end
defp count_up(0, _n), do: []
defp count_up(i, n), do: [n | count_up(i - 1, n + 1)]
defp axes_for_rank(0), do: []
defp axes_for_rank(rank) do
Enum.to_list(0..(rank - 1))
end
## Op Helpers
defp op_type(op), do: EXLA.Op.get_shape(op).dtype
defp op_shape(op), do: EXLA.Op.get_shape(op).dims
defp to_type(op, type) do
if op_type(op) == type, do: op, else: EXLA.Op.convert_element_type(op, type)
end
# Inside cond/while, we need to convert pred to u8.
# We could do so lazily by comparing the versions of
# the branches, but that gets tricky with cond/if,
# so we always perform the operation.
defp cast_pred_to_u8(op) do
case EXLA.Op.get_shape(op).dtype do
{:pred, 8} -> EXLA.Op.convert_element_type(op, {:u, 8})
_ -> op
end
end
defp merge_type({:pred, 8}, {:pred, 8}), do: {:pred, 8}
defp merge_type(left, right), do: Nx.Type.merge(to_nx_type(left), to_nx_type(right))
defp to_nx_type({:pred, 8}), do: {:u, 8}
defp to_nx_type(type), do: type
defp to_constant(builder, constant, type) do
EXLA.Op.constant_r0(builder, constant, type)
end
defp subbuilder(%EXLA.Builder{name: name} = builder, desc) do
suffix = System.unique_integer([:positive])
EXLA.Builder.new(builder, name <> "-" <> desc <> "-" <> Integer.to_string(suffix))
end
# Helpers
defp nx_to_shape!(%T{type: type, shape: shape}),
do: EXLA.Shape.make_shape(type, shape)
defp delete_slice(enumerable, index, length) do
{left, right} = Enum.split(enumerable, index)
left ++ Enum.drop(right, length)
end
end