Skip to content

Accept containers of tensors as Nx.block arguments - #1895

Merged
polvalente merged 5 commits into
elixir-nx:mainfrom
blasphemetheus:feat/block-list-callback
Oct 26, 2026
Merged

polvalente merged 5 commits into
elixir-nx:mainfrom
blasphemetheus:feat/block-list-callback

Conversation

@blasphemetheus

@blasphemetheus blasphemetheus commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Nx.block/4 now accepts a container of tensors, such as a tuple or a map, as an argument.

A block's default implementation receives the struct and then one argument per input, and an Erlang function cannot take more than 255 arguments. A block with hundreds of inputs, such as a checkpointed group of layers whose weights all arrive as block arguments, could not be written. With containers, those inputs are passed as a single tuple or map, and the callback keeps its one-argument-per-input shape:

Nx.block(%MyBlock{}, [x, %{scale: scale, bias: bias}], nil, fn _, x, %{scale: scale, bias: bias} ->
  Nx.add(Nx.multiply(x, scale), bias)
end)

This is part of the gradient checkpointing work in #1865. It replaces the list-form callback this PR first proposed, following the review suggestion to accept a container instead.

How it works

The block node keeps its arguments as the user wrote them, containers included. Everything that walks the arguments walks into the containers:

  • The tracer converts each tensor inside a container to an expression, creates one body parameter per tensor in traversal order, and gives the user's function containers of parameters. Vectorized axes are tracked per tensor, so a container can mix vectorized and plain entries. The wrapper that re-vectorizes arguments counts a container as one argument.
  • Nx.Defn.Tree.apply_args/4, the Evaluator, Nx.Defn.Graph and inspect traverse containers and leave keyword options alone.
  • EXLA flattens the arguments into the compiled function's parameters in the same traversal order the tracer used. Native lowerings and EXLA.CustomCall see the arguments as written.
  • Nx.block/4 and the Evaluator pick the backend from the tensors inside the containers.

Backends that run the default implementation with apply(fun, [struct | args]) need no change, since the containers pass straight through to the function. The gradient re-traces the body from the stored arguments, so it needs no change either.

Tests

Nx: a map argument alongside a tensor, a tuple with a vectorized entry, a gradient through a tuple argument, a tuple of 300 tensors forward and gradient, and a graph split whose boundary runs through a container argument. EXLA: a tuple with a vectorized entry forward and gradient, and a tuple of 300 tensors. Full Nx suite and EXLA block tests on host pass.

🤖 Generated with Claude Code

@polvalente polvalente left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe this makes for a confusing interface. Does block accept a container as one of the arguments? If not, then that's the proper fix, I believe

A block's default implementation receives the struct and then one
argument per input, so a block with hundreds of inputs ran into the
255-argument limit on Erlang functions. An argument may now be a tuple,
map or other container of tensors, which lets such a block take all of
its inputs as one argument.

The block node stores the arguments as written. The tracer creates one
body parameter per tensor inside a container in traversal order and
gives the function containers of parameters, tracking vectorized axes
per tensor. Tree.apply_args, the Evaluator, Graph and inspect traverse
containers and leave keyword options alone. EXLA flattens the arguments
into the compiled function's parameters in the same order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blasphemetheus
blasphemetheus force-pushed the feat/block-list-callback branch from f1d4285 to 68367b6 Compare October 25, 2026 18:29
@blasphemetheus blasphemetheus changed the title Let a block callback take the argument list past the arity limit Accept containers of tensors as Nx.block arguments Oct 25, 2026
Adds an Nx.block example with a tuple argument, checks a boolean output
alongside the vectorized container case, and trims a tracer comment that
repeated the docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
blasphemetheus and others added 2 commits October 25, 2026 13:59
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EXLA.Backend.block split the arguments on tensor structs, so a container
argument fell into the trailing options and reached the tracer as a
closure. Split on keyword lists instead, pick the client from the tensors
inside the containers, and clear the donatable flag inside them as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@polvalente
polvalente merged commit ebe2b9f into elixir-nx:main Oct 26, 2026
9 checks passed
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.

2 participants