Fix sorted gather_mm activation row stride - #3960
Conversation
|
Confirming root cause and isolation. We discovered this through the ExecuTorch MLX delegate: gather_mm(sorted_indices=True) was returning max_diff ≈ 33 on inputs shaped [T, 1, K] (produced by weight.transpose(-1,-2) → expand_dims in the MoE prefill path). Your fix is the exact root cause. Your analysis is sound: the stride of a singleton dimension is arbitrary by design, so deriving lda from it is unsafe. Using lda = K directly is the right fix. Impact: This unblocks MoE prefill on Apple Silicon. We tested locally on Qwen3.5 with MoE expert sorting: throughput improved from 73 to 80 tok/sec on decode. The ExecutorTorch PR (#20685) includes a workaround (materializing a) while this lands upstream. Once it merges, the workaround can be dropped and the tests will be clean. |
Fix sorted
gather_mmwith singleton-dimension inputsSummary
Fix the activation row stride used by the specialized sorted RHS
gather_mmimplementations.Both the Steel and NAX paths flatten the leading activation dimensions into
M, so consecutive rows areKelements apart. However, they currently deriveldafrom the original second-to-last dimension, which can have stride1when it is a singleton dimension introduced byexpand_dims.Reproduction
Before this change, MLX 0.32 produces:
The first row is correct, while subsequent rows are read using the wrong offset.
Root cause
The sorted RHS paths calculate:
For the
[2, 1, 64]view produced byexpand_dims, the singleton dimension can have stride1. The kernel consequently reads the second row froma.flatten()[1:65]instead ofa.flatten()[64:128].mx.contiguous(a)does not reliably avoid the issue because MLX can reuse the buffer and preserve its strides.Fix
The activation is row-contiguous before this calculation, and the leading dimensions are flattened into rows of length
K. Therefore, both the Steel and NAX paths should use:int lda = K;With this change, the sorted result agrees with the reference within floating-point tolerance.