@@ -14,10 +14,11 @@ defmodule Emily.Backend do
1414 message pointing to f32.
1515 * `from_pointer`, `to_pointer`, `population_count`, and
1616 `count_leading_zeros` raise `ArgumentError` — MLX has no primitive.
17- * Window operations (`window_sum`, `window_scatter_max`, etc.) and
18- advanced linalg (`lu`, `svd`, `qr`, `cholesky`, `eigh`, `solve`,
19- `determinant`, `triangular_solve`) fall back to Nx's default
20- `optional/3` implementation — correct but slow.
17+ * Window operations (`window_sum`, `window_scatter_max`, etc.) fall
18+ back to Nx's default `optional/3` implementation — correct but slow.
19+ `qr` with `mode: :complete` also falls back (MLX only supports
20+ reduced QR). `determinant` uses Nx's default implementation, which
21+ calls `lu` (native via MLX) for matrices larger than 3×3.
2122 * `quotient` uses MLX `floor_divide` semantics (floor toward -inf
2223 rather than Nx's truncate-toward-zero). For non-negative integer
2324 operands the results agree; mixed-sign inputs diverge by one. We
@@ -1194,15 +1195,114 @@ defmodule Emily.Backend do
11941195 batch ++ trailing
11951196 end
11961197
1198+ # =================================================================
1199+ # Native linalg — decompositions & solvers via mx::linalg::*
1200+ # =================================================================
1201+
11971202 @ impl true
1198- def lu ( outs , t , opts ) , do: via_binary_tuple ( outs , [ t ] , & Nx.LinAlg . lu ( & 1 , opts ) )
1203+ def lu ( { p_out , l_out , u_out } , t , _opts ) do
1204+ w = worker ( )
1205+ { perm_ref , l_ref , u_ref } = Native . linalg_lu ( w , ref ( t ) )
1206+ n = elem ( t . shape , tuple_size ( t . shape ) - 1 )
1207+ eye_ref = Native . eye ( w , n , n , 0 , p_out . type )
1208+ p_ref = Native . take ( w , eye_ref , perm_ref , 0 )
1209+ { wrap ( p_ref , p_out , w ) , wrap ( l_ref , l_out , w ) , wrap ( u_ref , u_out , w ) }
1210+ end
11991211
12001212 @ impl true
1201- def triangular_solve ( out , a , b , opts ) ,
1202- do: via_binary ( out , [ a , b ] , & Nx.LinAlg . triangular_solve ( & 1 , & 2 , opts ) )
1213+ def svd ( { u_out , s_out , v_out } , t , _opts ) do
1214+ w = worker ( )
1215+ { u_ref , s_ref , v_ref } = Native . linalg_svd ( w , ref ( t ) )
1216+ rank = tuple_size ( t . shape )
1217+ m = elem ( t . shape , rank - 2 )
1218+ n = elem ( t . shape , rank - 1 )
1219+ u_ref = maybe_slice_svd ( u_ref , u_out . shape , { m , m } , w )
1220+ v_ref = maybe_slice_svd ( v_ref , v_out . shape , { n , n } , w )
1221+ { wrap ( u_ref , u_out , w ) , wrap ( s_ref , s_out , w ) , wrap ( v_ref , v_out , w ) }
1222+ end
1223+
1224+ defp maybe_slice_svd ( ref , out_shape , full_last2 , w ) do
1225+ rank = tuple_size ( out_shape )
1226+
1227+ if { elem ( out_shape , rank - 2 ) , elem ( out_shape , rank - 1 ) } == full_last2 do
1228+ ref
1229+ else
1230+ starts = List . duplicate ( 0 , rank )
1231+ strides = List . duplicate ( 1 , rank )
1232+ Native . slice ( w , ref , starts , Tuple . to_list ( out_shape ) , strides )
1233+ end
1234+ end
12031235
12041236 @ impl true
1205- def svd ( outs , t , opts ) , do: via_binary_tuple ( outs , [ t ] , & Nx.LinAlg . svd ( & 1 , opts ) )
1237+ def triangular_solve ( % T { } = out , a , b , opts ) do
1238+ w = worker ( )
1239+ a_ref = ref ( a )
1240+ b_ref = ref ( b )
1241+
1242+ case { opts [ :transform_a ] , opts [ :left_side ] } do
1243+ { :none , true } ->
1244+ Native . linalg_solve_triangular ( w , a_ref , b_ref , not opts [ :lower ] )
1245+ |> wrap ( out , w )
1246+
1247+ { :transpose , true } ->
1248+ at = Native . transpose ( w , a_ref , mat_transpose_axes ( a . shape ) )
1249+
1250+ Native . linalg_solve_triangular ( w , at , b_ref , opts [ :lower ] )
1251+ |> wrap ( out , w )
1252+
1253+ { :none , false } ->
1254+ at = Native . transpose ( w , a_ref , mat_transpose_axes ( a . shape ) )
1255+ bt = Native . transpose ( w , b_ref , mat_transpose_axes ( b . shape ) )
1256+ xt = Native . linalg_solve_triangular ( w , at , bt , opts [ :lower ] )
1257+
1258+ Native . transpose ( w , xt , mat_transpose_axes ( out . shape ) )
1259+ |> wrap ( out , w )
1260+
1261+ { :transpose , false } ->
1262+ bt = Native . transpose ( w , b_ref , mat_transpose_axes ( b . shape ) )
1263+ xt = Native . linalg_solve_triangular ( w , a_ref , bt , not opts [ :lower ] )
1264+
1265+ Native . transpose ( w , xt , mat_transpose_axes ( out . shape ) )
1266+ |> wrap ( out , w )
1267+ end
1268+ end
1269+
1270+ defp mat_transpose_axes ( shape ) do
1271+ rank = tuple_size ( shape )
1272+ Enum . to_list ( 0 .. ( rank - 3 ) // 1 ) ++ [ rank - 1 , rank - 2 ]
1273+ end
1274+
1275+ @ impl true
1276+ def qr ( { q_out , r_out } , t , opts ) do
1277+ case opts [ :mode ] do
1278+ :reduced ->
1279+ w = worker ( )
1280+ { q_ref , r_ref } = Native . linalg_qr ( w , ref ( t ) )
1281+ { wrap ( q_ref , q_out , w ) , wrap ( r_ref , r_out , w ) }
1282+
1283+ :complete ->
1284+ via_binary_tuple ( { q_out , r_out } , [ t ] , & Nx.LinAlg . qr ( & 1 , opts ) )
1285+ end
1286+ end
1287+
1288+ @ impl true
1289+ def cholesky ( % T { } = out , t ) do
1290+ w = worker ( )
1291+ Native . linalg_cholesky ( w , ref ( t ) , false ) |> wrap ( out , w )
1292+ end
1293+
1294+ @ impl true
1295+ def eigh ( { vals_out , vecs_out } , t , _opts ) do
1296+ w = worker ( )
1297+ { vals_ref , vecs_ref } = Native . linalg_eigh ( w , ref ( t ) , "L" )
1298+ { wrap ( vals_ref , vals_out , w ) , wrap ( vecs_ref , vecs_out , w ) }
1299+ end
1300+
1301+ @ impl true
1302+ def solve ( % T { } = out , a , b ) do
1303+ w = worker ( )
1304+ Native . linalg_solve ( w , ref ( a ) , ref ( b ) ) |> wrap ( out , w )
1305+ end
12061306
12071307 # =================================================================
12081308 # Custom fused-kernel callbacks for Emily.Fast
0 commit comments