Skip to content

Commit 07ea0c2

Browse files
committed
fix: lower tuple-returning cond (multi-output) natively
A `cond`/`if` whose branches return a tuple crashed the native compiler with a FunctionClauseError: the :cond lowering passed the tuple branch straight to lower_node/2, which only matches a single tensor. Worse, a FunctionClauseError escapes the compiler's graceful-fallback rescue (which only traps ArgumentError), so it hard-faulted instead of degrading to the evaluator. Surfaced by a Whisper encoder forward under native: true (transpose(elem(cond -> {f32[1,1500,6,64] x N}, i))). Lower a tuple cond to one where-chain per leaf position — identical wholesale-select semantics to the single-output cond (the predicate is a whole-tensor scalar bool; every branch is still computed) — and return a {:multi_refs, [...]} handle that :elem projects, shared across sibling :elems via lower_node's memo (like while). A nested / non-tensor container raises a clean ArgumentError, so it falls back gracefully rather than crashing. - ir.ex: split the :cond clause (single-tensor head unchanged; new is_tuple head for the multi-output case); extend :elem to project {:multi_refs, refs}; add the tensors?/1 guard. - compiler_control_flow_test.exs: tuple if + multi-clause tuple cond with mixed-shape (vector + scalar) leaves, native-vs-evaluator bit-identical.
1 parent 7988ca8 commit 07ea0c2

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

lib/emily/ir.ex

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ defmodule Emily.IR do
870870
# gather/index there clamps rather than faults, so the discarded value
871871
# never changes the result; a hard-faulting op on a not-taken path would
872872
# diverge from the Evaluator's lazy single-branch eval.
873-
defp lower_op(%T{data: %Nx.Defn.Expr{op: :cond, args: [clauses, last]}} = t, state) do
873+
defp lower_op(%T{data: %Nx.Defn.Expr{op: :cond, args: [clauses, %T{} = last]}} = t, state) do
874874
{last_ref, state} = lower_node(last, state)
875875

876876
{result, state} =
@@ -884,6 +884,52 @@ defmodule Emily.IR do
884884
coerce(result, t.type, state)
885885
end
886886

887+
# Multi-output cond: each branch returns a tuple of tensors. Lower to one
888+
# `where`-chain per leaf position — identical wholesale-select semantics to
889+
# the single-output case above (the predicate is a whole-tensor scalar bool;
890+
# every branch is still computed). Returns a `{:multi_refs, [...]}` handle
891+
# that `:elem` projects; sibling `:elem`s share it via lower_node's memo.
892+
# A nested / non-tensor container raises (no fallback path for it yet).
893+
defp lower_op(%T{data: %Nx.Defn.Expr{op: :cond, args: [clauses, last]}}, state)
894+
when is_tuple(last) do
895+
last_leaves = Tuple.to_list(last)
896+
897+
unless tensors?(last_leaves) and
898+
Enum.all?(clauses, fn {_p, b} -> is_tuple(b) and tensors?(Tuple.to_list(b)) end) do
899+
raise ArgumentError,
900+
"Emily Expr compiler: cond over a nested / non-tensor container is not " <>
901+
"lowered yet (only a flat tuple of tensors)."
902+
end
903+
904+
# Lower each predicate (cast to pred) + its branch's leaf refs once, so the
905+
# per-leaf where-chains share them.
906+
{clauses, state} =
907+
Enum.map_reduce(clauses, state, fn {pred, body}, st ->
908+
{pred_ref, st} = lower_node(pred, st)
909+
{pred_ref, st} = emit(st, :astype, [pred_ref], [[dtype_code({:pred, 1})]])
910+
{body_refs, st} = Enum.map_reduce(Tuple.to_list(body), st, &lower_node/2)
911+
{{pred_ref, body_refs}, st}
912+
end)
913+
914+
rev = Enum.reverse(clauses)
915+
916+
{refs, state} =
917+
last_leaves
918+
|> Enum.with_index()
919+
|> Enum.map_reduce(state, fn {leaf, j}, st ->
920+
{last_ref, st} = lower_node(leaf, st)
921+
922+
{result, st} =
923+
Enum.reduce(rev, {last_ref, st}, fn {pred_ref, body_refs}, {else_ref, st2} ->
924+
emit(st2, :where, [pred_ref, Enum.at(body_refs, j), else_ref])
925+
end)
926+
927+
coerce(result, leaf.type, st)
928+
end)
929+
930+
{{:multi_refs, refs}, state}
931+
end
932+
887933
# attach_token: sequences a token (hooks) before `expr`. With no active
888934
# hook the token is a no-op, so pass through to the inner expr. Hooks
889935
# would need a callback into Elixir mid-graph (program-split) — deferred.
@@ -955,11 +1001,14 @@ defmodule Emily.IR do
9551001
{{:multi, base, _arity}, state} ->
9561002
{{:instr, base + i}, state}
9571003

