Download src/mlx_ternary/kernel.py from Jakevin/mlx-ternary: direct link, hf CLI and curl.
- Browser
- Download file 32.5 kB
-
https://huggingface.co/Jakevin/mlx-ternary/resolve/main/src/mlx_ternary/kernel.py
- Command line
-
hf download hf://Jakevin/mlx-ternary/src/mlx_ternary/kernel.py
-
curl -L -o kernel.py https://huggingface.co/Jakevin/mlx-ternary/resolve/main/src/mlx_ternary/kernel.py
32.5 kB
| # Copyright 2026 Jakevin | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """Ternary GEMV and GEMM for MLX. | |
| Codes {-1,0,1} are stored as 2-bit values {0,1,2}, four per byte, low bits | |
| first. One scale per 64 weights (or 128, as an option) and no zero point: value = (code - 1) * scale. | |
| The inner loop is the MLX 2-bit mask dot: pre-scale x by 1, 1/4, 1/16, 1/64 | |
| and AND the byte. | |
| Small batches use a simdgroup GEMV. K multiple of 512 follows the MLX | |
| qmv_fast walk (two rows per simdgroup, 16 weights per lane). Other K, | |
| including 640, uses a 4-wide dot. Batch >= 32 with the row count and K | |
| divisible by 32 uses a 32x32x32 simdgroup-matrix GEMM; if the batch is not a | |
| multiple of 32, the leftover rows go through the GEMV. The weight | |
| tile is half, the accumulator is float, and a tile never crosses a scale group. | |
| Batches up to 8 run through a small LRU of compiled graphs keyed by shape. | |
| Weights are passed as arguments, so the cache holds no weight arrays. | |
| Larger batches dispatch directly. | |
| """ | |
| from __future__ import annotations | |
| import mlx.core as mx | |
| from .pack import GROUP, GROUPS | |
| def set_cache_limit(gb: float = 2) -> int: | |
| """Opt-in: cap the MLX cache at ``gb`` gigabytes. Returns the previous limit. | |
| Importing the package does not change the MLX cache limit. | |
| """ | |
| return mx.set_cache_limit(int(gb * 1024**3)) | |
| def configure() -> int: | |
| """Cap the MLX cache at 2GB. Same as ``set_cache_limit(2)``.""" | |
| return set_cache_limit(2) | |
| _KERNELS: dict = {} | |
| _META: dict = {} | |
| QMV_ROWS = 2 | |
| QMV_NSG = 1 | |
| GEMM_BM = 32 | |
| GEMM_BN = 32 | |
| GEMM_BK = 32 | |
| GEMM_MIN_BATCH = 32 | |
| _HEADER = r""" | |
| #include <metal_simdgroup> | |
| #include <metal_simdgroup_matrix> | |
| #include <metal_stdlib> | |
| using namespace metal; | |
| // code*x for four 2-bit lanes packed in one byte. x1/x2/x3 are already | |
| // divided by 4, 16 and 64 so the masked field does not need a shift. | |
| #define TERN_DOT4(b, x0, x1, x2, x3) \ | |
| (((x0) * float((b) & 3u) + (x1) * float((b) & 0x0cu)) \ | |
| + ((x2) * float((b) & 0x30u) + (x3) * float((b) & 0xc0u))) | |
| #define TERN_DOT4H(b, x0, x1, x2, x3) \ | |
| (((x0) * half((b) & 3u) + (x1) * half((b) & 0x0cu)) \ | |
| + ((x2) * half((b) & 0x30u) + (x3) * half((b) & 0xc0u))) | |
| """ | |
| def metal(name: str, source: str, header: str = _HEADER): | |
| # MLX 0.22 metal_kernel has no compile_options. The cache key does not | |
| # include the source, so a body change must use a new name. | |
| key = (name, header[:40]) | |
| if key not in _KERNELS: | |
| _KERNELS[key] = mx.fast.metal_kernel( | |
| name=name, | |
| input_names=["x", "codes", "scales", "meta"], | |
| output_names=["y"], | |
| header=header, | |
| source=source, | |
| ensure_row_contiguous=False, | |
| ) | |
| return _KERNELS[key] | |
| def meta_u32(values: list[int]) -> mx.array: | |
| key = tuple(int(v) for v in values) | |
| buf = _META.get(key) | |
| if buf is None: | |
| buf = mx.array(list(key), dtype=mx.uint32) | |
| mx.eval(buf) | |
| _META[key] = buf | |
| return buf | |
| def _prepare(x, codes, scales, k, group): | |
| if group not in GROUPS: | |
| raise ValueError(f"group {group} not in {GROUPS}") | |
| if k % group != 0: | |
| raise ValueError(f"K {k} is not a multiple of {group}") | |
| if x.ndim == 1: | |
| x = x.reshape(1, -1) | |
| if not isinstance(x, mx.array): | |
| x = mx.array(x) | |
| if x.dtype != mx.float32: | |
| x = x.astype(mx.float32) | |
| if not isinstance(codes, mx.array): | |
| codes = mx.array(codes) | |
| if codes.dtype != mx.uint8: | |
| codes = codes.astype(mx.uint8) | |
| if not isinstance(scales, mx.array): | |
| scales = mx.array(scales) | |
| if int(x.shape[-1]) != k: | |
| raise ValueError(f"x last dim {x.shape[-1]} != K {k}") | |
| # The kernels index raw row-major buffers (ensure_row_contiguous=False), | |
| # so transposed or sliced views must be copied first. No-op if contiguous. | |
| x = mx.contiguous(x) | |
| codes = mx.contiguous(codes) | |
| scales = mx.contiguous(scales) | |
| return x, codes, scales, int(x.shape[0]), int(codes.shape[0]) | |
| # Kernel sources are written for group 64. Group 128 only changes how a | |
| # weight index maps to its scale: k >> 7 instead of k >> 6, and one more | |
| # lane bit per scale. Group 64 sources are left byte-identical. | |
| _REGROUP_128 = ( | |
| ("k >> 6", "k >> 7"), | |
| ("t >> 6", "t >> 7"), | |
| ("(lane >> 2)", "(lane >> 3)"), | |
| ("(lane >> 4)", "(lane >> 5)"), | |
| ("((k0 + wc) >> 6)", "((k0 + wc) >> 7)"), | |
| ) | |
| def _regroup(source: str, group: int) -> str: | |
| if group == 64: | |
| return source | |
| if group != 128: | |
| raise ValueError(f"group {group} not in {GROUPS}") | |
| for old, new in _REGROUP_128: | |
| source = source.replace(old, new) | |
| if ">> 6" in source: | |
| raise AssertionError("group 128 source still indexes scales by 64") | |
| return source | |
| def _gname(group: int) -> str: | |
| return "" if group == 64 else f"_gs{group}" | |
| def _dot16_expr(w: str, vecdot: bool = False) -> str: | |
| if not vecdot: | |
| return ( | |
| f"TERN_DOT4({w} & 0xffu, xv0, xv1, xv2, xv3)" | |
| f" + TERN_DOT4(({w} >> 8u) & 0xffu, xv4, xv5, xv6, xv7)" | |
| f" + TERN_DOT4(({w} >> 16u) & 0xffu, xv8, xv9, xv10, xv11)" | |
| f" + TERN_DOT4(({w} >> 24u) & 0xffu, xv12, xv13, xv14, xv15)" | |
| ) | |
| # Packed dot so the four masked lanes stay in one float4. | |
| def one(byte: str, xs: str) -> str: | |
| return ( | |
| f"dot(float4(float({byte} & 3u), float({byte} & 0x0cu), " | |
| f"float({byte} & 0x30u), float({byte} & 0xc0u)), float4({xs}))" | |
| ) | |
| return ( | |
| f"{one(f'({w} & 0xffu)', 'xv0, xv1, xv2, xv3')}" | |
| f" + {one(f'(({w} >> 8u) & 0xffu)', 'xv4, xv5, xv6, xv7')}" | |
| f" + {one(f'(({w} >> 16u) & 0xffu)', 'xv8, xv9, xv10, xv11')}" | |
| f" + {one(f'(({w} >> 24u) & 0xffu)', 'xv12, xv13, xv14, xv15')}" | |
| ) | |
| def _qmv_source(nsg: int, rows: int, guarded: bool, blocks: int | None = None, vecdot: bool = False, narrow: bool = False, half_dot: bool = False, chunk128: bool = False) -> str: | |
| if rows not in (2, 4, 8) or nsg not in (1, 2, 4): | |
| raise ValueError(f"qmv rows={rows} nsg={nsg}") | |
| if blocks is not None and blocks < 1: | |
| raise ValueError(f"qmv blocks={blocks}") | |
| acc_init = "\n".join(f" float acc{r} = 0.0f;" for r in range(rows)) | |
| def w16_loads() -> str: | |
| lines = [] | |
| for r in range(rows): | |
| addr = ( | |
| f"*reinterpret_cast<const device uint*>(codes + (row0 + {r}u) * row_bytes + byte0 + (lane << 2))" | |
| ) | |
| if guarded: | |
| lines.append(f"uint w{r} = 0u; if (row0 + {r}u < N) w{r} = {addr};") | |
| else: | |
| lines.append(f"uint w{r} = {addr};") | |
| return "\n ".join(lines) | |
| def w4_loads() -> str: | |
| lines = [] | |
| for r in range(rows): | |
| addr = f"uint(codes[(row0 + {r}u) * row_bytes + byte0 + lane])" | |
| if guarded: | |
| lines.append(f"uint c{r} = 0u; if (row0 + {r}u < N) c{r} = {addr};") | |
| else: | |
| lines.append(f"uint c{r} = {addr};") | |
| return "\n ".join(lines) | |
| def apply(scale_shift: int, src: str) -> str: | |
| lines = [] | |
| for r in range(rows): | |
| fma = ( | |
| f"acc{r} = fma(float(scales[(row0 + {r}u) * n_groups + g0 + (lane >> {scale_shift})]), " | |
| f"d{r} - sumx, acc{r});" | |
| ) | |
| if guarded: | |
| lines.append(f"if (row0 + {r}u < N) {{ {fma} }}") | |
| else: | |
| lines.append(fma) | |
| return "\n ".join(lines) | |
| def tail_body() -> str: | |
| lines = [ | |
| "uint sh = (t & 3u) * 2u;", | |
| "uint byte = t >> 2;", | |
| "uint g = t >> 6;", | |
| ] | |
| for r in range(rows): | |
| body = ( | |
| f"uint code = (uint(codes[(row0 + {r}u) * row_bytes + byte]) >> sh) & 3u;" | |
| f"acc{r} = fma((float(code) - 1.0f) * float(scales[(row0 + {r}u) * n_groups + g]), xv, acc{r});" | |
| ) | |
| if guarded: | |
| lines.append(f"if (row0 + {r}u < N) {{ {body} }}") | |
| else: | |
| lines.append("{ " + body + " }") | |
| return "\n ".join(lines) | |
| red = "\n ".join(f"float red{r} = simd_sum(acc{r});" for r in range(rows)) | |
| writes = [] | |
| for r in range(rows): | |
| cond = f"lane == 0u && row0 + {r}u < N" if guarded else "lane == 0u" | |
| writes.append(f"if ({cond}) y[b * N + row0 + {r}u] = red{r};") | |
| store = red + "\n " + "\n ".join(writes) | |
| dots16 = "\n ".join( | |
| f"float d{r} = {_dot16_expr(f'w{r}', vecdot)};" for r in range(rows) | |
| ) | |
| dots4 = "\n ".join( | |
| f"float d{r} = TERN_DOT4(c{r}, xv0, xv1, xv2, xv3);" for r in range(rows) | |
| ) | |
| step512 = f""" const device float* xp = x + b * K + k + (lane << 4); | |
| float4 a0 = *reinterpret_cast<const device float4*>(xp); | |
| float4 a1 = *reinterpret_cast<const device float4*>(xp + 4); | |
| float4 a2 = *reinterpret_cast<const device float4*>(xp + 8); | |
| float4 a3 = *reinterpret_cast<const device float4*>(xp + 12); | |
| float sumx = ((a0.x + a0.y) + (a0.z + a0.w)) + ((a1.x + a1.y) + (a1.z + a1.w)); | |
| sumx += ((a2.x + a2.y) + (a2.z + a2.w)) + ((a3.x + a3.y) + (a3.z + a3.w)); | |
| float xv0 = a0.x; float xv1 = a0.y * 0.25f; float xv2 = a0.z * 0.0625f; float xv3 = a0.w * 0.015625f; | |
| float xv4 = a1.x; float xv5 = a1.y * 0.25f; float xv6 = a1.z * 0.0625f; float xv7 = a1.w * 0.015625f; | |
| float xv8 = a2.x; float xv9 = a2.y * 0.25f; float xv10 = a2.z * 0.0625f; float xv11 = a2.w * 0.015625f; | |
| float xv12 = a3.x; float xv13 = a3.y * 0.25f; float xv14 = a3.z * 0.0625f; float xv15 = a3.w * 0.015625f; | |
| uint byte0 = k >> 2; | |
| uint g0 = k >> 6; | |
| {w16_loads()} | |
| {dots16} | |
| {apply(2, "w")}""" | |
| if narrow: | |
| # One float4 of x is live at a time so the large-N grid can stay resident. | |
| shifts = (0, 8, 16, 24) | |
| nlines = [ | |
| "const device float* xp = x + b * K + k + (lane << 4);", | |
| "uint byte0 = k >> 2;", | |
| "uint g0 = k >> 6;", | |
| w16_loads(), | |
| ] | |
| for r in range(rows): | |
| nlines.append(f"float part{r} = 0.0f;") | |
| for bi, sh in enumerate(shifts): | |
| nlines.append("{") | |
| nlines.append( | |
| f"float4 a = *reinterpret_cast<const device float4*>(xp + {bi * 4});" | |
| ) | |
| nlines.append("float sum4 = (a.x + a.y) + (a.z + a.w);") | |
| nlines.append( | |
| "float u0 = a.x; float u1 = a.y * 0.25f; " | |
| "float u2 = a.z * 0.0625f; float u3 = a.w * 0.015625f;" | |
| ) | |
| for r in range(rows): | |
| expr = ( | |
| f"part{r} += TERN_DOT4((w{r} >> {sh}u) & 0xffu, u0, u1, u2, u3) - sum4;" | |
| ) | |
| if guarded: | |
| nlines.append(f"if (row0 + {r}u < N) {{ {expr} }}") | |
| else: | |
| nlines.append(expr) | |
| nlines.append("}") | |
| for r in range(rows): | |
| fma = ( | |
| f"acc{r} = fma(float(scales[(row0 + {r}u) * n_groups + g0 + (lane >> 2)]), " | |
| f"part{r}, acc{r});" | |
| ) | |
| if guarded: | |
| nlines.append(f"if (row0 + {r}u < N) {{ {fma} }}") | |
| else: | |
| nlines.append(fma) | |
| step512 = "\n ".join(nlines) | |
| if half_dot: | |
| # Half products, float partial. One float4 of x is live per byte. | |
| shifts = (0, 8, 16, 24) | |
| hlines = [ | |
| "const device float* xp = x + b * K + k + (lane << 4);", | |
| "uint byte0 = k >> 2;", | |
| "uint g0 = k >> 6;", | |
| w16_loads(), | |
| ] | |
| for r in range(rows): | |
| hlines.append(f"float part{r} = 0.0f;") | |
| for bi, sh in enumerate(shifts): | |
| hlines.append("{") | |
| hlines.append( | |
| f"float4 a = *reinterpret_cast<const device float4*>(xp + {bi * 4});" | |
| ) | |
| hlines.append( | |
| "half hx = half(a.x); half hy = half(a.y); " | |
| "half hz = half(a.z); half hw = half(a.w);" | |
| ) | |
| hlines.append("half sum4 = (hx + hy) + (hz + hw);") | |
| hlines.append( | |
| "half u0 = hx; half u1 = hy * 0.25h; " | |
| "half u2 = hz * 0.0625h; half u3 = hw * 0.015625h;" | |
| ) | |
| for r in range(rows): | |
| expr = ( | |
| f"part{r} += float(TERN_DOT4H((w{r} >> {sh}u) & 0xffu, u0, u1, u2, u3)) " | |
| f"- float(sum4);" | |
| ) | |
| if guarded: | |
| hlines.append(f"if (row0 + {r}u < N) {{ {expr} }}") | |
| else: | |
| hlines.append(expr) | |
| hlines.append("}") | |
| for r in range(rows): | |
| fma = ( | |
| f"acc{r} = fma(float(scales[(row0 + {r}u) * n_groups + g0 + (lane >> 2)]), " | |
| f"part{r}, acc{r});" | |
| ) | |
| if guarded: | |
| hlines.append(f"if (row0 + {r}u < N) {{ {fma} }}") | |
| else: | |
| hlines.append(fma) | |
| step512 = "\n ".join(hlines) | |
| if chunk128: | |
| # Four values per lane. Fewer live registers than the 16-wide body. | |
| k_loops = ( | |
| " uint k = 0u;\n" | |
| " for (; k + 128u <= K; k += 128u) {\n" | |
| " const device float* xp = x + b * K + k + (lane << 2);\n" | |
| " float4 a0 = *reinterpret_cast<const device float4*>(xp);\n" | |
| " float sumx = (a0.x + a0.y) + (a0.z + a0.w);\n" | |
| " float xv0 = a0.x; float xv1 = a0.y * 0.25f; float xv2 = a0.z * 0.0625f; float xv3 = a0.w * 0.015625f;\n" | |
| " uint byte0 = k >> 2;\n" | |
| " uint g0 = k >> 6;\n" | |
| f" {w4_loads()}\n" | |
| f" {dots4}\n" | |
| f" {apply(4, 'c')}\n" | |
| " }\n" | |
| " for (uint t = k + lane; t < K; t += 32u) {\n" | |
| " float xv = x[b * K + t];\n" | |
| f" {tail_body()}\n" | |
| " }\n" | |
| ) | |
| elif blocks is None: | |
| # K is not a multiple of 512 (expert down is 640). Keep the short tail. | |
| k_loops = ( | |
| " uint k = 0u;\n" | |
| " for (; k + 512u <= K; k += 512u) {\n" | |
| f"{step512}\n" | |
| " }\n" | |
| " for (; k + 128u <= K; k += 128u) {\n" | |
| " const device float* xp = x + b * K + k + (lane << 2);\n" | |
| " float4 a0 = *reinterpret_cast<const device float4*>(xp);\n" | |
| " float sumx = (a0.x + a0.y) + (a0.z + a0.w);\n" | |
| " float xv0 = a0.x; float xv1 = a0.y * 0.25f; float xv2 = a0.z * 0.0625f; float xv3 = a0.w * 0.015625f;\n" | |
| " uint byte0 = k >> 2;\n" | |
| " uint g0 = k >> 6;\n" | |
| f" {w4_loads()}\n" | |
| f" {dots4}\n" | |
| f" {apply(4, 'c')}\n" | |
| " }\n" | |
| " for (uint t = k + lane; t < K; t += 32u) {\n" | |
| " float xv = x[b * K + t];\n" | |
| f" {tail_body()}\n" | |
| " }\n" | |
| ) | |
| else: | |
| # Bake the trip count and drop the tail so the mask body can unroll. | |
| k_loops = ( | |
| " #pragma unroll\n" | |
| f" for (uint blk = 0u; blk < {int(blocks)}u; ++blk) {{\n" | |
| " uint k = blk * 512u;\n" | |
| f"{step512}\n" | |
| " }\n" | |
| ) | |
| return f""" | |
| constexpr uint NSG = {nsg}u; | |
| constexpr uint ROWS = {rows}u; | |
| uint lane = thread_index_in_simdgroup; | |
| uint sg = simdgroup_index_in_threadgroup; | |
| uint tile = threadgroup_position_in_grid.x; | |
| uint b = threadgroup_position_in_grid.y; | |
| uint N = meta[0]; | |
| uint K = meta[1]; | |
| uint row_bytes = meta[2]; | |
| uint n_groups = meta[3]; | |
| uint row0 = (tile * NSG + sg) * ROWS; | |
| {acc_init} | |
| {k_loops} | |
| {store} | |
| """ | |
| def _gemm_source(bm: int, bn: int, bk: int, half_w: bool) -> str: | |
| if bm % 16 or bn % 16 or bk % 32 or 64 % bk: | |
| raise ValueError(f"bad tile {bm} {bn} {bk}") | |
| nsg_n = bn // 16 | |
| nsg_m = bm // 16 | |
| tg = nsg_n * nsg_m * 32 | |
| per_w = (bn * bk) // tg | |
| per_x = (bm * bk) // tg | |
| if per_w % 4 or per_x % 4 or bk % per_w or bk % per_x or per_w > bk or per_x > bk: | |
| raise ValueError(f"tile {bm}x{bn}x{bk} does not split across {tg} threads") | |
| lda_x = bk + 4 | |
| lda_w = bk + (8 if half_w else 4) | |
| wtype = "half" if half_w else "float" | |
| nbytes = per_w // 4 | |
| if nbytes % 4 == 0: | |
| w_load = f""" | |
| uint e = tid * {per_w}u; | |
| uint wr = e / BK; | |
| uint wc = e - wr * BK; | |
| uint grow = n0 + wr; | |
| const device uint* wp = reinterpret_cast<const device uint*>( | |
| codes + grow * row_bytes + ((k0 + wc) >> 2)); | |
| float s = float(scales[grow * n_groups + ((k0 + wc) >> 6)]); | |
| float s1 = s * 0.25f; | |
| float s2 = s * 0.0625f; | |
| float s3 = s * 0.015625f; | |
| #pragma unroll | |
| for (uint j = 0u; j < {nbytes // 4}u; ++j) {{ | |
| uint w32 = wp[j]; | |
| #pragma unroll | |
| for (uint i = 0u; i < 4u; ++i) {{ | |
| uint bcode = (w32 >> (i * 8u)) & 0xffu; | |
| float d0 = s * float(bcode & 3u) - s; | |
| float d1 = s1 * float(bcode & 0x0cu) - s; | |
| float d2 = s2 * float(bcode & 0x30u) - s; | |
| float d3 = s3 * float(bcode & 0xc0u) - s; | |
| uint kc = wc + (j * 4u + i) * 4u; | |
| *reinterpret_cast<threadgroup {wtype}4*>(&WBUF[wr][kc]) = {wtype}4({wtype}(d0), {wtype}(d1), {wtype}(d2), {wtype}(d3)); | |
| }} | |
| }} | |
| """ | |
| else: | |
| w_load = f""" | |
| uint e = tid * {per_w}u; | |
| uint wr = e / BK; | |
| uint wc = e - wr * BK; | |
| uint grow = n0 + wr; | |
| const device uint8_t* wp = codes + grow * row_bytes + ((k0 + wc) >> 2); | |
| float s = float(scales[grow * n_groups + ((k0 + wc) >> 6)]); | |
| float s1 = s * 0.25f; | |
| float s2 = s * 0.0625f; | |
| float s3 = s * 0.015625f; | |
| #pragma unroll | |
| for (uint i = 0u; i < {nbytes}u; ++i) {{ | |
| uint bcode = uint(wp[i]); | |
| float d0 = s * float(bcode & 3u) - s; | |
| float d1 = s1 * float(bcode & 0x0cu) - s; | |
| float d2 = s2 * float(bcode & 0x30u) - s; | |
| float d3 = s3 * float(bcode & 0xc0u) - s; | |
| uint kc = wc + i * 4u; | |
| *reinterpret_cast<threadgroup {wtype}4*>(&WBUF[wr][kc]) = {wtype}4({wtype}(d0), {wtype}(d1), {wtype}(d2), {wtype}(d3)); | |
| }} | |
| """ | |
| n4 = per_x // 4 | |
| x_load = f""" | |
| uint ex = tid * {per_x}u; | |
| uint xr = ex / BK; | |
| uint xc = ex - xr * BK; | |
| const device float* xp = x + (b0 + xr) * K + (k0 + xc); | |
| #pragma unroll | |
| for (uint i = 0u; i < {n4}u; ++i) {{ | |
| float4 v = *reinterpret_cast<const device float4*>(xp + i * 4u); | |
| *reinterpret_cast<threadgroup float4*>(&XBUF[xr][xc + i * 4u]) = v; | |
| }} | |
| """ | |
| return f""" | |
| constexpr uint BM = {bm}u; | |
| constexpr uint BN = {bn}u; | |
| constexpr uint BK = {bk}u; | |
| constexpr uint LDAX = {lda_x}u; | |
| constexpr uint LDAW = {lda_w}u; | |
| uint tid = thread_position_in_threadgroup.x; | |
| uint sg = simdgroup_index_in_threadgroup; | |
| uint N = meta[0]; | |
| uint K = meta[1]; | |
| uint row_bytes = meta[2]; | |
| uint n_groups = meta[3]; | |
| uint B = meta[4]; | |
| uint n0 = threadgroup_position_in_grid.x * BN; | |
| uint b0 = threadgroup_position_in_grid.y * BM; | |
| threadgroup float XBUF[BM][LDAX]; | |
| threadgroup {wtype} WBUF[BN][LDAW]; | |
| uint sg_n = sg % {nsg_n}u; | |
| uint sg_m = sg / {nsg_n}u; | |
| uint mbase = sg_m * 16u; | |
| uint nbase = sg_n * 16u; | |
| ushort lane = thread_index_in_simdgroup; | |
| short qid = short(lane) >> 2; | |
| short fm = (qid & 4) + ((short(lane) >> 1) & 3); | |
| short fn = ((qid & 2) << 1) + ((short(lane) & 1) << 1); | |
| uint ufm = uint(fm); | |
| uint ufn = uint(fn); | |
| simdgroup_matrix<float, 8, 8> c00(0.0f); | |
| simdgroup_matrix<float, 8, 8> c01(0.0f); | |
| simdgroup_matrix<float, 8, 8> c10(0.0f); | |
| simdgroup_matrix<float, 8, 8> c11(0.0f); | |
| for (uint k0 = 0u; k0 < K; k0 += BK) {{ | |
| {x_load} | |
| {w_load} | |
| threadgroup_barrier(mem_flags::mem_threadgroup); | |
| #pragma unroll | |
| for (uint kk = 0u; kk < BK; kk += 8u) {{ | |
| simdgroup_matrix<float, 8, 8> a0(0.0f); | |
| simdgroup_matrix<float, 8, 8> a1(0.0f); | |
| simdgroup_matrix<float, 8, 8> b0m(0.0f); | |
| simdgroup_matrix<float, 8, 8> b1m(0.0f); | |
| float2 av0 = *reinterpret_cast<threadgroup float2*>(&XBUF[mbase + ufm][kk + ufn]); | |
| float2 av1 = *reinterpret_cast<threadgroup float2*>(&XBUF[mbase + 8u + ufm][kk + ufn]); | |
| a0.thread_elements()[0] = av0.x; | |
| a0.thread_elements()[1] = av0.y; | |
| a1.thread_elements()[0] = av1.x; | |
| a1.thread_elements()[1] = av1.y; | |
| b0m.thread_elements()[0] = float(WBUF[nbase + ufn][kk + ufm]); | |
| b0m.thread_elements()[1] = float(WBUF[nbase + ufn + 1u][kk + ufm]); | |
| b1m.thread_elements()[0] = float(WBUF[nbase + 8u + ufn][kk + ufm]); | |
| b1m.thread_elements()[1] = float(WBUF[nbase + 8u + ufn + 1u][kk + ufm]); | |
| simdgroup_multiply_accumulate(c00, a0, b0m, c00); | |
| simdgroup_multiply_accumulate(c01, a0, b1m, c01); | |
| simdgroup_multiply_accumulate(c10, a1, b0m, c10); | |
| simdgroup_multiply_accumulate(c11, a1, b1m, c11); | |
| }} | |
| threadgroup_barrier(mem_flags::mem_threadgroup); | |
| }} | |
| uint gm = b0 + mbase + ufm; | |
| uint gn = n0 + nbase + ufn; | |
| if (gm < B) {{ | |
| if (gn < N) y[gm * N + gn] = c00.thread_elements()[0]; | |
| if (gn + 1u < N) y[gm * N + gn + 1u] = c00.thread_elements()[1]; | |
| if (gn + 8u < N) y[gm * N + gn + 8u] = c01.thread_elements()[0]; | |
| if (gn + 9u < N) y[gm * N + gn + 9u] = c01.thread_elements()[1]; | |
| }} | |
| uint gm8 = gm + 8u; | |
| if (gm8 < B) {{ | |
| if (gn < N) y[gm8 * N + gn] = c10.thread_elements()[0]; | |
| if (gn + 1u < N) y[gm8 * N + gn + 1u] = c10.thread_elements()[1]; | |
| if (gn + 8u < N) y[gm8 * N + gn + 8u] = c11.thread_elements()[0]; | |
| if (gn + 9u < N) y[gm8 * N + gn + 9u] = c11.thread_elements()[1]; | |
| }} | |
| """ | |
| def _qmv_mlx_source(nsg: int, rows: int, guarded: bool) -> str: | |
| """MLX qmv_fast layout: 16 values per lane, one K loop, byte loads.""" | |
| if rows not in (2, 4, 8) or nsg not in (1, 2, 4): | |
| raise ValueError(f"qmv mlx rows={rows} nsg={nsg}") | |
| init = "\n ".join(f"float result{r} = 0.0f;" for r in range(rows)) | |
| dots = [] | |
| for r in range(rows): | |
| body = ( | |
| f"const device uint8_t* wl = codes + (out_row + {r}u) * row_bytes + (lane << 2) + (k >> 2);" | |
| f"float s = float(scales[(out_row + {r}u) * n_groups + (k >> 6) + (lane >> 2)]);" | |
| "float accum = 0.0f;" | |
| "accum += xt0 * float(wl[0] & 3u) + xt1 * float(wl[0] & 0x0cu);" | |
| "accum += xt2 * float(wl[0] & 0x30u) + xt3 * float(wl[0] & 0xc0u);" | |
| "accum += xt4 * float(wl[1] & 3u) + xt5 * float(wl[1] & 0x0cu);" | |
| "accum += xt6 * float(wl[1] & 0x30u) + xt7 * float(wl[1] & 0xc0u);" | |
| "accum += xt8 * float(wl[2] & 3u) + xt9 * float(wl[2] & 0x0cu);" | |
| "accum += xt10 * float(wl[2] & 0x30u) + xt11 * float(wl[2] & 0xc0u);" | |
| "accum += xt12 * float(wl[3] & 3u) + xt13 * float(wl[3] & 0x0cu);" | |
| "accum += xt14 * float(wl[3] & 0x30u) + xt15 * float(wl[3] & 0xc0u);" | |
| f"result{r} = fma(s, accum - sum, result{r});" | |
| ) | |
| if guarded: | |
| dots.append(f"if (out_row + {r}u < N) {{ {body} }}") | |
| else: | |
| dots.append("{ " + body + " }") | |
| row_gate = "\n ".join(dots) | |
| if guarded: | |
| store = "\n ".join( | |
| f"float red{r} = simd_sum(result{r}); if (lane == 0u && out_row + {r}u < N) y[b * N + out_row + {r}u] = red{r};" | |
| for r in range(rows) | |
| ) | |
| else: | |
| store = "\n ".join( | |
| f"float red{r} = simd_sum(result{r}); if (lane == 0u) y[b * N + out_row + {r}u] = red{r};" | |
| for r in range(rows) | |
| ) | |
| return f""" | |
| uint lane = thread_index_in_simdgroup; | |
| uint sg = simdgroup_index_in_threadgroup; | |
| uint tile = threadgroup_position_in_grid.x; | |
| uint b = threadgroup_position_in_grid.y; | |
| uint N = meta[0]; | |
| uint K = meta[1]; | |
| uint row_bytes = meta[2]; | |
| uint n_groups = meta[3]; | |
| uint out_row = (tile * {nsg}u + sg) * {rows}u; | |
| {init} | |
| for (uint k = 0u; k < K; k += 512u) {{ | |
| const device float* xp = x + b * K + k + (lane << 4); | |
| float4 a0 = *reinterpret_cast<const device float4*>(xp); | |
| float4 a1 = *reinterpret_cast<const device float4*>(xp + 4); | |
| float4 a2 = *reinterpret_cast<const device float4*>(xp + 8); | |
| float4 a3 = *reinterpret_cast<const device float4*>(xp + 12); | |
| float sum = ((a0.x + a0.y) + (a0.z + a0.w)) + ((a1.x + a1.y) + (a1.z + a1.w)); | |
| sum += ((a2.x + a2.y) + (a2.z + a2.w)) + ((a3.x + a3.y) + (a3.z + a3.w)); | |
| float xt0 = a0.x; float xt1 = a0.y * 0.25f; float xt2 = a0.z * 0.0625f; float xt3 = a0.w * 0.015625f; | |
| float xt4 = a1.x; float xt5 = a1.y * 0.25f; float xt6 = a1.z * 0.0625f; float xt7 = a1.w * 0.015625f; | |
| float xt8 = a2.x; float xt9 = a2.y * 0.25f; float xt10 = a2.z * 0.0625f; float xt11 = a2.w * 0.015625f; | |
| float xt12 = a3.x; float xt13 = a3.y * 0.25f; float xt14 = a3.z * 0.0625f; float xt15 = a3.w * 0.015625f; | |
| {row_gate} | |
| }} | |
| {store} | |
| """ | |
| def run_qmv_mlx(x, codes, scales, k, rows: int = 4, nsg: int = 2, group: int = GROUP): | |
| x, codes, scales, b, n = _prepare(x, codes, scales, k, group) | |
| if k % 512: | |
| raise ValueError(f"qmv mlx needs K multiple of 512, got {k}") | |
| rows_per_tg = nsg * rows | |
| guarded = (n % rows_per_tg) != 0 | |
| tg = nsg * 32 | |
| n_tiles = (n + rows_per_tg - 1) // rows_per_tg | |
| name = f"mlxtern3_qmvmlx_r{rows}_s{nsg}" + ("_g" if guarded else "_a") + _gname(group) | |
| ker = metal(name, _regroup(_qmv_mlx_source(nsg, rows, guarded), group)) | |
| (y,) = ker( | |
| inputs=[x, codes, scales, meta_u32([n, k, k // 4, k // group, b])], | |
| template=[("T", mx.float32)], | |
| grid=(n_tiles * tg, b, 1), | |
| threadgroup=(tg, 1, 1), | |
| output_shapes=[(b, n)], | |
| output_dtypes=[mx.float32], | |
| ) | |
| return y | |
| def run_qmv(x, codes, scales, k, rows: int = QMV_ROWS, nsg: int = QMV_NSG, group: int = GROUP, vecdot: bool = False, specialize: bool = True, narrow: bool = False, half_dot: bool = False, chunk128: bool = False): | |
| x, codes, scales, b, n = _prepare(x, codes, scales, k, group) | |
| if rows not in (2, 4, 8) or nsg not in (1, 2, 4): | |
| raise ValueError(f"qmv rows={rows} nsg={nsg}") | |
| rows_per_tg = nsg * rows | |
| guarded = (n % rows_per_tg) != 0 | |
| tg = nsg * 32 | |
| n_tiles = (n + rows_per_tg - 1) // rows_per_tg | |
| blocks = None if chunk128 else ((k // 512) if (specialize and k % 512 == 0) else None) | |
| name = f"mlxtern3_u2q2_r{rows}_s{nsg}" | |
| if chunk128: | |
| name += "_c128" | |
| elif blocks is not None: | |
| name += f"_k{k}" | |
| if vecdot: | |
| name += "_v" | |
| if narrow: | |
| name += "_n" | |
| if half_dot: | |
| name += "_h" | |
| name += "_g" if guarded else "_a" | |
| name += "_fast" + _gname(group) | |
| ker = metal(name, _regroup(_qmv_source(nsg, rows, guarded, blocks, vecdot, narrow, half_dot, chunk128), group)) | |
| (y,) = ker( | |
| inputs=[x, codes, scales, meta_u32([n, k, k // 4, k // group, b])], | |
| template=[("T", mx.float32)], | |
| grid=(n_tiles * tg, b, 1), | |
| threadgroup=(tg, 1, 1), | |
| output_shapes=[(b, n)], | |
| output_dtypes=[mx.float32], | |
| ) | |
| return y | |
| def run_gemm(x, codes, scales, k, bm: int = GEMM_BM, bn: int = GEMM_BN, bk: int = GEMM_BK, group: int = GROUP, half_w: bool = True): | |
| x, codes, scales, b, n = _prepare(x, codes, scales, k, group) | |
| if k % bk or n % bn or b % bm: | |
| raise ValueError(f"gemm tile {bm}x{bn}x{bk} does not cover N={n} K={k} B={b}") | |
| nsg = (bm // 16) * (bn // 16) | |
| tg = nsg * 32 | |
| name = f"mlxtern3_u2g2_{bm}x{bn}x{bk}" + ("" if half_w else "_fw") + "_fast" + _gname(group) | |
| ker = metal(name, _regroup(_gemm_source(bm, bn, bk, half_w), group)) | |
| (y,) = ker( | |
| inputs=[x, codes, scales, meta_u32([n, k, k // 4, k // group, b])], | |
| template=[("T", mx.float32)], | |
| grid=((n // bn) * tg, b // bm, 1), | |
| threadgroup=(tg, 1, 1), | |
| output_shapes=[(b, n)], | |
| output_dtypes=[mx.float32], | |
| ) | |
| return y | |
| from collections import OrderedDict | |
| COMPILE_MAX_BATCH = 8 | |
| COMPILE_CACHE_SIZE = 64 | |
| _COMPILED: OrderedDict = OrderedDict() | |
| def _compiled(k: int, n: int, b: int, group: int, x_dtype, c_dtype, s_dtype): | |
| key = (k, n, b, group, x_dtype, c_dtype, s_dtype) | |
| fn = _COMPILED.get(key) | |
| if fn is not None: | |
| _COMPILED.move_to_end(key) | |
| return fn | |
| def _run(z, c, s): | |
| return _dispatch(z, c, s, k, group) | |
| fn = mx.compile(_run) | |
| _COMPILED[key] = fn | |
| while len(_COMPILED) > COMPILE_CACHE_SIZE: | |
| _COMPILED.popitem(last=False) | |
| return fn | |
| def _dispatch(x, codes, scales, k: int, group: int): | |
| b = 1 if x.ndim == 1 else int(x.shape[0]) | |
| n = int(codes.shape[0]) | |
| tile = _gemm_launch(b, n, k) | |
| if tile is not None: | |
| bm, bn, bk = tile | |
| return run_gemm(x, codes, scales, k, bm, bn, bk, group=group) | |
| head = _gemm_head(b, n, k) | |
| if head: | |
| # Batch >= 32 but not a multiple of 32: the leading multiple of 32 | |
| # rows go through the GEMM, the remainder through the GEMV. | |
| top = run_gemm(x[:head], codes, scales, k, GEMM_BM, GEMM_BN, GEMM_BK, group=group) | |
| rest = _gemv(x[head:], codes, scales, k, group) | |
| return mx.concatenate([top, rest], axis=0) | |
| return _gemv(x, codes, scales, k, group) | |
| def _gemv(x, codes, scales, k: int, group: int): | |
| # K multiple of 512 uses the MLX qmv_fast walk (2 rows, 1 simdgroup). | |
| # Shorter K, including the 640-wide expert down projection, keeps the | |
| # 128-wide body, which is faster there. | |
| if k % 512 == 0: | |
| return run_qmv_mlx(x, codes, scales, k, rows=2, nsg=1, group=group) | |
| return run_qmv(x, codes, scales, k, group=group) | |
| def _gemm_head(b: int, n: int, k: int) -> int: | |
| """Rows of a ragged batch that the GEMM can take; 0 if none.""" | |
| if b < GEMM_MIN_BATCH or n % 32 or k % 32: | |
| return 0 | |
| return (b // 32) * 32 | |
| def _gemm_launch(b: int, n: int, k: int) -> tuple[int, int, int] | None: | |
| if b < GEMM_MIN_BATCH or b % 32 or n % 32 or k % 32: | |
| return None | |
| # 32x32x32 keeps the shared tile small enough to stay resident on the M4 Pro. | |
| # Larger tiles were slower on the expert and are re-checked on the wide d3 shapes. | |
| return 32, 32, 32 | |
| def tern_matmul(x, codes, scales, k: int, group: int = GROUP) -> mx.array: | |
| """x [B, K] or [K], codes uint8 [N, K/4], scales [N, K/group] -> [B, N]. | |
| ``group`` is 64 (default) or 128 weights per scale. | |
| Batches up to ``COMPILE_MAX_BATCH`` reuse a compiled graph from a bounded | |
| LRU keyed by shape and dtype; weights are arguments, not captured, so the | |
| cache never keeps them alive. Larger batches dispatch the kernel directly. | |
| A vector input comes back as shape [1, N]. | |
| """ | |
| if not isinstance(x, mx.array): | |
| x = mx.array(x) | |
| if x.ndim == 0 or x.ndim > 2: | |
| raise ValueError(f"x must have shape [K] or [B, K], got ndim {x.ndim}") | |
| k = int(k) | |
| group = int(group) | |
| if group not in GROUPS: | |
| raise ValueError(f"group {group} not in {GROUPS}") | |
| if k <= 0 or k % group: | |
| raise ValueError(f"K {k} is not a positive multiple of {group}") | |
| if int(x.shape[-1]) != k: | |
| raise ValueError(f"x last dim {x.shape[-1]} != K {k}") | |
| if codes.ndim != 2 or int(codes.shape[1]) != k // 4: | |
| raise ValueError(f"codes shape {tuple(codes.shape)} != [N, K/4] with K={k}") | |
| n = int(codes.shape[0]) | |
| if tuple(scales.shape) != (n, k // group): | |
| raise ValueError(f"scales shape {tuple(scales.shape)} != ({n}, {k // group})") | |
| b = 1 if x.ndim == 1 else int(x.shape[0]) | |
| if b == 0 or n == 0: | |
| # An empty launch would hand Metal a zero-size buffer. | |
| return mx.zeros((b, n), dtype=mx.float32) | |
| if b > COMPILE_MAX_BATCH: | |
| return _dispatch(x, codes, scales, k, int(group)) | |
| fn = _compiled(k, n, b, int(group), x.dtype, codes.dtype, scales.dtype) | |
| return fn(x, codes, scales) | |