@@ -124,7 +124,15 @@ defmodule Emily.IR do
124124 # RNG / dynamic indexing primitives
125125 bitcast: 72 ,
126126 erf_inv: 73 ,
127- dyn_slice: 74
127+ dyn_slice: 74 ,
128+ # inclusive cumulative reductions (iattrs [[axis],[reverse]])
129+ cumsum: 75 ,
130+ cumprod: 76 ,
131+ cummax: 77 ,
132+ cummin: 78 ,
133+ # multi-axis gather: operands [input, idx0, ...]; iattrs [[axes],[slice_sizes]]
134+ gather: 79 ,
135+ stack: 80
128136 }
129137
130138 # Quant mode string -> code; decoded by qmode_from_code in
@@ -409,6 +417,14 @@ defmodule Emily.IR do
409417 reduce_min: :min
410418 }
411419
420+ # Cumulative reductions arrive as `Nx.block/4` nodes. Block struct -> opcode.
421+ @ cumulative_blocks % {
422+ Nx.Block.CumulativeSum => :cumsum ,
423+ Nx.Block.CumulativeProduct => :cumprod ,
424+ Nx.Block.CumulativeMax => :cummax ,
425+ Nx.Block.CumulativeMin => :cummin
426+ }
427+
412428 defp lower_op ( % T { data: % Nx.Defn.Expr { op: op , args: [ a , opts ] } } = t , state )
413429 when is_map_key ( @ reductions , op ) do
414430 { ra , state } = lower_node ( a , state )
@@ -568,6 +584,41 @@ defmodule Emily.IR do
568584 end
569585 end
570586
587+ # gather(input, indices, opts). Mirrors Emily.Backend.gather/4: single-axis
588+ # gathers `take` along the one axis (indices cast to s32); multi-axis
589+ # gathers split the `{..., R}` index tensor into R per-axis index arrays
590+ # and use MLX's multi-index gather. Both reshape to the output shape (token
591+ # selection in sampling is this shape). A layout MLX gather can't express
592+ # raises, so the graceful fallback handles it.
593+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :gather , args: [ input , indices , opts ] } } = t , state ) do
594+ axes = opts [ :axes ]
595+ indices_shape = Tuple . to_list ( indices . shape )
596+
597+ { ri , state } = lower_node ( input , state )
598+ { rx , state } = lower_node ( indices , state )
599+
600+ { r , state } =
601+ case axes do
602+ [ axis ] ->
603+ { ix , state } = emit ( state , :astype , [ rx ] , [ [ dtype_code ( { :s , 32 } ) ] ] )
604+ emit ( state , :take , [ ri , ix ] , [ [ axis ] ] )
605+
606+ _ when is_list ( axes ) ->
607+ unless scatter_gather_compatible? ( indices_shape , axes ) do
608+ raise ArgumentError ,
609+ "Emily Expr compiler: gather index layout #{ inspect ( indices_shape ) } " <>
610+ "for axes #{ inspect ( axes ) } is not MLX-gather-compatible."
611+ end
612+
613+ { idx_refs , state } = split_indices_for_gather ( rx , indices_shape , length ( axes ) , state )
614+ slice_sizes = slice_sizes_for_gather ( input . shape , axes )
615+ emit ( state , :gather , [ ri | idx_refs ] , [ axes , slice_sizes ] )
616+ end
617+
618+ { r , state } = emit ( state , :reshape , [ r ] , [ Tuple . to_list ( t . shape ) ] )
619+ coerce ( r , t . type , state )
620+ end
621+
571622 # put_slice(src, start_indices, slice): write `slice` into `src` at
572623 # `start_indices`. Mirrors Emily.Backend.put_slice/4 (cast src + update
573624 # to out.type), but supports RUNTIME (tensor) start indices — the decode
@@ -623,6 +674,12 @@ defmodule Emily.IR do
623674 emit_coerced ( state , :concatenate , refs , [ [ axis ] ] , t . type )
624675 end
625676
677+ # stack(tensors, axis): join along a NEW axis. Mirrors Emily.Backend.stack/3.
678+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :stack , args: [ tensors , axis ] } } = t , state ) do
679+ { refs , state } = Enum . map_reduce ( tensors , state , & lower_node / 2 )
680+ emit_coerced ( state , :stack , refs , [ [ axis ] ] , t . type )
681+ end
682+
626683 # conv: ports Emily.Backend.conv/4 — permute input -> NHWC and kernel ->
627684 # OHWI (casting both to out.type), mx::conv_general, then permute the
628685 # result NHWC -> NCHW -> the user's output layout. batch_group_size > 1
@@ -858,11 +915,26 @@ defmodule Emily.IR do
858915 emit_coerced ( state , :take , [ ri , rx ] , [ [ axis ] ] , t . type )
859916 end
860917
918+ # Cumulative families. Like Emily.Backend.block/4, the last-axis case uses
919+ # the native MLX `cumsum`/`cumprod`/`cummax`/`cummin` kernel; interior axes
920+ # (which MLX can't always factor) fall back to the block's composed
921+ # expansion. Nx cumulation is always inclusive.
922+ defp lower_block ( % mod { axis: axis , reverse: reverse } , [ t ] , expr , out , state )
923+ when is_map_key ( @ cumulative_blocks , mod ) do
924+ if axis == tuple_size ( out . shape ) - 1 do
925+ { rt , state } = lower_node ( t , state )
926+ op = Map . fetch! ( @ cumulative_blocks , mod )
927+ emit_coerced ( state , op , [ rt ] , [ [ axis ] , [ bool_int ( reverse ) ] ] , out . type )
928+ else
929+ lower_node ( expr , state )
930+ end
931+ end
932+
861933 # Any other block struct raises. Lowering the block's composed
862934 # expansion would silently diverge from the Evaluator whenever
863935 # Emily.Backend.block/4 dispatches that struct through a fused / native
864- # kernel (e.g. SDPAWithSinks, the Nx.Block.LinAlg.* / Take / FFT /
865- # cumulative families) — a worse failure than a clear "unsupported".
936+ # kernel (e.g. SDPAWithSinks, the Nx.Block.LinAlg.* / FFT families) — a
937+ # worse failure than a clear "unsupported".
866938 # Additional fused blocks are added alongside their opcode.
867939 defp lower_block ( struct , _in_args , _expr , _t , _state ) do
868940 raise ArgumentError ,
@@ -1007,4 +1079,39 @@ defmodule Emily.IR do
10071079 { r , state } = emit ( state , :maximum , [ ref , lo_c ] )
10081080 emit ( state , :minimum , [ r , hi_c ] )
10091081 end
1082+
1083+ # MLX's multi-index gather needs the index tensor's leading dims to be the
1084+ # batch and the last axis to select across `axes` (mirrors
1085+ # Emily.Backend.scatter_gather_compatible?/2).
1086+ defp scatter_gather_compatible? ( indices_shape , axes ) do
1087+ is_list ( axes ) and axes != [ ] and length ( indices_shape ) >= 2 and
1088+ List . last ( indices_shape ) == length ( axes )
1089+ end
1090+
1091+ # Split an `{..., R}` index tensor into R per-axis s32 index arrays (each
1092+ # the leading batch with the last axis dropped) — ports
1093+ # Emily.Backend.split_indices_per_axis/4 with static slices.
1094+ defp split_indices_for_gather ( indices_ref , indices_shape , n_axes , state ) do
1095+ rank = length ( indices_shape )
1096+ last_axis = rank - 1
1097+ batch_shape = Enum . take ( indices_shape , last_axis )
1098+ strides = List . duplicate ( 1 , rank )
1099+ batch_zeros = List . duplicate ( 0 , last_axis )
1100+
1101+ Enum . map_reduce ( 0 .. ( n_axes - 1 ) // 1 , state , fn i , state ->
1102+ { r , state } =
1103+ emit ( state , :slice , [ indices_ref ] , [ batch_zeros ++ [ i ] , batch_shape ++ [ i + 1 ] , strides ] )
1104+
1105+ { r , state } = emit ( state , :squeeze , [ r ] , [ [ last_axis ] ] )
1106+ emit ( state , :astype , [ r ] , [ [ dtype_code ( { :s , 32 } ) ] ] )
1107+ end )
1108+ end
1109+
1110+ # Per-axis slice size for gather: 1 on a gathered axis, the full extent
1111+ # otherwise (mirrors Emily.Backend.slice_sizes_for_gather/2).
1112+ defp slice_sizes_for_gather ( input_shape , axes ) do
1113+ axes_set = MapSet . new ( axes )
1114+ rank = tuple_size ( input_shape )
1115+ for i <- 0 .. ( rank - 1 ) // 1 , do: if ( i in axes_set , do: 1 , else: elem ( input_shape , i ) )
1116+ end
10101117end
0 commit comments