1004+
{{:multi_refs, refs}, state} ->
1005+
{Enum.at(refs, i), state}
1006+
9581007
{_handle, _state} ->
9591008
raise ArgumentError,
9601009
"Emily Expr compiler: :elem projects a tuple-producing op it can't " <>
961-
"lower yet (only `while` produces projectable tuples today; other " <>
962-
"multi-output ops are unsupported)."
1010+
"lower yet (only `while` and tuple `cond` produce projectable " <>
1011+
"tuples today; other multi-output ops are unsupported)."
9631012
end
9641013
end
9651014

@@ -1157,6 +1206,8 @@ defmodule Emily.IR do
11571206
defp bool_int(true), do: 1
11581207
defp bool_int(false), do: 0
11591208

1209+
defp tensors?(list), do: Enum.all?(list, &match?(%T{}, &1))
1210+
11601211
defp float_like?({kind, _}) when kind in [:f, :bf, :c], do: true
11611212
defp float_like?(_), do: false
11621213

test/emily/compiler_control_flow_test.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,34 @@ defmodule Emily.CompilerControlFlowTest do
4545
acc
4646
end
4747

48+
# Tuple-returning cond (multi-output): both branches produce a {vec, vec}
49+
# tuple; `:elem` projects each leaf out of the lowered per-leaf where-chains.
50+
defn tuple_if_fn(x) do
51+
{a, b} =
52+
if Nx.greater(Nx.sum(x), 0) do
53+
{Nx.multiply(x, 2), Nx.add(x, 1)}
54+
else
55+
{Nx.negate(x), Nx.subtract(x, 1)}
56+
end
57+
58+
Nx.add(a, b)
59+
end
60+
61+
# Multi-clause tuple cond with leaves of DIFFERENT shapes (vector + scalar),
62+
# so each leaf's where-chain is built and coerced independently.
63+
defn tuple_cond3_fn(x) do
64+
s = Nx.sum(x)
65+
66+
{vec, scalar} =
67+
cond do
68+
Nx.greater(s, 10) -> {Nx.multiply(x, 10), Nx.reduce_max(x)}
69+
Nx.greater(s, 0) -> {Nx.multiply(x, 2), Nx.product(x)}
70+
true -> {Nx.negate(x), s}
71+
end
72+
73+
Nx.add(vec, scalar)
74+
end
75+
4876
defp equiv(fun, x) do
4977
native = Nx.Defn.jit(fun, @native).(x)
5078
eval = Nx.Defn.jit(fun, @eval).(x)
@@ -71,6 +99,18 @@ defmodule Emily.CompilerControlFlowTest do
7199
equiv(&nested_if_fn/1, Nx.tensor(data, backend: Emily.Backend))
72100
end
73101
end
102+
103+
test "tuple-returning if (multi-output cond) projects each leaf" do
104+
for data <- [[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]] do
105+
equiv(&tuple_if_fn/1, Nx.tensor(data, backend: Emily.Backend))
106+
end
107+
end
108+
109+
test "multi-clause tuple cond with mixed-shape leaves matches the evaluator" do
110+
for data <- [[5.0, 4.0, 3.0], [1.0, 1.0, 1.0], [-2.0, -2.0, -2.0]] do
111+
equiv(&tuple_cond3_fn/1, Nx.tensor(data, backend: Emily.Backend))
112+
end
113+
end
74114
end
75115

76116
describe "defn while" do

0 commit comments

Comments
 (0)