Download decider/mps_moe.py from hayriyigit/decider-4b-tr-rag: direct link, hf CLI and curl.
- Browser
- Download file 5.23 kB
-
https://huggingface.co/hayriyigit/decider-4b-tr-rag/resolve/main/decider/mps_moe.py
- Command line
-
hf download hf://hayriyigit/decider-4b-tr-rag/decider/mps_moe.py
-
curl -L -o mps_moe.py https://huggingface.co/hayriyigit/decider-4b-tr-rag/resolve/main/decider/mps_moe.py
5.23 kB
| """Two MPS slow paths in transformers' Qwen3.5-MoE reference code, replaced in-process (decider-35b-a3b on Apple Silicon). | |
| Contributed by @nassersala in issue #6 under the project's Apache-2.0 licence; adapted for the package (out-of-range expert | |
| ids are dropped as `histc` drops them, and the replacements are installed from `decider.mps_ops.patch_mps`). | |
| On an M-series Mac (torch 2.14, transformers 5.17) `torch.histc` takes about 45 ms a call on MPS (once per MoE layer, to | |
| count tokens per expert) and `torch.linalg.solve_triangular` about 17 ms (twice per linear-attention layer): most of a | |
| 2.8-4 s decision. With both replaced the 35B takes 0.23-0.5 s a decision for typical inputs (reported in issue #6). | |
| The `histc` replacement is an exact count. The block inverse is within 1e-6 of the MPS solver on real systems; on six items | |
| the probabilities moved at most 0.033, less than switching to the exact CPU solver does (0.042), and no argmax changed. | |
| The replacements are visible only inside those two transformers modules (install() gives each its own view of `torch`), and | |
| there they act only on MPS tensors of the matching call shape; everything else goes to torch. | |
| """ | |
| import torch | |
| _histc, _solve = torch.histc, torch.linalg.solve_triangular | |
| def count_ids(x, bins, min=0): | |
| """histc for integer-valued ids binned one per bin: a count of ids min..min+bins-1; other values are dropped as histc | |
| drops them (transformers passes expert-parallel sentinels >= num_experts and relies on that).""" | |
| idx = x.long() - int(min) | |
| keep = (idx >= 0) & (idx < bins) | |
| out = torch.zeros(bins, device=x.device, dtype=x.dtype) | |
| return out.scatter_add_(0, idx.clamp(0, bins - 1), keep.to(x.dtype)) | |
| def histc(input, bins=100, min=0, max=0, *, out=None): | |
| # expert ids 0..n-1 into n bins over [0, n-1]: bin k holds id k exactly, so this is a count (histc: ~45 ms on MPS) | |
| if input.device.type == "mps" and input.dim() == 1 and out is None and min == 0 and max > 0 and bins == max - min + 1: | |
| return count_ids(input, bins, min) | |
| return _histc(input, bins=bins, min=min, max=max) if out is None else _histc(input, bins=bins, min=min, max=max, out=out) | |
| def unit_lower_inverse(A): | |
| """Inverse of unit lower-triangular A (last dim a power of two) by block doubling: | |
| [[A11, 0], [A21, A22]]^-1 = [[X11, 0], [-X22 A21 X11, X22]].""" | |
| n = A.shape[-1] | |
| inv = torch.ones(*A.shape[:-2], n, 1, 1, device=A.device, dtype=A.dtype) # n diagonal 1x1 blocks | |
| b = 1 | |
| while b < n: | |
| nb = n // (2 * b) | |
| blocks = A.reshape(*A.shape[:-2], nb, 2 * b, nb, 2 * b).diagonal(dim1=-4, dim2=-2).movedim(-1, -3) | |
| x11, x22, a21 = inv[..., 0::2, :, :], inv[..., 1::2, :, :], blocks[..., b:, :b] | |
| new = torch.zeros(*A.shape[:-2], nb, 2 * b, 2 * b, device=A.device, dtype=A.dtype) | |
| new[..., :b, :b], new[..., b:, b:], new[..., b:, :b] = x11, x22, -(x22 @ a21 @ x11) | |
| inv, b = new, 2 * b | |
| return inv[..., 0, :, :] | |
| def solve_unit_lower(A, B): | |
| """solve_triangular(A, B, upper=False, unitriangular=True): only the strict lower triangle of A is read.""" | |
| n = A.shape[-1] | |
| return unit_lower_inverse(A.tril(-1) + torch.eye(n, device=A.device, dtype=A.dtype)) @ B | |
| def solve_triangular(A, B, *, upper, left=True, unitriangular=False, out=None): | |
| # unit lower-triangular: invert by block doubling, all batched matmuls (the MPS solver: ~17 ms per call) | |
| n = A.shape[-1] | |
| if A.device.type == "mps" and not upper and left and unitriangular and out is None and n > 0 and n & (n - 1) == 0: | |
| return solve_unit_lower(A, B) | |
| return _solve(A, B, upper=upper, left=left, unitriangular=unitriangular, out=out) | |
| class _Proxy: | |
| """A module's view of `torch` (or `torch.linalg`) with some attributes replaced; everything else is the real one.""" | |
| def __init__(self, real, **over): | |
| self._real, self._over = real, over | |
| def __getattr__(self, name): | |
| return self._over[name] if name in self._over else getattr(self._real, name) | |
| _installed = {} | |
| def install(): | |
| """Replace the two operations inside the two transformers modules that call them, and nowhere else: the grouped-MoE | |
| expert count in `transformers.integrations.moe` and the gated delta rule in `modeling_qwen3_5_moe` see a `torch` whose | |
| `histc` / `linalg.solve_triangular` are the replacements; every other caller keeps torch's. -> names patched.""" | |
| import importlib | |
| targets = {"transformers.integrations.moe": dict(histc=histc), | |
| "transformers.models.qwen3_5_moe.modeling_qwen3_5_moe": dict(linalg=_Proxy(torch.linalg, solve_triangular=solve_triangular))} | |
| for name, over in targets.items(): | |
| if name in _installed: | |
| continue | |
| try: | |
| mod = importlib.import_module(name) | |
| except ImportError: | |
| continue | |
| if getattr(mod, "torch", None) is not torch: | |
| continue # not the layout this was written for: leave it alone | |
| _installed[name] = mod | |
| mod.torch = _Proxy(torch, **over) | |
| return sorted(_installed) | |
| def uninstall(): | |
| for name, mod in list(_installed.items()): | |
| mod.torch = torch | |
| del _installed[name] | |