Skip to content

Native Expr compiler: op coverage checklist (37 ops not yet lowered) #188

Description

@ausimian

Context

Emily.Compiler with native: true lowers a traced Nx.Defn.Expr to flat IR and replays it in one NIF call. Anything the IR can't lower falls back to Nx.Defn.Evaluator (the default native_fallback: :eval) or raises (:raise, used by the conformance suite and scripts/test-livebooks-native.sh).

This issue inventories the ops that don't lower yet. Each one is reachable only if a user model uses it: none of the bundled livebooks (scripts/test-livebooks-native.sh passes with native_fallback: :raise) and none of the conformance suites hit any of these. Adding a lowering removes a silent-fallback cliff for user code.

How this list was produced

scripts/expr_op_coverage.exs builds a tiny Nx.Defn.jit over each Nx op (via the public Nx API), runs it through compiler: Emily.Compiler, native: true, native_fallback: :raise, and reports whether it lowered. Re-run after each PR to keep this list honest:

mix run scripts/expr_op_coverage.exs

The script's final section prints a markdown checklist in the same format used below.

Checklist

All clusters complete. Probe is down to 2 misses, both in "Unlowerable by design" below.

Easy wins (unary elementwise — pattern is a single line in @unary_ops + opcode)

The IR's @unary_ops map (lib/emily/ir.ex:290) covers 16 ops. These are the missing peers — each has a Backend implementation already (Emily.Backend.@renamed_unary / @direct_unary), so the lowering is one map entry plus an opcode if MLX has the primitive:

Binary / compare gaps

Top-level Expr ops

Nx.Block structs

Linalg blocks (heavier — multi-output, mostly via mx::linalg::*)

Emily-internal blocks (gpt-oss attention sinks)

These don't appear in the probe script (it walks Nx-public ops only) but are reachable from Emily.Fast.attention_with_sinks:

Unlowerable by design (don't add to the checklist)

These will always raise under the native path — they need a host bridge mid-graph that the single-NIF replay can't provide. They are correctly handled today: :eval mode routes the whole defn through the Evaluator (which has its own per-op via_binary fallback), and :raise mode surfaces the limitation loudly.

  • Nx.reduce / Nx.window_reduce with a user-supplied BEAM reducer (lib/emily/ir.ex:950)
  • Nx.Defn.Kernel.hook / :attach_token with active hooks (lib/emily/ir.ex:936)
  • Nx.count_leading_zeros, Nx.population_count — MLX has no primitive (Backend also raises)

Out of scope for this probe

The script doesn't probe control flow (cond, while) or window_scatter_max/min directly because they need defn macro context, not bare Nx.Defn.jit. All three are already lowered (while via CM10/CM11, tuple cond via CM21, window_scatter_max/min via the CNN-training milestone) — coverage is verified by test/emily/compiler_while_test.exs, test/emily/compiler_control_flow_test.exs, and test/emily/backend_window_scatter_test.exs.

Prioritisation suggestion

The unary block is by far the largest cluster and individually the cheapest — each is a one-line addition to @unary_ops plus an opcode + C++ dispatch. Knocking those out first shrinks the list dramatically and removes the silent-fallback cliff for the most common user code (trig / numeric stability ops). The LinAlg blocks are the highest-value remaining gap for scientific-computing users but each needs more thought (multi-output, dtype matrix, MLX kernel coverage).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions