Skip to content

Materialise non-contiguous runtime_call arguments - #138

Open
Codcore wants to merge 4 commits into
elixir-nx:mainfrom
Codcore:fix-runtime-call-broadcast
Open

Materialise non-contiguous runtime_call arguments#138
Codcore wants to merge 4 commits into
elixir-nx:mainfrom
Codcore:fix-runtime-call-broadcast

Conversation

@Codcore

@Codcore Codcore commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #134.

emlx_runtime_call_bridge.hpp sized each argument binary with nbytes() — the logical size — and then copied that many bytes straight out of data(). For a non-contiguous array that buffer holds fewer elements than the logical size, so the copy ran past its end: the callback saw the first element and whatever followed it, which read as zeros.

The fix mirrors what to_blob_term already does for the same reason: when the array is not row-contiguous, materialise a row-major copy and read from that.

Three regression tests in expr_test.exs cover a broadcast argument, a transposed argument, and a callback that reduces over every element. They fail without the bridge change and pass with it, and nothing else in the suite moves either way.

On the correction in the issue thread: agreed, and the plugin path is what the model this came from actually ends up using — Nx.runtime_call inside a compiled program cost about 7.5 ms per call against roughly 0.08 ms for the same kernel as a plugin node, so it was never viable as the hot path. The bug is worth fixing regardless, since runtime_call still corrupts data silently wherever it is used, and it is the first thing anyone reaches for before discovering the plugin pattern. Happy to send a docs note pointing at EMLXAxon from the runtime_call docs if that would help the next person find it sooner.

A tensor with zero strides — anything produced by Nx.broadcast/2 — reached
an Nx.runtime_call/4 callback with only its first element intact when the
enclosing function was compiled with compiler: EMLX.

The bridge sized the argument binary with nbytes(), which is the logical
size, and then copied that many bytes straight out of data(). A
non-contiguous array holds fewer elements than its logical size, so the
copy ran past the end of the buffer: the first element survived and the
rest was whatever followed.

Materialise a row-major copy first when the array is not row-contiguous,
the same fallback to_blob_term already uses for the same reason.

Broadcast tensors are ordinary values in model code — a scalar gate
expanded over a batch, an all-ones norm weight, a repeated index vector —
so the failure was silent and data-dependent: shapes were right and
nothing was raised, only the numbers were wrong.

The three regression tests fail without the bridge change and pass with
it, covering a broadcast argument, a transposed argument, and a callback
that reduces over every element.
Comment thread emlx/c_src/emlx_runtime_call_bridge.hpp Outdated
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.

Nx.runtime_call receives stride-0 tensors with only the first element intact under compiler: EMLX

2 participants