Skip to content

Commit e04f579

Browse files
fix(mlx): --manual-sdpa candidate fix (manual batched matmul SDPA, works around mx.fast.scaled_dot_product_attention batch>1+GQA bug)
Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent 6b61c12 commit e04f579

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

inference_engine/bridge/manifest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,25 @@ def _harness_preset(
147147
timeout_minutes=60,
148148
validate_reports=False,
149149
),
150+
Preset(
151+
name="mlx-batched-manual-sdpa",
152+
description="Candidate fix: MLX batched multi-tenant with a manual "
153+
"matmul-softmax SDPA replacing mx.fast.scaled_dot_"
154+
"product_attention (works around the batch>1 + GQA "
155+
"fast-kernel bug). Expect per-session recall -> 1.0.",
156+
command_templates=(
157+
(
158+
"python3", "scripts/research/mlx_batched_multitenant_bench.py",
159+
"--verifier-path", "${ENV:KAKEYA_MAC_VERIFIER_PATH}",
160+
"--sessions", "8", "--haystack-lines", "60",
161+
"--max-new-tokens", "24", "--manual-sdpa",
162+
"--output",
163+
"results/research/k3_mac_bridge_mlx_batched_manual_sdpa.json",
164+
),
165+
),
166+
timeout_minutes=90,
167+
validate_reports=False,
168+
),
150169
Preset(
151170
name="mlx-batched-layer-diff-concat",
152171
description="Layer-diff with the concat SinkWindowKVCache (no "

scripts/research/mlx_batched_multitenant_bench.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def main() -> int:
4242
ap.add_argument("--full-window", type=int, default=100000,
4343
help="full-attn-layer window when --kakeya-cache "
4444
"(large = keep all, exact recall).")
45+
ap.add_argument("--manual-sdpa", action="store_true",
46+
help="Replace mlx_lm gemma's mx.fast.scaled_dot_product_"
47+
"attention with a manual batched matmul-softmax SDPA "
48+
"(works around the suspected batch>1 + GQA fast-kernel "
49+
"bug). The candidate fix.")
4550
ap.add_argument("--output", default=None)
4651
args = ap.parse_args()
4752

@@ -54,6 +59,36 @@ def main() -> int:
5459
resolve_mlx_text_model, mlx_full_attention_layer_indices,
5560
)
5661

62+
if args.manual_sdpa:
63+
import mlx_lm.models.gemma4_text as g4
64+
65+
def _manual_sdpa(queries, keys, values, cache=None, scale=1.0, mask=None,
66+
sinks=None):
67+
# queries [B, n_heads, L, D]; keys/values [B, n_kv, S, D] (GQA).
68+
n_heads = queries.shape[1]
69+
n_kv = keys.shape[1]
70+
if n_kv != n_heads:
71+
rep = n_heads // n_kv
72+
keys = mx.repeat(keys, rep, axis=1)
73+
values = mx.repeat(values, rep, axis=1)
74+
scores = (queries * scale) @ mx.swapaxes(keys, -1, -2) # [B,h,L,S]
75+
if mask is not None:
76+
if isinstance(mask, str): # "causal"
77+
qL, kL = scores.shape[-2], scores.shape[-1]
78+
qi = mx.arange(kL - qL, kL)[:, None]
79+
ki = mx.arange(kL)[None]
80+
bmask = qi >= ki
81+
scores = mx.where(bmask, scores, mx.finfo(scores.dtype).min)
82+
elif mask.dtype == mx.bool_:
83+
scores = mx.where(mask, scores, mx.finfo(scores.dtype).min)
84+
else:
85+
scores = scores + mask
86+
scores = mx.softmax(scores, axis=-1, precise=True)
87+
return scores @ values
88+
89+
g4.scaled_dot_product_attention = _manual_sdpa
90+
print("[mlx-mt] patched gemma SDPA -> manual batched matmul", flush=True)
91+
5792
print(f"[mlx-mt] loading {args.verifier_path}", flush=True)
5893
model, tok = mlx_lm.load(args.verifier_path)
5994
N = args.sessions

tests/inference_engine/bridge/test_manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def test_allowlist_contains_exactly_the_documented_presets():
7676
"mlx-batched-kakeya-cache",
7777
"mlx-batched-layer-diff",
7878
"mlx-batched-layer-diff-concat",
79+
"mlx-batched-manual-sdpa",
7980
"mlx-batched-multitenant",
8081
"mlx-env-probe",
8182
"mlx-multitenant-pressure",

0 commit comments

Comments
 (0)