Summary
Every Emily.Fast fused kernel (rms_norm, layer_norm, rope,
rope_with_freqs, and all four scaled_dot_product_attention* variants)
raises RuntimeError: cannot invoke Emily.Fast.<kernel> inside defn because it was not defined with defn when called from inside a defn-defined
function nested one level below the top-level jit_apply/defn entry
point — even though the module's own moduledoc documents this exact usage
as supported ("Call these from inside a defn or Nx.Defn.jit-traced
function, alongside regular Nx ops").
Environment
emily 1.0.0 (Hex), MLX 0.32.0
- macOS, Apple Silicon
elixir 1.18.4 / OTP 28
nx 0.12.1
- Both
Emily.Compiler with native: true and without (plain
Nx.Defn.Evaluator) — same failure either way.
Repro
The moduledoc's own example, run two ways:
defmodule DebugFastDefn do
import Nx.Defn
defn block(x, w) do
Emily.Fast.rms_norm(x, w, eps: 1.0e-5)
end
end
x = Nx.tensor([[1.0, 2.0, 3.0, 4.0]], backend: Emily.Backend)
w = Nx.tensor([1.0, 1.0, 1.0, 1.0], backend: Emily.Backend)
Works — calling the moduledoc's Nx.Defn.jit_apply form directly, with
the anonymous function as the top-level jit'd unit:
Nx.Defn.jit_apply(fn x, w -> Emily.Fast.rms_norm(x, w, eps: 1.0e-5) end, [x, w])
#=> #Nx.Tensor<...> -- succeeds
Fails — the exact same call, one level deeper, wrapped in a
module-level defn (i.e. the moduledoc's own defn block(x, w) do ... end
example, called via DebugFastDefn.block(x, w) or via
Nx.Defn.jit_apply(&DebugFastDefn.block/2, [x, w], compiler: Emily.Compiler, native: true)):
** (RuntimeError) cannot invoke Emily.Fast.rms_norm/3 inside defn because it was not defined with defn
(nx 0.12.1) lib/nx/defn/compiler.ex:190: Nx.Defn.Compiler.runtime_fun/3
...
Confirmed reproducible under both compiler: Emily.Compiler (with and
without native: true) and plain Nx.Defn.Evaluator (no Emily.Compiler
configured at all) — identical error either way.
Root cause (traced through lib/emily/fast.ex)
Every fused kernel wraps its Nx.block/4 fallback in a plain def,
not defnp:
# lib/emily/fast.ex
def rms_norm(x, weight, opts \\ []) do
opts = Keyword.validate!(opts, eps: 1.0e-6)
block = struct!(FB.RMSNorm, opts)
Nx.block(block, [x, weight], output_like(x), fn ^block, x, weight ->
rms_norm_fallback(x, weight, opts) # <-- plain def, not defnp
end)
end
defp rms_norm_fallback(x, weight, opts) do
...
end
Nx.block/4's fallback function argument must itself be defn-traceable.
When the outer call is the literal top-level unit handed to
Nx.Defn.jit_apply/defn, tracing apparently short-circuits before this
matters. Once Emily.Fast.rms_norm/3 is called from inside an
already-defn-traced function (i.e. genuine composition — the moduledoc's
own stated use case), the trace has to actually enter Nx.block's fallback
path, and hits the plain-def rms_norm_fallback/2 — hence "was not
defined with defn."
This isn't rms_norm-specific — the identical pattern (Nx.block(..., fn ... -> some_plain_def_fallback(...) end)) appears in every fused kernel in
this module: layer_norm/layer_norm_fallback,
rope/rope_fallback, rope_with_freqs/rope_freqs_fallback,
scaled_dot_product_attention/sdpa_fallback and sdpa_sinks_fallback,
scaled_dot_product_attention_with_mask/sdpa_masked_fallback and
sdpa_masked_sinks_fallback.
Impact
This blocks a real use case: composing multiple fused kernels (e.g. several
transformer layers' worth of rms_norm + rope + SDPA) into one larger
defn-traced graph, to get Emily.Compiler's native single-NIF
whole-graph replay speedup (per #163's own numbers, ~5x on a decode loop)
across the composed graph rather than dispatching each fused kernel as its
own separate top-level call. Right now, the only way to call any
Emily.Fast kernel successfully is as the literal top-level jit'd unit —
never nested inside a caller's own defn.
Suggested fix
Change each *_fallback function from defp to defnp (and correspondingly
any helper functions those fallbacks call that aren't already defn-safe).
Given the fallback bodies already appear to be pure Nx composition (matmuls,
elementwise ops, reductions — nothing outside defn's supported op set, based
on reading rms_norm_fallback/layer_norm_fallback), this looks like it
should be a mechanical, low-risk change, but I haven't attempted the fix
myself or checked whether it's contained to this file.
Acceptance criteria (suggested)
- The moduledoc's own
defn block(x, w) do Emily.Fast.layer_norm(w, b, ...) end
example succeeds when called through a caller's defn, not just as the
top-level jit'd unit.
- Same for
rope, rope_with_freqs, and both SDPA variants.
- A regression test nesting at least one
Emily.Fast call two levels deep
(a defn calling a defnp calling Emily.Fast.*) so this doesn't
regress silently.
Summary
Every
Emily.Fastfused kernel (rms_norm,layer_norm,rope,rope_with_freqs, and all fourscaled_dot_product_attention*variants)raises
RuntimeError: cannot invoke Emily.Fast.<kernel> inside defn because it was not defined with defnwhen called from inside adefn-definedfunction nested one level below the top-level
jit_apply/defnentrypoint — even though the module's own moduledoc documents this exact usage
as supported ("Call these from inside a
defnorNx.Defn.jit-tracedfunction, alongside regular
Nxops").Environment
emily1.0.0 (Hex), MLX 0.32.0elixir1.18.4 / OTP 28nx0.12.1Emily.Compilerwithnative: trueand without (plainNx.Defn.Evaluator) — same failure either way.Repro
The moduledoc's own example, run two ways:
Works — calling the moduledoc's
Nx.Defn.jit_applyform directly, withthe anonymous function as the top-level jit'd unit:
Fails — the exact same call, one level deeper, wrapped in a
module-level
defn(i.e. the moduledoc's owndefn block(x, w) do ... endexample, called via
DebugFastDefn.block(x, w)or viaNx.Defn.jit_apply(&DebugFastDefn.block/2, [x, w], compiler: Emily.Compiler, native: true)):Confirmed reproducible under both
compiler: Emily.Compiler(with andwithout
native: true) and plainNx.Defn.Evaluator(noEmily.Compilerconfigured at all) — identical error either way.
Root cause (traced through
lib/emily/fast.ex)Every fused kernel wraps its
Nx.block/4fallback in a plaindef,not
defnp:Nx.block/4's fallback function argument must itself bedefn-traceable.When the outer call is the literal top-level unit handed to
Nx.Defn.jit_apply/defn, tracing apparently short-circuits before thismatters. Once
Emily.Fast.rms_norm/3is called from inside analready-
defn-traced function (i.e. genuine composition — the moduledoc'sown stated use case), the trace has to actually enter
Nx.block's fallbackpath, and hits the plain-
defrms_norm_fallback/2— hence "was notdefined with defn."
This isn't
rms_norm-specific — the identical pattern (Nx.block(..., fn ... -> some_plain_def_fallback(...) end)) appears in every fused kernel inthis module:
layer_norm/layer_norm_fallback,rope/rope_fallback,rope_with_freqs/rope_freqs_fallback,scaled_dot_product_attention/sdpa_fallbackandsdpa_sinks_fallback,scaled_dot_product_attention_with_mask/sdpa_masked_fallbackandsdpa_masked_sinks_fallback.Impact
This blocks a real use case: composing multiple fused kernels (e.g. several
transformer layers' worth of
rms_norm+rope+ SDPA) into one largerdefn-traced graph, to getEmily.Compiler's native single-NIFwhole-graph replay speedup (per #163's own numbers, ~5x on a decode loop)
across the composed graph rather than dispatching each fused kernel as its
own separate top-level call. Right now, the only way to call any
Emily.Fastkernel successfully is as the literal top-level jit'd unit —never nested inside a caller's own
defn.Suggested fix
Change each
*_fallbackfunction fromdefptodefnp(and correspondinglyany helper functions those fallbacks call that aren't already
defn-safe).Given the fallback bodies already appear to be pure
Nxcomposition (matmuls,elementwise ops, reductions — nothing outside
defn's supported op set, basedon reading
rms_norm_fallback/layer_norm_fallback), this looks like itshould be a mechanical, low-risk change, but I haven't attempted the fix
myself or checked whether it's contained to this file.
Acceptance criteria (suggested)
defn block(x, w) do Emily.Fast.layer_norm(w, b, ...) endexample succeeds when called through a caller's
defn, not just as thetop-level jit'd unit.
rope,rope_with_freqs, and both SDPA variants.Emily.Fastcall two levels deep(a
defncalling adefnpcallingEmily.Fast.*) so this doesn'tregress silently.