Download src/mlx_ternary/linear.py from Jakevin/mlx-ternary: direct link, hf CLI and curl.
- Browser
- Download file 6.23 kB
-
https://huggingface.co/Jakevin/mlx-ternary/resolve/main/src/mlx_ternary/linear.py
- Command line
-
hf download hf://Jakevin/mlx-ternary/src/mlx_ternary/linear.py
-
curl -L -o linear.py https://huggingface.co/Jakevin/mlx-ternary/resolve/main/src/mlx_ternary/linear.py
6.23 kB
| # Copyright 2026 Jakevin | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """A linear layer that runs the ternary kernel. | |
| ``TernaryLinear`` follows ``nn.Linear`` on the forward: ``y = x W^T + b``, | |
| with ``W`` stored packed. It also replaces an ``nn.QuantizedLinear`` whose | |
| affine 2-bit parameters are ternary (codes in {0, 1, 2}, bias = -scale). | |
| """ | |
| from __future__ import annotations | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| import numpy as np | |
| from .convert import from_mlx_2bit | |
| from .kernel import tern_matmul | |
| from .pack import GROUP, GROUPS, pack_trits | |
| class TernaryLinear(nn.Module): | |
| """Packed ternary affine layer. | |
| ``weight`` is uint8 ``[output_dims, input_dims / 4]``. ``scales`` is | |
| float32 ``[output_dims, input_dims / group_size]``. ``group_size`` is 64 | |
| (default) or 128. Parameters are frozen. | |
| """ | |
| def __init__(self, weight, scales, bias=None, group_size: int = GROUP): | |
| super().__init__() | |
| group_size = int(group_size) | |
| if group_size not in GROUPS: | |
| raise ValueError(f"group_size {group_size} not in {GROUPS}") | |
| if not isinstance(weight, mx.array): | |
| weight = mx.array(np.ascontiguousarray(weight)) | |
| if weight.dtype != mx.uint8: | |
| weight = weight.astype(mx.uint8) | |
| if not isinstance(scales, mx.array): | |
| scales = mx.array(np.asarray(scales, dtype=np.float32)) | |
| if scales.dtype != mx.float32: | |
| scales = scales.astype(mx.float32) | |
| weight = mx.contiguous(weight) | |
| scales = mx.contiguous(scales) | |
| if weight.ndim != 2 or scales.ndim != 2: | |
| raise ValueError("weight and scales must be matrices") | |
| n, row_bytes = (int(v) for v in weight.shape) | |
| k = row_bytes * 4 | |
| if k % group_size or scales.shape != (n, k // group_size): | |
| raise ValueError( | |
| f"weight {tuple(weight.shape)} and scales {tuple(scales.shape)} " | |
| f"are not a group-{group_size} ternary matrix" | |
| ) | |
| self.weight = weight | |
| self.scales = scales | |
| self.input_dims = k | |
| self.output_dims = n | |
| self.group_size = group_size | |
| if bias is not None: | |
| if not isinstance(bias, mx.array): | |
| bias = mx.array(bias) | |
| if tuple(bias.shape) != (n,): | |
| raise ValueError(f"bias shape {tuple(bias.shape)} != ({n},)") | |
| self.bias = bias | |
| self.freeze() | |
| def _extra_repr(self) -> str: | |
| return ( | |
| f"input_dims={self.input_dims}, output_dims={self.output_dims}, " | |
| f"bias={'bias' in self}, group_size={self.group_size}, bits=2" | |
| ) | |
| def __call__(self, x): | |
| if int(x.shape[-1]) != self.input_dims: | |
| raise ValueError(f"x last dim {x.shape[-1]} != input_dims {self.input_dims}") | |
| lead = x.shape[:-1] | |
| flat = x.reshape(-1, self.input_dims) | |
| y = tern_matmul(flat, self.weight, self.scales, self.input_dims, self.group_size) | |
| if y.dtype != x.dtype: | |
| y = y.astype(x.dtype) | |
| if "bias" in self: | |
| b = self.bias | |
| if b.dtype != y.dtype: | |
| b = b.astype(y.dtype) | |
| y = y + b | |
| return y.reshape(*lead, self.output_dims) | |
| def from_trits(cls, trits, scales, bias=None, group_size: int = GROUP) -> "TernaryLinear": | |
| """Build from int8 trits ``{-1,0,1}`` of shape ``[output, input]``.""" | |
| packed, _row = pack_trits(np.asarray(trits)) | |
| return cls(packed, scales, bias, group_size) | |
| def from_mlx_2bit(cls, weight, scales, biases, bias=None, group_size: int = GROUP) -> "TernaryLinear": | |
| """Build from MLX affine 2-bit ternary tensors.""" | |
| packed, scale_f = from_mlx_2bit(weight, scales, biases, group_size) | |
| return cls(packed, scale_f, bias, group_size) | |
| def from_quantized_linear(cls, layer: nn.Module) -> "TernaryLinear": | |
| """Replace one affine 2-bit ``QuantizedLinear`` that stores ternary weights.""" | |
| if not isinstance(layer, nn.QuantizedLinear): | |
| raise TypeError(f"expected QuantizedLinear, got {type(layer).__name__}") | |
| if layer.bits != 2 or layer.group_size not in GROUPS or layer.mode != "affine": | |
| raise ValueError( | |
| f"expected affine 2-bit group {GROUPS}, got bits={layer.bits} " | |
| f"group_size={layer.group_size} mode={layer.mode}" | |
| ) | |
| if layer.biases is None: | |
| raise ValueError("affine 2-bit ternary storage needs biases equal to -scales") | |
| extra = layer.bias if "bias" in layer else None | |
| return cls.from_mlx_2bit( | |
| layer.weight, layer.scales, layer.biases, bias=extra, group_size=layer.group_size | |
| ) | |
| def _replace_linear(value, path: str): | |
| if isinstance(value, nn.QuantizedLinear): | |
| try: | |
| return TernaryLinear.from_quantized_linear(value), 1 | |
| except ValueError as exc: | |
| raise ValueError(f"{path}: {exc}") from exc | |
| if isinstance(value, list): | |
| n = 0 | |
| for i, item in enumerate(list(value)): | |
| new, k = _replace_linear(item, f"{path}.{i}") | |
| if k: | |
| value[i] = new | |
| n += k | |
| return value, n | |
| if isinstance(value, dict) and not isinstance(value, nn.Module): | |
| n = 0 | |
| for key, item in list(value.items()): | |
| new, k = _replace_linear(item, f"{path}.{key}") | |
| if k: | |
| value[key] = new | |
| n += k | |
| return value, n | |
| return value, 0 | |
| def replace_quantized_linears(model: nn.Module) -> int: | |
| """Swap ternary affine 2-bit ``QuantizedLinear`` modules inside ``model``. | |
| The model object is updated in place. A ``QuantizedLinear`` that is not | |
| ternary raises. Returns how many layers were replaced. | |
| """ | |
| replaced = 0 | |
| def visit(path, mod): | |
| nonlocal replaced | |
| for key, value in list(mod.items()): | |
| where = f"{path}.{key}" if path else str(key) | |
| new, n = _replace_linear(value, where) | |
| if n and new is not value: | |
| mod[key] = new | |
| replaced += n | |
| model.apply_to_modules(visit) | |
| return replaced | |