@@ -145,7 +145,15 @@ defmodule Emily.IR do
145145 # window select-and-scatter (pooling backward). operands
146146 # [input, source, init]; iattrs [[window],[strides],[pad_lo],[pad_hi]].
147147 window_scatter_max: 86 ,
148- window_scatter_min: 87
148+ window_scatter_min: 87 ,
149+ # FFT family — n-D transforms. operands [input]; iattrs
150+ # [[sizes...],[axes...]]. `fft`/`ifft` (1-D, last axis) and the
151+ # `fft2`/`ifft2`/`rfft`/`irfft` blocks all route here. Unnormalized
152+ # (`FFTNorm::Backward`) is baked C++-side, matching Nx / the eager NIFs.
153+ fftn: 88 ,
154+ ifftn: 89 ,
155+ rfftn: 90 ,
156+ irfftn: 91
149157 }
150158
151159 # Quant mode string -> code; decoded by qmode_from_code in
@@ -498,6 +506,19 @@ defmodule Emily.IR do
498506 coerce ( r , t . type , state )
499507 end
500508
509+ # 1-D FFT / inverse FFT (Nx.fft / Nx.ifft). Mirrors Emily.Backend.{fft,
510+ # ifft}/3: route through the n-D MLX kernel restricted to one axis. The
511+ # eager path uses the trailing axis and ignores `opts[:axis]`, so we do
512+ # too — keeping native bit-identical to the evaluator. Output is complex
513+ # (`Nx.Type.to_complex/1`); the trailing coerce matches the backend `wrap`.
514+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: op , args: [ a , opts ] } } = t , state )
515+ when op in [ :fft , :ifft ] do
516+ { ra , state } = lower_node ( a , state )
517+ axis = tuple_size ( a . shape ) - 1
518+ opcode = if op == :fft , do: :fftn , else: :ifftn
519+ emit_coerced ( state , opcode , [ ra ] , [ [ opts [ :length ] ] , [ axis ] ] , t . type )
520+ end
521+
501522 # Non-batched dot -> tensordot over the contraction axes.
502523 defp lower_op ( % T { data: % Nx.Defn.Expr { op: :dot , args: [ a , ca , [ ] , b , cb , [ ] ] } } = t , state ) do
503524 { ra , state } = lower_node ( a , state )
@@ -1012,10 +1033,35 @@ defmodule Emily.IR do
10121033 end
10131034 end
10141035
1036+ # FFT family blocks (Nx.fft2 / ifft2 / rfft / irfft). Each mirrors the
1037+ # matching Emily.Backend.native_* wrapper: route through the n-D MLX
1038+ # fft/ifft/rfft/irfft kernel with the block's sizes + axes, then coerce to
1039+ # out.type (complex for the forward transforms, real for irfft). The
1040+ # block's `eps` is unused (MLX needs none), as in the eager path.
1041+ defp lower_block ( % Nx.Block.FFT2 { lengths: lengths , axes: axes } , [ t ] , _expr , out , state ) do
1042+ { rt , state } = lower_node ( t , state )
1043+ emit_coerced ( state , :fftn , [ rt ] , [ lengths , axes ] , out . type )
1044+ end
1045+
1046+ defp lower_block ( % Nx.Block.IFFT2 { lengths: lengths , axes: axes } , [ t ] , _expr , out , state ) do
1047+ { rt , state } = lower_node ( t , state )
1048+ emit_coerced ( state , :ifftn , [ rt ] , [ lengths , axes ] , out . type )
1049+ end
1050+
1051+ defp lower_block ( % Nx.Block.RFFT { length: length , axis: axis } , [ t ] , _expr , out , state ) do
1052+ { rt , state } = lower_node ( t , state )
1053+ emit_coerced ( state , :rfftn , [ rt ] , [ [ length ] , [ axis ] ] , out . type )
1054+ end
1055+
1056+ defp lower_block ( % Nx.Block.IRFFT { length: length , axis: axis } , [ t ] , _expr , out , state ) do
1057+ { rt , state } = lower_node ( t , state )
1058+ emit_coerced ( state , :irfftn , [ rt ] , [ [ length ] , [ axis ] ] , out . type )
1059+ end
1060+
10151061 # Any other block struct raises. Lowering the block's composed
10161062 # expansion would silently diverge from the Evaluator whenever
10171063 # Emily.Backend.block/4 dispatches that struct through a fused / native
1018- # kernel (e.g. SDPAWithSinks, the Nx.Block.LinAlg.* / FFT families) — a
1064+ # kernel (e.g. SDPAWithSinks, the Nx.Block.LinAlg.* families) — a
10191065 # worse failure than a clear "unsupported".
10201066 # Additional fused blocks are added alongside their opcode.
10211067 defp lower_block ( struct , _in_args , _expr , _t , _state ) do
0 commit comments