Skip to content

Commit eb58023

Browse files
committed
ci(mlx): lazy mx.transpose repro + keep long build alive
Probe gather_mm(sorted=True) on an unevaluated transpose op (mirrors ET TransposeNode). Disable cancel-in-progress / ignore repro-only paths so the fast job can run beside the Vid-vs-literal build.
1 parent fcbdfb9 commit eb58023

2 files changed

Lines changed: 69 additions & 21 deletions

File tree

.github/workflows/mlx-fork-check.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ on:
2525
push:
2626
branches:
2727
- ci-mlx-check
28+
paths-ignore:
29+
- mlx_gather_mm_repro.py
30+
- .github/workflows/mlx-only-repro.yml
2831

2932
concurrency:
3033
group: ${{ github.workflow }}-${{ github.ref }}
31-
cancel-in-progress: true
34+
# false: a repro-only push must not cancel an in-flight ExecuTorch build
35+
cancel-in-progress: false
3236

3337
permissions:
3438
contents: read

mlx_gather_mm_repro.py

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,37 @@
1717
E, 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):
7886
except Exception:
7987
pass
8088
print(
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
)
8694
ok = []
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é.
102110
ok.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.
106121
ok.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"))
108125
ok.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).
141185
print("\n--- controle gather_qmm (meme layout)")
142186
rng = np.random.default_rng(0)

0 commit comments

Comments
 (0)