defmodule Candlex.Backend do @moduledoc """ An opaque Nx backend with bindings to candle. """ defstruct [:device, :resource] @behaviour Nx.Backend alias Nx.Tensor, as: T alias Candlex.Native @device_cuda :cuda @device_cpu :cpu @impl true def init(opts) do Keyword.validate!(opts, [:device]) end # Creation @impl true def constant(%T{} = tensor, scalar, backend_options) do tensor |> Nx.BinaryBackend.constant(scalar, []) |> Nx.BinaryBackend.backend_transfer(__MODULE__, backend_options) end @impl true def from_binary(%T{shape: shape, type: type} = tensor, binary, backend_options) do binary |> Native.from_binary(to_candle_dtype(type), shape, device_option(backend_options)) |> unwrap!() |> to_nx(tensor) end @impl true def iota(%T{shape: {}} = out, nil, backend_options) do constant(out, 0, backend_options) end def iota(%T{shape: shape, type: type} = out, nil, backend_options) do Native.arange(0, Nx.size(shape), to_candle_dtype(type), shape, device_option(backend_options)) |> unwrap!() |> to_nx(out) end def iota(%T{shape: shape, type: type} = out, axis, backend_options) do # Build in one dimension, then broadcast axis_size = elem(shape, axis) Native.arange( 0, axis_size, to_candle_dtype(type), Tuple.duplicate(1, Nx.rank(shape)) |> put_elem(axis, axis_size), device_option(backend_options) ) |> unwrap!() |> Native.broadcast_to(shape) |> unwrap!() |> to_nx(out) end @impl true def eye(%T{shape: shape, type: type} = _out, backend_options) do iota = Nx.iota(shape, backend: {__MODULE__, backend_options}) Nx.equal(Nx.tril(iota), Nx.triu(iota)) |> Nx.as_type(type) end # Backend @impl true def backend_transfer(tensor, backend, backend_options) do if backend == __MODULE__ && same_device?(tensor, device_option(backend_options)) do tensor else try do backend_copy(tensor, backend, backend_options) after backend_deallocate(tensor) end end end @impl true def backend_copy(%T{} = tensor, Candlex.Backend, backend_options) do tensor |> from_nx() |> Native.to_device(device_option(backend_options)) |> unwrap!() |> to_nx(tensor) end def backend_copy(%T{} = tensor, backend, backend_options) do backend.from_binary(tensor, to_binary(tensor), backend_options) end @impl true def backend_deallocate(%T{} = _tensor) do true end # Conversion @impl true def to_binary(tensor, _limit \\ nil) do # TODO: don't ignore limit from_nx(tensor) |> Native.to_binary() |> unwrap!() end # Aggregates @impl true def all(%T{} = out, %T{} = tensor, opts) do case opts[:axes] do nil -> from_nx(tensor) |> Native.all() axes -> from_nx(tensor) |> Native.all_within_dims(axes, opts[:keep_axes]) end |> unwrap!() |> to_nx(out) end @impl true def any(%T{} = out, %T{} = tensor, opts) do case opts[:axes] do nil -> from_nx(tensor) |> Native.any() axes -> from_nx(tensor) |> Native.any_within_dims(axes, opts[:keep_axes]) end |> unwrap!() |> to_nx(out) end @impl true def sum(%T{type: out_type} = out, %T{} = t, opts) do axes = opts[:axes] || Nx.axes(t) keep_axes = opts[:keep_axes] || false t |> from_nx() |> Native.sum(axes, keep_axes) |> unwrap!() |> Native.to_type(to_candle_dtype(out_type)) |> unwrap!() |> to_nx(out) end for op <- [:argmax, :argmin] do @impl true def unquote(op)(%T{} = out, %T{shape: {}} = _tensor, _opts) do out |> constant(0, []) end def unquote(op)(%T{type: type} = out, %T{} = tensor, opts) do axis = opts[:axis] || -1 keep_axis = opts[:keep_axis] || false tensor |> from_nx() |> Native.unquote(op)(axis, keep_axis) |> unwrap!() # candle argmax/argmin changes to u32 |> Native.to_type(to_candle_dtype(type)) |> unwrap!() |> to_nx(out) end end @impl true def reduce_max(%T{} = out, %T{shape: {}} = tensor, _opts) do out |> from_binary(to_binary(tensor), []) end def reduce_max(%T{} = out, %T{} = tensor, opts) do axis = case opts[:axes] do nil -> 0 [] -> 0 [axis] -> axis axes -> raise "doesn't support axes option with more than 1 axis, '#{inspect(axes)}'" end keep_axis = opts[:keep_axes] || false tensor |> from_nx() |> Native.reduce_max(axis, keep_axis) |> unwrap!() |> to_nx(out) end @impl true def reduce_min(%T{} = out, %T{shape: {}} = tensor, _opts) do out |> from_binary(to_binary(tensor), []) end def reduce_min(%T{} = out, %T{} = tensor, opts) do axis = case opts[:axes] do nil -> 0 [] -> 0 [axis] -> axis axes -> raise "doesn't support axes option with more than 1 axis, '#{inspect(axes)}'" end keep_axis = opts[:keep_axes] || false tensor |> from_nx() |> Native.reduce_min(axis, keep_axis) |> unwrap!() |> to_nx(out) end # Element-wise @impl true def clip(%T{} = out, %T{} = t, %T{} = min, %T{} = max) do [t, min, max] = maybe_upcast([t, min, max]) t |> from_nx() |> Native.clamp(from_nx(min), from_nx(max)) |> unwrap!() |> to_nx(out) end @impl true def select(%T{shape: shape, type: type} = out, pred, on_true, on_false) do on_true = on_true |> from_nx() |> Native.to_type(to_candle_dtype(type)) |> unwrap!() |> Native.broadcast_to(shape) |> unwrap!() on_false = on_false |> from_nx() |> Native.to_type(to_candle_dtype(type)) |> unwrap!() |> Native.broadcast_to(shape) |> unwrap!() pred |> from_nx() |> Native.where_cond(on_true, on_false) |> unwrap!() |> to_nx(out) end # Binary ops for op <- [:add, :divide, :max, :min, :multiply, :subtract] do @impl true def unquote(op)(%T{} = out, %T{} = left, %T{} = right) do {left, right} = maybe_transfer_device(left, right) {left, right} = maybe_upcast(left, right) from_nx(left) |> Native.unquote(op)(from_nx(right)) |> unwrap!() |> to_nx(out) end end for op <- [:atan2, :pow, :quotient, :remainder] do @impl true def unquote(op)(%T{} = out, %T{} = left, %T{} = right) do {left, right} = maybe_upcast(left, right) {left, right} = maybe_broadcast_bin_args(out.shape, left, right) left |> Native.unquote(op)(right) |> unwrap!() |> to_nx(out) end end for op <- [ :bitwise_and, :bitwise_or, :bitwise_xor, :equal, :greater, :greater_equal, :left_shift, :less, :less_equal, :logical_and, :logical_or, :logical_xor, :not_equal, :right_shift ] do @impl true def unquote(op)(%T{} = out, %T{} = left, %T{} = right) do {left, right} = maybe_transfer_device(left, right) {left, right} = maybe_upcast(left, right) {left, right} = maybe_broadcast_bin_args(out.shape, left, right) left |> Native.unquote(op)(right) |> unwrap!() # TODO: Do this conditionally or as part of native op |> Native.to_type(to_candle_dtype(out.type)) |> unwrap!() |> to_nx(out) end end # Unary ops for op <- [ :abs, :acos, :acosh, :asin, :asinh, :atan, :atanh, :bitwise_not, :cbrt, :ceil, :cos, :cosh, :erf, :erfc, :erf_inv, :exp, :expm1, :floor, :is_infinity, :is_nan, :log, :log1p, :negate, :round, :rsqrt, :sigmoid, :sign, :sin, :sinh, :sqrt, :tan, :tanh ] do @impl true def unquote(op)(%T{} = out, %T{} = tensor) do tensor |> from_nx() |> Native.unquote(op)() |> unwrap!() |> to_nx(out) end end # Indexed @impl true def gather(%T{} = out, %T{shape: {_}} = tensor, %T{} = indices, _opts) do tensor |> from_nx() |> Native.gather(from_nx(Nx.flatten(indices)), 0) |> unwrap!() |> to_nx(out) end def gather(%T{} = _out, %T{} = _tensor, %T{} = _indices, _opts) do raise("unsupported gather for tensor of rank greater than 1") end @impl true def indexed_add(%T{} = out, %T{shape: {_}} = tensor, %T{} = indices, %T{} = updates, _opts) do {tensor, updates} = maybe_upcast(tensor, updates) tensor |> from_nx() |> Native.index_add(from_nx(Nx.flatten(indices)), from_nx(updates), 0) |> unwrap!() |> to_nx(out) end def indexed_add(%T{} = _out, %T{} = _tensor, %T{} = _indices, %T{} = _updates) do raise("unsupported indexed_add for tensor of rank greater than 1") end @impl true def put_slice( %T{} = out, %T{} = t, [_ | _] = start_indices, %T{shape: slice_shape} = slice ) do ranges = slice_shape |> Tuple.to_list() |> Enum.with_index(fn axis_size, i -> start_index = start_indices |> Enum.at(i) |> Nx.to_number() {start_index, start_index + axis_size - 1} end) t |> from_nx() |> Native.slice_assign(ranges, from_nx(slice)) |> unwrap!() |> to_nx(out) end @impl true def slice( %T{shape: _output_shape} = out, %T{shape: input_shape} = t, starts, lengths, _strides ) do t |> from_nx() |> narrow(starts, lengths, 0, input_shape) # TODO: Support strides # |> stride(output_shape, lengths, strides) |> to_nx(out) end @impl true def take(%T{shape: out_shape} = out, %T{} = tensor, %T{} = indexes, axis) do tensor |> from_nx() |> Native.index_select(from_nx(Nx.flatten(indexes)), axis) |> unwrap!() |> Native.reshape(out_shape) |> unwrap!() |> to_nx(out) end @impl true def take_along_axis(%T{} = out, %T{} = tensor, %T{} = indexes, axis) do tensor |> from_nx() |> Native.gather(from_nx(indexes), axis) |> unwrap!() |> to_nx(out) end # N-dim @impl true def argsort(%T{} = out, %T{shape: {_}} = tensor, opts) do 0 = opts[:axis] tensor |> from_nx() |> Native.argsort(opts[:direction] != :desc) |> unwrap!() |> to_nx(out) end def argsort(%T{} = out, %T{shape: {n, _}} = tensor, opts) do 1 = opts[:axis] tensor |> from_nx() |> Native.chunk(n) |> unwrap!() |> Enum.map(fn chunk -> chunk |> Native.squeeze(0) |> unwrap!() |> Native.argsort(opts[:direction] != :desc) |> unwrap!() end) |> Native.stack(0) |> unwrap!() |> to_nx(out) end @impl true def concatenate(%T{} = out, tensors, axis) do tensors |> maybe_upcast() |> Enum.map(&from_nx/1) |> Native.concatenate(axis) |> unwrap!() |> to_nx(out) end @impl true def conv(%T{type: out_type} = out, %T{shape: shape} = tensor, %T{} = kernel, opts) do # TODO: Support more opts unsupported_option!(opts, :batch_group_size, 1) unsupported_option!(opts, :feature_group_size, 1) # For now we assume: # input_dilation = opts[:input_dilation] # [1, 1] # kernel_dilation = opts[:kernel_dilation] # [1, 1] input_permutation = opts[:input_permutation] kernel_permutation = opts[:kernel_permutation] output_permutation = case opts[:output_permutation] do nil -> nil l -> # The permutation that Nx.Shape expects is actually the reverse permutation # for the given input l |> Enum.with_index() |> Enum.sort() |> Enum.map(&elem(&1, 1)) end native_tensor = tensor |> from_nx() |> permute(input_permutation) |> Native.to_type(to_candle_dtype(out_type)) |> unwrap!() native_kernel = kernel |> from_nx() |> permute(kernel_permutation) |> Native.to_type(to_candle_dtype(out_type)) |> unwrap!() padding = case opts[:padding] do [{p, p}] -> p [{p, p}, {p, p}] -> p p -> raise("unsupported padding #{inspect(p)}") end stride = case opts[:strides] do [s] -> s [s, s] -> s s -> raise("unsupported strides #{inspect(s)}") end conv_opts = %Candlex.Native.ConvOpts{padding: padding, stride: stride, dilation: 1, groups: 1} native_result = case Nx.rank(shape) do 3 -> Native.conv1d(native_tensor, native_kernel, conv_opts) 4 -> Native.conv2d(native_tensor, native_kernel, conv_opts) rank -> raise("unsupported conv for tensor of rank #{rank}, only 3 or 4 supported") end native_result |> unwrap!() |> permute(output_permutation) |> to_nx(out) end @impl true def dot(%T{shape: out_shape} = out, left, left_axes, [], right, right_axes, []) do # {left, right} = maybe_upcast(left, right) do_dot(left, left_axes, right, right_axes) # Reinstate 1-D axes removed by candle |> Native.reshape(out_shape) |> unwrap!() |> to_nx(out) end def dot( %T{shape: out_shape} = out, left, left_axes, [_ | _] = batched_axes, right, right_axes, batched_axes ) do do_batched_dot( left, left_axes, right, right_axes, batched_axes ) # Reinstate 1-D axes removed by candle |> Native.reshape(out_shape) |> unwrap!() |> to_nx(out) end defp do_batched_dot( %T{shape: left_shape} = left, [left_axis], %T{shape: right_shape} = right, [right_axis], [0 | batched_axes_rest] ) when elem(left_shape, 0) == elem(right_shape, 0) do Enum.zip( left |> Nx.to_batched(1), right |> Nx.to_batched(1) ) |> Enum.map(fn {l, r} -> do_batched_dot( l |> Nx.squeeze(axes: [0]), [left_axis - 1], r |> Nx.squeeze(axes: [0]), [right_axis - 1], batched_axes_rest |> Enum.map(&(&1 - 1)) ) end) |> Native.stack(0) |> unwrap!() end defp do_batched_dot(left, left_axes, right, right_axes, []) do do_dot(left, left_axes, right, right_axes) end defp do_dot( %T{shape: left_shape} = left, [left_axis], %T{shape: {_}} = right, [0] ) when tuple_size(left_shape) >= 1 and left_axis == tuple_size(left_shape) - 1 do {left, right} = maybe_upcast(left, right) from_nx(left) |> Native.dot(from_nx(right)) |> unwrap!() end defp do_dot( %T{shape: left_shape} = left, [left_axis], %T{shape: right_shape} = right, [right_axis] ) when tuple_size(left_shape) >= 2 and tuple_size(right_shape) >= 2 and elem(left_shape, left_axis) == elem(right_shape, right_axis) do {left, right} = maybe_upcast(left, right) left |> moved_axis(left_axis, tuple_size(left_shape) - 1) |> from_nx() |> Native.matmul( right |> moved_axis(right_axis, tuple_size(right_shape) - 2) |> from_nx() ) |> unwrap!() end defp moved_axis(x, axis, axis) do x end defp moved_axis(%T{} = t, axis, target_position) do t |> Nx.transpose(axes: moved_axis(Nx.axes(t), axis, target_position)) |> contiguous() end defp moved_axis([_ | _] = axes, axis, target_position) do axes |> List.delete_at(axis) |> List.insert_at(target_position, axis) end defp contiguous(t) do t |> from_nx() |> Native.contiguous() |> unwrap!() |> to_nx(t) end @impl true def reverse(%T{} = out, %T{} = tensor, axes) do tensor |> from_nx() |> Native.reverse(axes) |> unwrap!() |> to_nx(out) end # Shape @impl true def broadcast(out, %T{} = t, shape, axes) do t |> maybe_reshape(shape, axes) |> from_nx() |> Native.broadcast_to(shape) |> unwrap!() |> to_nx(out) end @impl true def pad(%T{} = out, %T{} = _t, _pad_value, []) do out end def pad(%T{} = out, %T{} = t, %T{shape: {}} = pad_value, padding_configs) do if !Nx.equal(pad_value, 0) do raise "only pad_value=0 supported for now" end padding_configs |> Enum.with_index() |> Enum.reduce( from_nx(t), fn {{low, high, 0 = _inner}, i}, acc -> acc |> Native.pad_with_zeros(i, max(low, 0), max(high, 0)) |> unwrap!() end ) |> slice_negative_padding(padding_configs) |> to_nx(out) end @impl true def reshape(%T{shape: shape} = out, %T{} = t) do from_nx(t) |> Native.reshape(shape) |> unwrap!() |> to_nx(out) end @impl true def squeeze(%T{} = out, %T{} = t, axes) do # sort the axes desc so we don't have to decrease the axis numbers after each squeeze for axis <- Enum.sort(axes, :desc), reduce: from_nx(t) do ref -> ref |> Native.squeeze(axis) |> unwrap!() end |> to_nx(out) end @impl true def transpose(out, %T{} = t, axes) do from_nx(t) |> Native.permute(axes) |> unwrap!() |> to_nx(out) end # Type @impl true def as_type(%T{type: type} = out, %T{} = t) do from_nx(t) |> Native.to_type(to_candle_dtype(type)) |> unwrap!() |> to_nx(out) end @impl true def bitcast(out, tensor) do out |> from_binary(to_binary(tensor), []) end # Window @impl true def window_max(%T{type: out_type} = out, tensor, {1, 1, dx, dy} = _window_dimensions, opts) do strides = case opts[:strides] do [1, 1, sx, sy] -> {sx, sy} s -> raise("unsupported strides #{inspect(s)}") end [{0, 0}, {0, 0}, {px_left, px_right}, {py_left, py_right}] = opts[:padding] tensor |> from_nx() |> Native.to_type(to_candle_dtype(out_type)) |> unwrap!() |> Native.pad_with_same(2, px_left, px_right) |> unwrap!() |> Native.pad_with_same(3, py_left, py_right) |> unwrap!() |> Native.max_pool2d({dx, dy}, strides) |> unwrap!() |> to_nx(out) end def window_max(%T{} = _out, tensor, {1, dx, dy, 1}, opts) do [1, 1, 1, 1] = opts[:window_dilations] [1, sx, sy, 1] = opts[:strides] [{0, 0}, {px, px}, {py, py}, {0, 0}] = opts[:padding] tensor |> Nx.transpose(axes: [0, 3, 1, 2]) |> Nx.window_max( {1, 1, dx, dy}, strides: [1, 1, sx, sy], padding: [{0, 0}, {0, 0}, {px, px}, {py, py}] ) |> Nx.transpose(axes: [0, 2, 3, 1]) end @impl true def window_sum(%T{} = out, %T{} = tensor, {1, 1, dx, dy}, opts) do [1, 1, 1, 1] = opts[:window_dilations] [1, 1, sx, sy] = opts[:strides] [{0, 0}, {0, 0}, {px_left, px_right}, {py_left, py_right}] = opts[:padding] tensor |> from_nx() |> Native.pad_with_same(2, px_left, px_right) |> unwrap!() |> Native.pad_with_same(3, py_left, py_right) |> unwrap!() |> Native.sum_pool2d({dx, dy}, {sx, sy}) |> unwrap!() |> to_nx(out) end def window_sum(%T{} = _out, tensor, {1, dx, dy, 1}, opts) do [1, 1, 1, 1] = opts[:window_dilations] [1, sx, sy, 1] = opts[:strides] [{0, 0}, {px, px}, {py, py}, {0, 0}] = opts[:padding] tensor |> Nx.transpose(axes: [0, 3, 1, 2]) |> Nx.window_sum( {1, 1, dx, dy}, strides: [1, 1, sx, sy], padding: [{0, 0}, {0, 0}, {px, px}, {py, py}] ) |> Nx.transpose(axes: [0, 2, 3, 1]) end # Inspect @impl true def inspect(%T{} = tensor, inspect_opts) do limit = if inspect_opts.limit == :infinity, do: :infinity, else: inspect_opts.limit + 1 tensor |> to_binary(min(limit, Nx.size(tensor))) |> then(&Nx.Backend.inspect(tensor, &1, inspect_opts)) |> maybe_add_signature(tensor) end defp maybe_add_signature(result, %T{data: %__MODULE__{device: device, resource: ref}}) when is_reference(ref) do Inspect.Algebra.concat([ "Candlex.Backend(#{device})", Inspect.Algebra.line(), result ]) end defp narrow(t, [start | starts], [length | lengths], axis, shape) do dim = elem(shape, axis) start = start |> Nx.to_number() |> min(dim - length) if start == 0 and length == dim do # Nothing to narrow at this step t else t |> Native.narrow(axis, start, length) |> unwrap!() end |> narrow(starts, lengths, axis + 1, shape) end defp narrow(t, [], [], _axis, _shape), do: t defp maybe_reshape(%T{shape: {}} = t, target_shape, _axes) do shape = 1 |> List.duplicate(tuple_size(target_shape)) |> List.to_tuple() t |> Nx.reshape(shape) end defp maybe_reshape(%T{shape: shape} = t, target_shape, axes) do base_broadcast_shape = 1 |> List.duplicate(tuple_size(target_shape)) |> List.to_tuple() new_shape = shape |> Tuple.to_list() |> Enum.zip(axes) |> Enum.reduce(base_broadcast_shape, fn {dim_size, target_axis}, shape_acc -> shape_acc |> Tuple.delete_at(target_axis) |> Tuple.insert_at(target_axis, dim_size) end) t |> Nx.reshape(new_shape) end defp maybe_upcast(%T{type: t} = left, %T{type: t} = right) do {left, right} end defp maybe_upcast(left, right) do type = Nx.Type.merge(left.type, right.type) {Nx.as_type(left, type), Nx.as_type(right, type)} end defp maybe_upcast([first | _] = tensors) do type = tensors |> Enum.reduce( first.type, fn tensor, type -> Nx.Type.merge(type, tensor.type) end ) tensors |> Enum.map(fn tensor -> Nx.as_type(tensor, type) end) end defp maybe_broadcast_bin_args(out_shape, l, r) do { case l.shape do ^out_shape -> from_nx(l) _ -> l |> from_nx() |> Native.broadcast_to(out_shape) |> unwrap!() end, case r.shape do ^out_shape -> from_nx(r) _ -> r |> from_nx() |> Native.broadcast_to(out_shape) |> unwrap!() end } end defp maybe_transfer_device( %T{data: %__MODULE__{device: device}} = l, %T{data: %__MODULE__{device: device}} = r ) do {l, r} end defp maybe_transfer_device( %T{data: %__MODULE__{device: device}} = l, %T{data: %__MODULE__{device: _other_device}} = r ) do { l, r |> Nx.backend_transfer({__MODULE__, device: device}) } end defp maybe_transfer_device(%T{} = l, %T{data: %__MODULE__{device: device}} = r) do { l |> Nx.backend_transfer({__MODULE__, device: device}), r } end defp maybe_transfer_device(%T{data: %__MODULE__{device: device}} = l, %T{} = r) do { l, r |> Nx.backend_transfer({__MODULE__, device: device}) } end ## Conversions @impl true def to_batched(%T{shape: out_shape} = out, %T{shape: shape} = t, opts) do leftover = opts[:leftover] first_dimension = 0 batch_size = elem(out_shape, first_dimension) axis_total = elem(shape, first_dimension) remainder = rem(axis_total, batch_size) num_batches = div(axis_total, batch_size) native_tensor = from_nx(t) cond do remainder == 0 -> native_tensor |> Native.chunk(num_batches) |> unwrap!() remainder > 0 && leftover == :repeat -> [ native_tensor, Native.narrow(native_tensor, first_dimension, 0, batch_size - remainder) |> unwrap!() ] |> Native.concatenate(first_dimension) |> unwrap!() |> Native.chunk(num_batches + 1) |> unwrap!() true -> raise "not implemented" end |> Stream.map(&to_nx(&1, out)) end for op <- [ :cholesky, :conjugate, :count_leading_zeros, :imag, :population_count, :real ] do @impl true def unquote(op)(_out, _tensor) do raise "unsupported Candlex.Backend.#{unquote(op)} function" end end for op <- [ :eigh, :fft, :ifft, :lu, :product, :qr, :sort ] do @impl true def unquote(op)(_out, _tensor, _) do raise "unsupported Candlex.Backend.#{unquote(op)} function" end end for op <- [ :map, # TODO: Remove after nx 0.7 is released :random_normal, # TODO: Remove after nx 0.7 is released :random_uniform, :triangular_solve, :window_min, :window_product ] do @impl true def unquote(op)(_out, _tensor, _, _) do raise "unsupported Candlex.Backend.#{unquote(op)} function" end end for op <- [ :indexed_put, :reduce ] do @impl true def unquote(op)(_out, _tensor, _, _, _) do raise "unsupported Candlex.Backend.#{unquote(op)} function" end end for op <- [ :window_reduce, :window_scatter_max, :window_scatter_min ] do @impl true def unquote(op)(_out, _tensor, _, _, _, _) do raise "unsupported Candlex.Backend.#{unquote(op)} function" end end defp permute(native_tensor, permutation) do native_tensor |> Native.permute(permutation) |> unwrap!() end defp slice_negative_padding(native_tensor, padding_configs) do if Enum.any?(padding_configs, fn {low, high, _} -> low < 0 or high < 0 end) do shape = native_tensor |> Native.t_shape() |> unwrap!() {starts, lengths} = padding_configs |> Enum.with_index(fn {low, high, 0 = _inner}, i -> axis_size = elem(shape, i) start = max(-low, 0) { start, axis_size - start + min(high, 0) } end) |> Enum.unzip() native_tensor |> narrow(starts, lengths, 0, shape) else native_tensor end end @doc false defp from_nx(%T{data: %__MODULE__{} = data}), do: data defp from_nx(%T{} = tensor) do tensor |> Nx.backend_transfer(__MODULE__) |> from_nx() end defp to_nx(%__MODULE__{resource: ref} = backend_tensor, %T{type: nx_type, shape: nx_shape} = t) when is_reference(ref) do {:ok, candle_dtype} = Native.dtype(backend_tensor) {:ok, candle_shape} = Native.t_shape(backend_tensor) case {nx_type, from_candle_dtype(candle_dtype)} do {{:u, 64}, {:s, 64}} -> :ok {type, type} -> :ok {type, other_type} -> raise "tensor type mismatch, Nx (#{inspect(type)}) and Candle (#{inspect(other_type)})" end if nx_shape != candle_shape do raise "tensor shape mismatch, Nx (#{inspect(nx_shape)}) and Candle (#{inspect(candle_shape)})" end %{t | data: backend_tensor} end defp to_candle_dtype({:s, 8} = t), do: unsupported_dtype(t) defp to_candle_dtype({:s, 16} = t), do: unsupported_dtype(t) defp to_candle_dtype({:s, 32} = t), do: unsupported_dtype(t) defp to_candle_dtype({:s, 64}), do: "i64" defp to_candle_dtype({:u, 8}), do: "u8" defp to_candle_dtype({:u, 16} = t), do: unsupported_dtype(t) defp to_candle_dtype({:u, 32}), do: "u32" defp to_candle_dtype({:u, 64}), do: "i64" defp to_candle_dtype({:f, 16}), do: "f16" defp to_candle_dtype({:f, 32}), do: "f32" defp to_candle_dtype({:f, 64}), do: "f64" defp to_candle_dtype({:bf, 16}), do: "bf16" defp to_candle_dtype({:c, 64} = t), do: unsupported_dtype(t) defp to_candle_dtype({:c, 128} = t), do: unsupported_dtype(t) defp from_candle_dtype("i64"), do: {:s, 64} defp from_candle_dtype("u8"), do: {:u, 8} defp from_candle_dtype("u32"), do: {:u, 32} defp from_candle_dtype("f16"), do: {:f, 16} defp from_candle_dtype("bf16"), do: {:bf, 16} defp from_candle_dtype("f32"), do: {:f, 32} defp from_candle_dtype("f64"), do: {:f, 64} defp device_option(nil) do default_device() end defp device_option(backend_options) do backend_options[:device] || default_device() end defp default_device do if cuda_available?() do @device_cuda else @device_cpu end end defp same_device?(%T{data: %__MODULE__{device: device}}, device) do true end defp same_device?(_t, _d) do false end def cuda_available? do Native.is_cuda_available() end defp unsupported_dtype(t) do raise("Unsupported candle dtype for #{inspect(t)}") end defp unsupported_option!(opts, key, acceptable_default) do if opts[key] != nil and opts[key] != acceptable_default do raise "#{inspect(key)} option with #{inspect(opts[key])} is not supported" end end defp unwrap!({:ok, result}), do: result defp unwrap!({:error, error}), do: raise("Candlex: #{error}") end