1717E , K , NOUT = 4 , 64 , 128
1818
1919
20- def run_case (L , dtype , idx_np , a_ndim , label , b_strided = False ):
20+ def run_case (L , dtype , idx_np , a_ndim , label , b_mode = "contig" ):
2121 """Compare référence / sorted=False / sorted=True et localise les lignes fausses.
2222
23- b_strided=True reproduit EXACTEMENT ce que fait GatherMmModel dans
24- test_ops.py : le buffer est stocké en [E, out, in] et transposé à la volée
25- en [E, in, out] -> vue NON CONTIGUE (strides échangés sur les 2 derniers
26- axes). Le chemin de production (SwitchLinear.pack) fait au contraire
27- .transpose(-1,-2).contiguous(), donc b y est toujours contigu.
23+ b_mode:
24+ contig — b = mx.array([E,K,N]) contigu (baseline)
25+ swapaxes — vue non contiguë via mx.array(...).swapaxes (déjà OK)
26+ lazy_transpose — mx.transpose(mx.array(b_src), (0,2,1)) SANS eval avant
27+ gather_mm : imite le TransposeNode du graphe ET (array
28+ paresseux issu d'une op, pas une vue construite à la main)
2829 """
2930 rng = np .random .default_rng (0 )
3031 a_np = rng .standard_normal ((L , 1 , K )).astype (np .float32 )
3132 b_np = rng .standard_normal ((E , K , NOUT )).astype (np .float32 )
3233
3334 a = mx .array (a_np if a_ndim == 3 else a_np .reshape (L , K )).astype (dtype )
34- if b_strided :
35- # mêmes valeurs logiques, stockage transposé
35+ if b_mode == "contig" :
36+ b = mx .array (b_np ).astype (dtype )
37+ elif b_mode == "swapaxes" :
38+ # mêmes valeurs logiques, stockage transposé → vue strides échangés
39+ b_src = np .ascontiguousarray (b_np .transpose (0 , 2 , 1 )) # [E, NOUT, K]
40+ b = mx .array (b_src ).astype (dtype ).swapaxes (- 1 , - 2 ) # [E, K, NOUT]
41+ elif b_mode == "lazy_transpose" :
42+ # Comme le TransposeNode ET : op MLX, pas de mx.eval avant gather_mm.
3643 b_src = np .ascontiguousarray (b_np .transpose (0 , 2 , 1 )) # [E, NOUT, K]
37- b = mx .array (b_src ).astype (dtype ). swapaxes ( - 1 , - 2 ) # [E, K, NOUT] non contigu
44+ b = mx .transpose ( mx . array (b_src ).astype (dtype ), ( 0 , 2 , 1 )) # [E, K, NOUT]
3845 else :
39- b = mx . array ( b_np ). astype ( dtype )
46+ raise ValueError ( f"unknown b_mode= { b_mode !r } " )
4047 idx = mx .array (idx_np .astype (np .uint32 ))
4148
42- # Référence explicite : take puis matmul (ce que fait l'eager PyTorch).
49+ # Référence + gather : un seul mx.eval à la fin, pour ne pas matérialiser
50+ # le lazy_transpose avant le kernel.
4351 ref = mx .matmul (a , mx .take (b , idx , axis = 0 ))
4452 got_unsorted = mx .gather_mm (a , b , None , idx , sorted_indices = False )
4553 got_sorted = mx .gather_mm (a , b , None , idx , sorted_indices = True )
@@ -48,7 +56,7 @@ def run_case(L, dtype, idx_np, a_ndim, label, b_strided=False):
4856 d_uns = float (mx .max (mx .abs (got_unsorted .astype (mx .float32 ) - ref .astype (mx .float32 ))))
4957 d_srt = float (mx .max (mx .abs (got_sorted .astype (mx .float32 ) - ref .astype (mx .float32 ))))
5058
51- print (f"\n --- { label } | L={ L } a.ndim={ a_ndim } dtype={ dtype } idx={ idx_np .tolist ()} " )
59+ print (f"\n --- { label } | L={ L } a.ndim={ a_ndim } dtype={ dtype } b_mode= { b_mode } idx={ idx_np .tolist ()} " )
5260 print (f" sorted=False max_diff = { d_uns :.6e} { 'OK' if d_uns < 1e-2 else 'DIVERGE' } " )
5361 print (f" sorted=True max_diff = { d_srt :.6e} { 'OK' if d_srt < 1e-2 else 'DIVERGE' } " )
5462
@@ -78,10 +86,10 @@ def run_case(L, dtype, idx_np, a_ndim, label, b_strided=False):
7886except Exception :
7987 pass
8088print (
81- "ATTENTION : le backend ExecuTorch NE lie PAS ce paquet PyPI. \n "
82- "Il compile le sous-module epingle backends/mlx/third-party/mlx\n "
83- "(commit 7a1d4f5c12ac82f4b4d0a6e71538d89ca0605247 sur main). \n "
84- "Un OK ici ne dit rien de la version reellement liee dans le build .\n "
89+ "ATTENTION : le backend ExecuTorch lie le sous-module \n "
90+ "backends/mlx/third-party/mlx @ 7a1d4f5c… (= tag v0.32.0, meme SHA \n "
91+ "que pip install mlx==0.32.0). Un OK ici EST comparable au runtime ET \n "
92+ "pour la version MLX ; l'ecart restant est la construction des arrays .\n "
8593)
8694ok = []
8795
@@ -101,12 +109,24 @@ def run_case(L, dtype, idx_np, a_ndim, label, b_strided=False):
101109# layout [L, 1, K] (celui du chemin MoE) qui n'est pas géré.
102110ok .append (run_case (8 , mx .float32 , np .array ([0 , 0 , 0 , 1 , 1 , 2 , 3 , 3 ]), 2 , "4 segments, a 2-D" ))
103111
104- # 5. LE CAS DU MODELE DE TEST : b non contigu (weight.transpose sans .contiguous()).
105- # C'est le seul écart entre GatherMmModel et le chemin de production.
112+ # 5. Vue non contiguë via swapaxes (deja innocenté).
113+ ok .append (run_case (2 , mx .float32 , np .array ([1 , 3 ]), 3 ,
114+ "cas du test, b swapaxes" , b_mode = "swapaxes" ))
115+ ok .append (run_case (8 , mx .float32 , np .array ([0 , 0 , 0 , 1 , 1 , 2 , 3 , 3 ]), 3 ,
116+ "4 segments, b swapaxes" , b_mode = "swapaxes" ))
117+
118+ # 5b. LE CAS QUI MANQUAIT : transpose LAZY (op MLX, pas de eval avant gather_mm).
119+ # C'est ce que produit TransposeNode dans le graphe ET — distinct d'une
120+ # vue swapaxes construite a la main. Si ca diverge : bug MLX hors PR.
106121ok .append (run_case (2 , mx .float32 , np .array ([1 , 3 ]), 3 ,
107- "cas du test, b NON CONTIGU" , b_strided = True ))
122+ "cas du test, b LAZY transpose" , b_mode = "lazy_transpose" ))
123+ ok .append (run_case (2 , mx .bfloat16 , np .array ([1 , 3 ]), 3 ,
124+ "cas du test bf16, b LAZY transpose" , b_mode = "lazy_transpose" ))
108125ok .append (run_case (8 , mx .float32 , np .array ([0 , 0 , 0 , 1 , 1 , 2 , 3 , 3 ]), 3 ,
109- "4 segments, b NON CONTIGU" , b_strided = True ))
126+ "4 segments, b LAZY transpose" , b_mode = "lazy_transpose" ))
127+ # Contre-epreuve : lazy transpose + sorted=False doit rester OK.
128+ ok .append (run_case (2 , mx .float32 , np .array ([1 , 3 ]), 3 ,
129+ "lazy transpose (unsorted indices contract)" , b_mode = "lazy_transpose" ))
110130
111131# 7. BALAYAGE DU DTYPE DES INDICES.
112132# _gather_mm_handler passe rhs_indices tel quel, SANS cast (verifie dans
@@ -137,6 +157,30 @@ def run_case(L, dtype, idx_np, a_ndim, label, b_strided=False):
137157 except Exception as e : # dtype refuse par MLX
138158 print (f" idx { np .dtype (_idt ).name :8s} | rejete : { type (e ).__name__ } : { e } " )
139159
160+ # 7b. Meme balayage dtype sur b LAZY transpose (le combo du graphe ET).
161+ print ("\n --- balayage dtype + b LAZY transpose (sorted=True, idx=[1,3])" )
162+ _rng2 = np .random .default_rng (0 )
163+ _a2 = mx .array (_rng2 .standard_normal ((2 , 1 , K )).astype (np .float32 ))
164+ _b2_np = _rng2 .standard_normal ((E , K , NOUT )).astype (np .float32 )
165+ _b2_src = np .ascontiguousarray (_b2_np .transpose (0 , 2 , 1 ))
166+ _b2 = mx .transpose (mx .array (_b2_src ), (0 , 2 , 1 )) # lazy, no eval
167+ _idx_ref2 = mx .array (np .array ([1 , 3 ], dtype = np .uint32 ))
168+ _ref2 = mx .matmul (_a2 , mx .take (_b2 , _idx_ref2 , axis = 0 ))
169+ for _idt in (np .uint32 , np .int32 , np .int64 ):
170+ try :
171+ _idx = mx .array (np .array ([1 , 3 ], dtype = _idt ))
172+ _got = mx .gather_mm (_a2 , _b2 , None , _idx , sorted_indices = True )
173+ mx .eval (_ref2 , _got ) # first eval materializes both
174+ _d = float (mx .max (mx .abs (_got - _ref2 )))
175+ flag = "OK" if _d < 1e-2 else "DIVERGE <<<<"
176+ print (f" idx { np .dtype (_idt ).name :8s} + lazy_transpose | sorted=True { _d :.3e} { flag } " )
177+ ok .append (_d < 1e-2 )
178+ # rebuild lazy b for next iter (previous eval materialized it)
179+ _b2 = mx .transpose (mx .array (_b2_src ), (0 , 2 , 1 ))
180+ _ref2 = mx .matmul (_a2 , mx .take (_b2 , _idx_ref2 , axis = 0 ))
181+ except Exception as e :
182+ print (f" idx { np .dtype (_idt ).name :8s} + lazy_transpose | rejete : { type (e ).__name__ } : { e } " )
183+
140184# 6. Contre-épreuve quantifiée : gather_qmm sur le même layout (il passe en CI).
141185print ("\n --- controle gather_qmm (meme layout)" )
142186rng = np .random .default_rng (0 )
0 commit comments