Skip to content

Lift buffer donation to Nx.Defn - #1795

Open
Chapaman wants to merge 9 commits into
elixir-nx:mainfrom
Chapaman:sm-buffer-donation-nx
Open

Lift buffer donation to Nx.Defn#1795
Chapaman wants to merge 9 commits into
elixir-nx:mainfrom
Chapaman:sm-buffer-donation-nx

Conversation

@Chapaman

Copy link
Copy Markdown
Contributor

Builds on @seanmor5's EXLA buffer-donation work in #1755 (merged that branch onto current main).

seanmor5 and others added 4 commits May 25, 2026 18:20
Expose donate/1 and :donate_argnums on Nx.Defn so compilers share one
interface and callers can donate whole args or partial containers,
addressing review feedback on elixir-nx#1755.
@Chapaman
Chapaman force-pushed the sm-buffer-donation-nx branch from 9debef2 to 71ec4ea Compare July 20, 2026 23:17
@Chapaman Chapaman changed the title Introduce buffer donation construct Lift buffer donation to Nx.Defn Jul 21, 2026
@Chapaman
Chapaman marked this pull request as ready for review July 21, 2026 00:52
@josevalim

Copy link
Copy Markdown
Contributor

@Chapaman @polvalente if we are planning to go ahead with this, we should discuss a better API for it, because the argnum approach is not very intuitive for complex data structures once we have maps, structs, etc. Perhaps some sort of defn annotation?

@polvalente

Copy link
Copy Markdown
Contributor

This is what I was thinking. Defn annotation would be great for defn graph too. The one thing Handoff is missing to deploy Axon accross a network is args annotated as parameters.

The correct abstraction could solve both problems. But I'm prioritizing this as the latter is solvable by other means.

@josevalim

Copy link
Copy Markdown
Contributor

I believe we should at least spend a bit of time discussing some options. We have been working hard to stabilize the API for v1.0. Rushing and adding an API that we are aware to be inferior just before v1.0 goes counter to that effort. Some options I have in mind:

  1. Introduce EXLA.donatable or similar to mark the tensor as donatable before calling the backend. We then store a field in the EXLA.Backend struct. This function can use Nx.Container to traverse the elements. We could make this part of Nx itself as well (then it is just a new backend callback).

  2. Introduce a function to Nx.Defn.Kernel that marks some AST nodes as donatable and traverse them. This seems to be hard.

Comment thread nx/lib/nx/defn/compiler.ex Outdated
end

# Strip so donatable does not leak through template copies / expr params.
template = %{template | donatable: false}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we're stripping information from compilers too early, and this ends up just becoming available again at runtime when the lazy params get unwrapped. I believe certain compilers (perhaps a future OpenCL one, or even the EMLX compiler) might be able to leverage this information at compile-time. For Torchx runtime is fine.

Keeping this would also allow us to not accumulate donated, and therefore not have to add new info to the :options, which can break some caching strategies.

Comment on lines -871 to +893
def to_lazy_params_sharded(fun, args_list) do
def to_lazy_params_sharded(fun, args_list, opts \\ []) do
# Multiple args lists (for sharding): [[args1], [args2], [args3]]
# Returns: {fun, params, templates, [flatten1, flatten2, flatten3]}
# Each flatten is kept separate - we do NOT concatenate them
[first_args | rest_args] = args_list
{fun, params, templates, first_flatten} = to_lazy_params(fun, first_args)
{fun, params, templates, first_flatten} = to_lazy_params(fun, first_args, opts)

# Build flattens for remaining args lists
# Each flatten remains separate: [[lazy1, lazy2], [lazy3, lazy4], ...]
rest_flattens =
Enum.map(rest_args, fn args ->
{_, _, _, flatten} = to_lazy_params(fun, args)
{_, _, _, flatten} = to_lazy_params(fun, args, opts)
flatten
end)

{fun, params, templates, [first_flatten | rest_flattens]}
end

@doc false
def to_lazy_params(fun, args) do
def to_lazy_params(fun, args, _opts \\ []) do
{params, cache, {templates, funs, _}} =
Enum.reduce(args, {[], [], {[], [], 0}}, fn
args
|> Enum.reduce({[], [], {[], [], 0}}, fn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we can revert this file completely!

{params, cache, {templates, funs, _}} =
Enum.reduce(args, {[], [], {[], [], 0}}, fn
args
|> Enum.reduce({[], [], {[], [], 0}}, fn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changes to this file do not seem necessary?

Comment thread nx/lib/nx/defn.ex
be donated. Marking live arguments with `Nx.donate/1` only when invoking
the compiled function does not enable donation if the template was not
donatable; omitting `donate/1` on invoke also does not disable donation
already baked from templates. See `Nx.donate/1`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we please not have public docs generated by agents? :)

Comment thread nx/lib/nx/defn.ex

{fun, params, _templates, args_list} =
Nx.Defn.Compiler.to_lazy_params_sharded(fun, args_list, opts)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Many of the changes to this file do not seem to be necessary either?

Comment thread nx/lib/nx/tensor.ex

donatable =
if tensor.donatable do
concat([line(), "donatable: true"])

@josevalim josevalim Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of a newline, maybe we show just [donatable] after the shape?

Comment thread nx/lib/nx.ex

"""
@doc type: :conversion
def donate(tensor_or_container) do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure I like the name donate in active tense because we are really not donating at this moment.

Comment thread nx/lib/nx/tensor.ex

@type t :: %Nx.Tensor{data: data, type: type, shape: shape, names: [name]}
@type t(data) :: %Nx.Tensor{data: data, type: type, shape: shape, names: [name]}
@type t :: %Nx.Tensor{data: data, type: type, shape: shape, names: [name], donatable: boolean}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this be dontable??

Suggested change
@type t :: %Nx.Tensor{data: data, type: type, shape: shape, names: [name], donatable: boolean}
@type t :: %Nx.Tensor{data: data, type: type, shape: shape, names: [name], donatable?: boolean}

Comment thread nx/lib/nx.ex
"""
@doc type: :conversion
def donatable?(%T{donatable: true}), do: true
def donatable?(%T{}), do: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def donatable?(%T{}), do: false
def donatable?(t) when is_tensor(t), do: false

Please also add a test for it testing integers or complex numbers as arguments.

Comment thread nx/lib/nx.ex
indices,
updates,
[axes: axes]
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change and a bunch of changes below and above seem to be unrelated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants