Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d042c6124 | |||
| 0fbff353db | |||
| 0ad37e2bbf | |||
| 454bf9a22b | |||
| 133b5d3bd7 | |||
| abc52a0a48 | |||
| 6b42901468 | |||
| f353e0178b | |||
| f940cf3a95 | |||
| 34cbb8b51a | |||
| 4bc21cc17b | |||
| 9fd3e419ec | |||
| 743f4f7710 | |||
| 088e7ad7ca | |||
| 1d01257d2e | |||
| 2959af09fb | |||
| 8f1f88e5af | |||
| 606ff3ef06 | |||
| cd367819c7 | |||
| ba2cf3c0ee | |||
| 6c1a459314 | |||
| 3833c205c1 | |||
| 3356b0a017 | |||
| f6c94659d8 | |||
| d3bf847e6f | |||
| df6434185c | |||
| 974e17b43a | |||
| a82790a141 | |||
| 2aa31f95a7 | |||
| 4decc4d381 | |||
| 0d8272483b | |||
| 663b822de5 | |||
| f36977385f | |||
| 1e8fca4e0b | |||
| 61669b270f | |||
| f2b0262824 | |||
| 367d6d7686 |
@@ -1,100 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
apple: ml-explore/pr-approval@0.1.0
|
||||
|
||||
jobs:
|
||||
linux_build_and_test:
|
||||
docker:
|
||||
- image: cimg/python:3.9
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run style checks
|
||||
command: |
|
||||
pip install pre-commit
|
||||
pre-commit run --all
|
||||
if ! git diff --quiet; then echo 'Style checks failed, please install pre-commit and run pre-commit run --all and push the change'; exit 1; fi
|
||||
|
||||
mlx_lm_build_and_test:
|
||||
macos:
|
||||
xcode: "15.2.0"
|
||||
resource_class: m2pro.medium
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
brew install python@3.9
|
||||
python3.9 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install sentencepiece
|
||||
pip install unittest-xml-reporting
|
||||
pip install -e ".[test]"
|
||||
- run:
|
||||
name: Run Python tests
|
||||
command: |
|
||||
source env/bin/activate
|
||||
python -m xmlrunner discover -v tests -o test-results/
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
build_release:
|
||||
macos:
|
||||
xcode: "15.2.0"
|
||||
resource_class: m2pro.medium
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
brew install python@3.9
|
||||
python3.9 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install build
|
||||
pip install twine
|
||||
- run:
|
||||
name: Build and upload
|
||||
command: |
|
||||
source env/bin/activate
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
- store_artifacts:
|
||||
path: dist/
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
when:
|
||||
matches:
|
||||
pattern: "^(?!pull/)[-\\w]+$"
|
||||
value: << pipeline.git.branch >>
|
||||
jobs:
|
||||
- mlx_lm_build_and_test
|
||||
- linux_build_and_test
|
||||
|
||||
build_pypi_release:
|
||||
jobs:
|
||||
- build_release:
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
prb:
|
||||
when:
|
||||
matches:
|
||||
pattern: "^pull/\\d+(/head)?$"
|
||||
value: << pipeline.git.branch >>
|
||||
jobs:
|
||||
- hold:
|
||||
type: approval
|
||||
- apple/authenticate:
|
||||
context: pr-approval
|
||||
- mlx_lm_build_and_test:
|
||||
requires: [ hold ]
|
||||
- linux_build_and_test:
|
||||
requires: [ hold ]
|
||||
@@ -0,0 +1,16 @@
|
||||
name: 'Setup macOS Environment'
|
||||
description: 'Install dependencies for macOS'
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: 'Python version to use'
|
||||
required: false
|
||||
default: '3.10'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: "latest"
|
||||
python-version: ${{ inputs.python-version }}
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/head/main' }}
|
||||
|
||||
jobs:
|
||||
check_lint:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
|
||||
mac_build_and_test:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: [self-hosted, macos]
|
||||
needs: check_lint
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-macos
|
||||
- name: Install test dependencies
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
pip install unittest-xml-reporting
|
||||
pip install -e ".[test]"
|
||||
- name: Run tests
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
python -m xmlrunner discover -v tests -o test-results/
|
||||
@@ -0,0 +1,41 @@
|
||||
name: PyPI Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build_release:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mlx-lm
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Build package
|
||||
shell: sh
|
||||
run: |
|
||||
pip install build
|
||||
python -m build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
overwrite: true
|
||||
name: mlx-lm
|
||||
path: dist/*
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://upload.pypi.org/legacy/
|
||||
+18
-13
@@ -8,17 +8,22 @@ with a short description of your contribution(s) below. For example:
|
||||
MLX LM was developed with contributions from the following individuals:
|
||||
|
||||
- Shunta Saito: Added support for PLaMo models.
|
||||
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's
|
||||
`MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1`, `Mamba v2`, Z.ai &
|
||||
THUKEG's `GLM`, `GLM4`, Rednote `dots.llm1`, Baisu's `Ernie4.5 MoE`, inclusionAI's
|
||||
`Bailing MoE e.g. Ling-family`, Klear team - Kuaishou Technology's `Klear`, AI21 Lab's `Jamba`
|
||||
IBM's `Granite MoE`, Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's `Apertus`,
|
||||
Nikity's `Lille130m`, Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE` and `Olmo 3`;
|
||||
Helped add support for the following model architectures: Alibaba Qwen's `Qwen3 & Qwen3MoE)`;
|
||||
Added support for the following training algorithms: `Full Weight Fine-Tuning`, and the `Muon`
|
||||
optimizer; Added support for the following other features: `Multiple Optimizers
|
||||
to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
|
||||
- Gökdeniz Gülmez: Added support for the following architectures:
|
||||
OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1` and
|
||||
`Mamba v2`, Z.ai & THUKEG's `GLM`, `GLM4`, Rednote `dots.llm1`, Baidu's `Ernie4.5 MoE`,
|
||||
inclusionAI's `Bailing MoE e.g. Ling-family`, `Bailing MoE Linear e.g. Ling-Linear-family`,
|
||||
Klear team - Kuaishou Technology's `Klear`, AI21 Lab's `Jamba` IBM's `Granite MoE`,
|
||||
Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's `Apertus`, Nikity's `Lille130m`,
|
||||
Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE` and `Olmo 3`;
|
||||
Helped add support for the following model architectures:
|
||||
Alibaba Qwen's `Qwen3 & Qwen3MoE)`; Added support for the following training algorithms:
|
||||
`Full Weight Fine-Tuning`, and the `Muon` optimizer;
|
||||
Added support for the following other features:
|
||||
`Multiple Optimizers to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
|
||||
- Prince Canuma: Helped add support for the following model architectures:
|
||||
HuggingFace's `Starcoder2`, Cohere's `Cohere (1 and 2)`, Alibaba Qwen's `Qwen
|
||||
(2, 3 and MoE)`, Microsoft's `Phi (3 and 3.5 MoE)`, `BitNet1.58`, Meta's `Llama
|
||||
(3 and 4)`, Google DeepMind's `Gemma 3`, and InterLM's `InternLM 2.5`.
|
||||
HuggingFace's `Starcoder2`, Cohere's `Cohere (1 and 2)`, Alibaba Qwen's `Qwen (2, 3 and MoE)`,
|
||||
Microsoft's `Phi (3 and 3.5 MoE)`, `BitNet1.58`, Meta's `Llama (3 and 4)`, MinimaxAI's `MiniMax`,
|
||||
MoonshotAI's `Kimi-Linear`, LiquidAI's `LFM2` and `LFM2 MoE`,
|
||||
Google DeepMind's `Gemma 3`, TII's `Falcon H1` and InterLM's `InternLM 2.5`.
|
||||
- Ivan Fioravanti: Added support for the following architectures:
|
||||
ServiceNow-AI's `Apriel 1.5`, Tencent's `Hunyuan Dense V1` and `Hunyuan MoE V1`.
|
||||
@@ -236,45 +236,19 @@ for more usage details.
|
||||
|
||||
### Supported Models
|
||||
|
||||
`mlx-lm` supports thousands of Hugging Face format LLMs. If the model you want to
|
||||
run is not supported, file an
|
||||
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet,
|
||||
submit a pull request.
|
||||
`mlx-lm` supports thousands of LLMs available on the Hugging Face Hub. If the
|
||||
model you want to run is not supported, file an
|
||||
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet, submit
|
||||
a pull request. Many supported models are available in various quantization
|
||||
formats in the [MLX Community](https://huggingface.co/mlx-community) Hugging
|
||||
Face organization.
|
||||
|
||||
Here are a few examples of Hugging Face models that work with this example:
|
||||
For some models the tokenizer may require you to enable the `trust_remote_code`
|
||||
option. You can do this by passing `--trust-remote-code` in the command line.
|
||||
If you don't specify the flag explicitly, you will be prompted to trust remote
|
||||
code in the terminal when running the model.
|
||||
|
||||
- [mistralai/Mistral-7B-v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1)
|
||||
- [meta-llama/Llama-2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf)
|
||||
- [deepseek-ai/deepseek-coder-6.7b-instruct](https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-instruct)
|
||||
- [01-ai/Yi-6B-Chat](https://huggingface.co/01-ai/Yi-6B-Chat)
|
||||
- [microsoft/phi-2](https://huggingface.co/microsoft/phi-2)
|
||||
- [mistralai/Mixtral-8x7B-Instruct-v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1)
|
||||
- [Qwen/Qwen-7B](https://huggingface.co/Qwen/Qwen-7B)
|
||||
- [pfnet/plamo-13b](https://huggingface.co/pfnet/plamo-13b)
|
||||
- [pfnet/plamo-13b-instruct](https://huggingface.co/pfnet/plamo-13b-instruct)
|
||||
- [stabilityai/stablelm-2-zephyr-1_6b](https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b)
|
||||
- [internlm/internlm2-7b](https://huggingface.co/internlm/internlm2-7b)
|
||||
- [tiiuae/falcon-mamba-7b-instruct](https://huggingface.co/tiiuae/falcon-mamba-7b-instruct)
|
||||
|
||||
Most
|
||||
[Mistral](https://huggingface.co/models?library=transformers,safetensors&other=mistral&sort=trending),
|
||||
[Llama](https://huggingface.co/models?library=transformers,safetensors&other=llama&sort=trending),
|
||||
[Phi-2](https://huggingface.co/models?library=transformers,safetensors&other=phi&sort=trending),
|
||||
and
|
||||
[Mixtral](https://huggingface.co/models?library=transformers,safetensors&other=mixtral&sort=trending)
|
||||
style models should work out of the box.
|
||||
|
||||
For some models (such as `Qwen` and `plamo`) the tokenizer requires you to
|
||||
enable the `trust_remote_code` option. You can do this by passing
|
||||
`--trust-remote-code` in the command line. If you don't specify the flag
|
||||
explicitly, you will be prompted to trust remote code in the terminal when
|
||||
running the model.
|
||||
|
||||
For `Qwen` models you must also specify the `eos_token`. You can do this by
|
||||
passing `--eos-token "<|endoftext|>"` in the command
|
||||
line.
|
||||
|
||||
These options can also be set in the Python API. For example:
|
||||
Tokenizer options can also be set in the Python API. For example:
|
||||
|
||||
```python
|
||||
model, tokenizer = load(
|
||||
|
||||
@@ -9,3 +9,12 @@ os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
|
||||
from .convert import convert
|
||||
from .generate import batch_generate, generate, stream_generate
|
||||
from .utils import load
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"convert",
|
||||
"batch_generate",
|
||||
"generate",
|
||||
"stream_generate",
|
||||
"load",
|
||||
]
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.28.3"
|
||||
__version__ = "0.29.0"
|
||||
|
||||
+20
-10
@@ -6,6 +6,7 @@ import mlx.core as mx
|
||||
|
||||
from mlx_lm import batch_generate, load, stream_generate
|
||||
from mlx_lm.generate import DEFAULT_MODEL
|
||||
from mlx_lm.utils import pipeline_load
|
||||
|
||||
|
||||
def setup_arg_parser():
|
||||
@@ -56,11 +57,21 @@ def main():
|
||||
args = parser.parse_args()
|
||||
mx.random.seed(0)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
)
|
||||
if group.size() > 1:
|
||||
model, tokenizer, config = pipeline_load(args.model, return_config=True)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
tokenizer._eos_token_ids = {}
|
||||
@@ -68,9 +79,8 @@ def main():
|
||||
prompt_tokens = args.prompt_tokens
|
||||
generation_tokens = args.generation_tokens
|
||||
batch_size = args.batch_size
|
||||
prompts = mx.random.randint(
|
||||
0, config["vocab_size"], (batch_size, prompt_tokens)
|
||||
).tolist()
|
||||
vocab_size = config.get("vocab_size") or config["text_config"]["vocab_size"]
|
||||
prompts = mx.random.randint(0, vocab_size, (batch_size, prompt_tokens)).tolist()
|
||||
prompt = prompts[0]
|
||||
|
||||
def single_bench():
|
||||
@@ -90,18 +100,18 @@ def main():
|
||||
else:
|
||||
_bench = batch_bench
|
||||
|
||||
print("Running warmup..")
|
||||
rprint("Running warmup..")
|
||||
_bench()
|
||||
|
||||
report_keys = ["prompt_tps", "generation_tps", "peak_memory"]
|
||||
print(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
|
||||
rprint(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
|
||||
responses = []
|
||||
for i in range(args.num_trials):
|
||||
response = _bench()
|
||||
responses.append(response)
|
||||
results = [(k, getattr(response, k)) for k in report_keys]
|
||||
results = [f"{k}={v:.3f}" for k, v in results]
|
||||
print(f"Trial {i+1}: " + ", ".join(results))
|
||||
rprint(f"Trial {i+1}: " + ", ".join(results))
|
||||
|
||||
def avg(k):
|
||||
vals = (getattr(response, k) for response in responses)
|
||||
@@ -109,7 +119,7 @@ def main():
|
||||
|
||||
results = [(k, avg(k)) for k in report_keys]
|
||||
results = [f"{k}={v:.3f}" for k, v in results]
|
||||
print(f"Averages: " + ", ".join(results))
|
||||
rprint(f"Averages: " + ", ".join(results))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+37
-11
@@ -12,7 +12,7 @@ import logging
|
||||
import os
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import lm_eval
|
||||
import mlx.core as mx
|
||||
@@ -25,7 +25,10 @@ from tqdm import tqdm
|
||||
|
||||
from .generate import batch_generate
|
||||
from .models.cache import make_prompt_cache
|
||||
from .utils import common_prefix_len, load
|
||||
from .sample_utils import make_sampler
|
||||
from .utils import load
|
||||
|
||||
DEFAULT_MAX_TOKENS = 8192
|
||||
|
||||
|
||||
def _rstrip_until(s, untils):
|
||||
@@ -36,6 +39,13 @@ def _rstrip_until(s, untils):
|
||||
return s[: min(f)]
|
||||
|
||||
|
||||
def _lstrip(s, pattern):
|
||||
"""Truncate the prefix of the string after the first occurrence of pattern."""
|
||||
if (idx := s.find(pattern)) != -1:
|
||||
return s[idx + len(pattern) :]
|
||||
return s
|
||||
|
||||
|
||||
def _pad_inputs(inputs):
|
||||
lengths = np.array([len(x) for x in inputs])
|
||||
maxlen = lengths.max()
|
||||
@@ -68,9 +78,10 @@ class MLXLM(LM):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_hf_repo: str,
|
||||
max_tokens: int,
|
||||
max_tokens: Optional[int] = None,
|
||||
use_chat_template: Optional[bool] = None,
|
||||
trust_remote_code: bool = False,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
tokenizer_config = {"trust_remote_code": True if trust_remote_code else None}
|
||||
@@ -82,6 +93,7 @@ class MLXLM(LM):
|
||||
self.use_chat_template = use_chat_template
|
||||
if use_chat_template is None:
|
||||
self.use_chat_template = self.tokenizer.chat_template is not None
|
||||
self._sampler = sampler
|
||||
|
||||
def _process_prompt(self, prompt, step_size: int = 2048):
|
||||
prompt = mx.array(prompt)[None]
|
||||
@@ -182,7 +194,8 @@ class MLXLM(LM):
|
||||
max_completed_l = max(len(s) for s in full_sequences)
|
||||
|
||||
# compute truncation length
|
||||
truncation = max(0, max_completed_l - self._max_tokens - 1)
|
||||
max_tokens = self._max_tokens or DEFAULT_MAX_TOKENS
|
||||
truncation = max(0, max_completed_l - max_tokens - 1)
|
||||
orig_prefix_l = len(prefix)
|
||||
prefix_l = max(len(prefix) - truncation, 0)
|
||||
prefix = prefix[len(prefix) - prefix_l :]
|
||||
@@ -324,7 +337,10 @@ class MLXLM(LM):
|
||||
]
|
||||
|
||||
# TODO consider multi-token, per-prompt stop conditions
|
||||
max_tokens = [opt.get("max_gen_toks", self._max_tokens) for opt in options]
|
||||
max_tokens = [
|
||||
self._max_tokens or opt.get("max_gen_tokens", DEFAULT_MAX_TOKENS)
|
||||
for opt in options
|
||||
]
|
||||
|
||||
completions = batch_generate(
|
||||
model=self._model,
|
||||
@@ -332,12 +348,13 @@ class MLXLM(LM):
|
||||
prompts=contexts,
|
||||
max_tokens=max_tokens,
|
||||
verbose=True,
|
||||
sampler=self._sampler,
|
||||
).texts
|
||||
|
||||
for e, (text, opt) in enumerate(zip(completions, options)):
|
||||
until = opt["until"]
|
||||
if any(u in text for u in until):
|
||||
completions[e] = _rstrip_until(text, until)
|
||||
completions[e] = _rstrip_until(text, opt["until"])
|
||||
if self.tokenizer.has_thinking:
|
||||
completions[e] = _lstrip(text, self.tokenizer.think_end)
|
||||
|
||||
# Gather the completions
|
||||
if group.size() > 1:
|
||||
@@ -388,8 +405,9 @@ def main():
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
help="Maximum number of tokens to generate.",
|
||||
default=8912,
|
||||
help="Maximum number of tokens to generate. When set, this value takes"
|
||||
" precedence over task specific defaults.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
@@ -431,7 +449,9 @@ def main():
|
||||
action="store_true",
|
||||
help="Enable trusting remote code for tokenizer",
|
||||
)
|
||||
|
||||
parser.add_argument("--temp", type=float, default=0.0, help="Sampling temperature")
|
||||
parser.add_argument("--top-p", type=float, default=1.0, help="Sampling top-p")
|
||||
parser.add_argument("--top-k", type=int, default=0, help="Sampling top-k")
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
@@ -448,11 +468,17 @@ def main():
|
||||
if world.size() > 1 and world.rank() == 0:
|
||||
print(f"Evaluating with {world.size()} nodes")
|
||||
|
||||
sampler = make_sampler(
|
||||
temp=args.temp,
|
||||
top_p=args.top_p,
|
||||
top_k=args.top_k,
|
||||
)
|
||||
lm = MLXLM(
|
||||
args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
use_chat_template=args.apply_chat_template,
|
||||
trust_remote_code=args.trust_remote_code,
|
||||
sampler=sampler,
|
||||
)
|
||||
MLXLM.apply_chat_template = chat_template_fn(**args.chat_template_args)
|
||||
|
||||
|
||||
@@ -26,7 +26,26 @@ prompts = [
|
||||
]
|
||||
|
||||
# Set `verbose=True` to see generation statistics
|
||||
result = batch_generate(model, tokenizer, prompts, verbose=False, max_tokens=128)
|
||||
result = batch_generate(
|
||||
model, tokenizer, prompts, verbose=False, return_prompt_caches=True
|
||||
)
|
||||
print(result.texts[-1])
|
||||
|
||||
# The returned result contains texts completions in the same order as prompts
|
||||
print(result.texts[0])
|
||||
prompts = [
|
||||
"Could you summarize that?",
|
||||
"And what about the sea?",
|
||||
"Try again?",
|
||||
"And Mt Olympus?",
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts
|
||||
]
|
||||
|
||||
result = batch_generate(
|
||||
model, tokenizer, prompts, verbose=False, prompt_caches=result.caches
|
||||
)
|
||||
print(result.texts[-1])
|
||||
|
||||
@@ -17,71 +17,11 @@ https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import resource
|
||||
from pathlib import Path
|
||||
|
||||
import mlx.core as mx
|
||||
from huggingface_hub import snapshot_download
|
||||
from mlx.utils import tree_flatten
|
||||
|
||||
from mlx_lm import load, stream_generate
|
||||
from mlx_lm.utils import load_model, load_tokenizer
|
||||
|
||||
# Needed for 8 bit model
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
|
||||
|
||||
|
||||
def download(repo: str, allow_patterns: list[str]) -> Path:
|
||||
return Path(
|
||||
snapshot_download(
|
||||
repo,
|
||||
allow_patterns=allow_patterns,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def shard_and_load(repo):
|
||||
# Get model path with everything but weight safetensors
|
||||
model_path = download(
|
||||
args.model,
|
||||
allow_patterns=["*.json", "*.py", "tokenizer.model", "*.tiktoken", "*.txt"],
|
||||
)
|
||||
|
||||
# Lazy load and shard model to figure out
|
||||
# which weights we need
|
||||
model, config = load_model(model_path, lazy=True, strict=False)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
model.model.pipeline(group)
|
||||
|
||||
# Figure out which files we need for the local shard
|
||||
with open(model_path / "model.safetensors.index.json", "r") as fid:
|
||||
weight_index = json.load(fid)["weight_map"]
|
||||
|
||||
local_files = set()
|
||||
for k, _ in tree_flatten(model.parameters()):
|
||||
local_files.add(weight_index[k])
|
||||
|
||||
# Download weights for local shard
|
||||
download(args.model, allow_patterns=local_files)
|
||||
|
||||
# Load and shard the model, and load the weights
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
{"trust_remote_code": True},
|
||||
eos_token_ids=config.get("eos_token_id", None),
|
||||
)
|
||||
model, _ = load_model(model_path, lazy=True, strict=False)
|
||||
model.model.pipeline(group)
|
||||
mx.eval(model.parameters())
|
||||
|
||||
# Synchronize processes before generation to avoid timeout if downloading
|
||||
# model for the first time.
|
||||
mx.eval(mx.distributed.all_sum(mx.array(1.0), stream=mx.cpu))
|
||||
return model, tokenizer
|
||||
|
||||
from mlx_lm import stream_generate
|
||||
from mlx_lm.utils import pipeline_load
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
|
||||
@@ -112,7 +52,7 @@ if __name__ == "__main__":
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model, tokenizer = shard_and_load(args.model)
|
||||
model, tokenizer = pipeline_load(args.model)
|
||||
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
|
||||
+6
-6
@@ -4,8 +4,8 @@ from pathlib import Path
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from .gguf import convert_to_gguf
|
||||
from .tuner.utils import dequantize, load_adapters
|
||||
from .utils import (
|
||||
dequantize_model,
|
||||
load,
|
||||
save,
|
||||
upload_to_hub,
|
||||
@@ -39,8 +39,8 @@ def parse_arguments() -> argparse.Namespace:
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--de-quantize",
|
||||
help="Generate a de-quantized model.",
|
||||
"--dequantize",
|
||||
help="Generate a dequantized model.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -66,7 +66,7 @@ def main() -> None:
|
||||
)
|
||||
|
||||
fused_linears = [
|
||||
(n, m.fuse(de_quantize=args.de_quantize))
|
||||
(n, m.fuse(dequantize=args.dequantize))
|
||||
for n, m in model.named_modules()
|
||||
if hasattr(m, "fuse")
|
||||
]
|
||||
@@ -74,8 +74,8 @@ def main() -> None:
|
||||
if fused_linears:
|
||||
model.update_modules(tree_unflatten(fused_linears))
|
||||
|
||||
if args.de_quantize:
|
||||
print("De-quantizing model")
|
||||
if args.dequantize:
|
||||
print("Dequantizing model")
|
||||
model = dequantize(model)
|
||||
config.pop("quantization", None)
|
||||
|
||||
|
||||
+165
-30
@@ -7,6 +7,7 @@ import json
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -307,7 +308,7 @@ def generate_step(
|
||||
kv_bits: Optional[int] = None,
|
||||
kv_group_size: int = 64,
|
||||
quantized_kv_start: int = 0,
|
||||
prompt_progress_callback: Optional[Callable[[int], int]] = None,
|
||||
prompt_progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> Generator[Tuple[mx.array, mx.array], None, None]:
|
||||
"""
|
||||
@@ -333,7 +334,7 @@ def generate_step(
|
||||
kv_group_size (int): Group size for KV cache quantization. Default: ``64``.
|
||||
quantized_kv_start (int): Step to begin using a quantized KV cache.
|
||||
when ``kv_bits`` is non-None. Default: ``0``.
|
||||
prompt_progress_callback (Callable[[int], int]): A call-back which takes the
|
||||
prompt_progress_callback (Callable[[int, int], None]): A call-back which takes the
|
||||
prompt tokens processed so far and the total number of prompt tokens.
|
||||
input_embeddings (mx.array, optional): Input embeddings to use instead of or in
|
||||
conjunction with prompt tokens. Default: ``None``.
|
||||
@@ -418,7 +419,8 @@ def generate_step(
|
||||
prompt_processed_tokens = 0
|
||||
prompt_progress_callback(prompt_processed_tokens, total_prompt_tokens)
|
||||
while total_prompt_tokens - prompt_processed_tokens > 1:
|
||||
n_to_process = min(prefill_step_size, prompt.size - 1)
|
||||
remaining = (total_prompt_tokens - prompt_processed_tokens) - 1
|
||||
n_to_process = min(prefill_step_size, remaining)
|
||||
_model_call(
|
||||
input_tokens=prompt[:n_to_process][None],
|
||||
input_embeddings=(
|
||||
@@ -785,6 +787,12 @@ def _left_pad_prompts(prompts, max_length=None):
|
||||
return mx.array([[0] * (max_length - len(p)) + p for p in prompts])
|
||||
|
||||
|
||||
def _right_pad_prompts(prompts, max_length=None):
|
||||
if max_length is None:
|
||||
max_length = max(len(p) for p in prompts)
|
||||
return mx.array([p + [0] * (max_length - len(p)) for p in prompts])
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchStats:
|
||||
"""
|
||||
@@ -821,6 +829,7 @@ class BatchResponse:
|
||||
|
||||
texts: List[str]
|
||||
stats: BatchStats
|
||||
caches: Optional[List[List[Any]]]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -837,23 +846,26 @@ class Batch:
|
||||
|
||||
def filter(self, keep_idx: List[int]):
|
||||
self.uids = [self.uids[k] for k in keep_idx]
|
||||
self.logprobs = [self.logprobs[k] for k in keep_idx]
|
||||
self.max_tokens = [self.max_tokens[k] for k in keep_idx]
|
||||
self.num_tokens = [self.num_tokens[k] for k in keep_idx]
|
||||
keep_idx = mx.array(keep_idx, mx.int32)
|
||||
self.y = self.y[keep_idx]
|
||||
self.logprobs = self.logprobs[keep_idx]
|
||||
for c in self.cache:
|
||||
c.filter(keep_idx)
|
||||
|
||||
def extend(self, other):
|
||||
self.uids.extend(other.uids)
|
||||
self.y = mx.concatenate([self.y, other.y])
|
||||
self.logprobs = mx.concatenate([self.logprobs, other.logprobs])
|
||||
self.logprobs.extend(other.logprobs)
|
||||
self.num_tokens.extend(other.num_tokens)
|
||||
self.max_tokens.extend(other.max_tokens)
|
||||
for c, o in zip(self.cache, other.cache):
|
||||
c.extend(o)
|
||||
|
||||
def extract_cache(self, idx):
|
||||
return [c.extract(idx) for c in self.cache]
|
||||
|
||||
|
||||
def _make_cache(model, left_padding):
|
||||
"""
|
||||
@@ -883,6 +895,22 @@ def _make_cache(model, left_padding):
|
||||
return [BatchKVCache(left_padding) for _ in model.layers]
|
||||
|
||||
|
||||
def _merge_caches(caches):
|
||||
batch_cache = []
|
||||
for i in range(len(caches[0])):
|
||||
cache = None
|
||||
if isinstance(caches[0][i], KVCache):
|
||||
cache = BatchKVCache.merge([c[i] for c in caches])
|
||||
elif isinstance(caches[0][i], RotatingKVCache):
|
||||
cache = BatchRotatingKVCache.merge([c[i] for c in caches])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{type(caches[0][i])} does not yet support batching with history"
|
||||
)
|
||||
batch_cache.append(cache)
|
||||
return batch_cache
|
||||
|
||||
|
||||
class BatchGenerator:
|
||||
|
||||
@dataclass
|
||||
@@ -891,6 +919,7 @@ class BatchGenerator:
|
||||
token: int
|
||||
logprobs: mx.array
|
||||
finish_reason: Optional[str]
|
||||
prompt_cache: Callable[[], List[Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -901,6 +930,9 @@ class BatchGenerator:
|
||||
completion_batch_size: int = 32,
|
||||
prefill_batch_size: int = 8,
|
||||
prefill_step_size: int = 2048,
|
||||
prompt_progress_callback: Optional[
|
||||
Callable[[List[Tuple[int, int, int]]], None]
|
||||
] = None,
|
||||
):
|
||||
self.model = model
|
||||
self.unprocessed_prompts = []
|
||||
@@ -910,44 +942,132 @@ class BatchGenerator:
|
||||
self.uid_count = 0
|
||||
self.prefill_step_size = prefill_step_size
|
||||
self.prefill_batch_size = prefill_batch_size
|
||||
self.completion_batch_size = completion_batch_size
|
||||
self.completion_batch_size = max(completion_batch_size, prefill_batch_size)
|
||||
self.prompt_progress_callback = prompt_progress_callback or (lambda *_: None)
|
||||
self._stats = BatchStats()
|
||||
|
||||
self.active_batch = None
|
||||
|
||||
def insert(self, prompts, max_tokens: Union[List[int], int, None] = None):
|
||||
if mx.metal.is_available():
|
||||
self._old_wired_limit = mx.set_wired_limit(
|
||||
mx.metal.device_info()["max_recommended_working_set_size"]
|
||||
)
|
||||
else:
|
||||
self._old_wired_limit = None
|
||||
|
||||
def close(self):
|
||||
if self._old_wired_limit is not None:
|
||||
mx.synchronize(generation_stream)
|
||||
mx.set_wired_limit(self._old_wired_limit)
|
||||
self._old_wired_limit = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def insert(
|
||||
self, prompts, max_tokens: Union[List[int], int, None] = None, caches=None
|
||||
):
|
||||
uids = []
|
||||
|
||||
if max_tokens is None or isinstance(max_tokens, int):
|
||||
max_tokens = [max_tokens or self.max_tokens] * len(prompts)
|
||||
|
||||
for p, m in zip(prompts, max_tokens):
|
||||
self.unprocessed_prompts.append((self.uid_count, p, m))
|
||||
if caches is None:
|
||||
caches = [None] * len(prompts)
|
||||
for i in range(len(prompts)):
|
||||
if caches[i] is None:
|
||||
caches[i] = cache.make_prompt_cache(self.model)
|
||||
|
||||
for p, m, c in zip(prompts, max_tokens, caches):
|
||||
self.unprocessed_prompts.append((self.uid_count, p, m, c))
|
||||
uids.append(self.uid_count)
|
||||
self.uid_count += 1
|
||||
# Sort in ascending order of length
|
||||
self.unprocessed_prompts = sorted(
|
||||
self.unprocessed_prompts, key=lambda x: len(x[1])
|
||||
self.unprocessed_prompts, key=lambda x: len(x[1]) + cache.cache_length(x[3])
|
||||
)
|
||||
return uids
|
||||
|
||||
def remove(self, uids: List[int]):
|
||||
uids = set(uids)
|
||||
if self.active_batch is not None:
|
||||
batch = self.active_batch
|
||||
keep_idx = [e for e, uid in enumerate(batch.uids) if uid not in uids]
|
||||
if len(keep_idx) > 0:
|
||||
batch.filter(keep_idx)
|
||||
else:
|
||||
self.active_batch = None
|
||||
|
||||
for i in reversed(range(len(self.unprocessed_prompts))):
|
||||
if self.unprocessed_prompts[i][0] in uids:
|
||||
self.unprocessed_prompts.pop(i)
|
||||
|
||||
def _process_prompts(self, prompts):
|
||||
uids, inputs, max_tokens = zip(*prompts)
|
||||
uids, inputs, max_tokens, caches = zip(*prompts)
|
||||
|
||||
cache_lengths = [cache.cache_length(c) for c in caches]
|
||||
max_cache_length = max(cache_lengths)
|
||||
lengths = [len(p) for p in inputs]
|
||||
max_length = max(lengths)
|
||||
batch_size = self.prefill_batch_size
|
||||
padding = [max_length - l for l in lengths]
|
||||
|
||||
self._stats.prompt_tokens += sum(lengths)
|
||||
left_padding = [max_length - l for l in lengths]
|
||||
inputs = _left_pad_prompts(inputs, max_length=max_length)
|
||||
|
||||
prompt_cache = _make_cache(self.model, left_padding)
|
||||
processed_tokens = 0
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
self.model(inputs[:, :n_to_process], cache=prompt_cache)
|
||||
# New prompts so
|
||||
# 1. Left-pad the inputs
|
||||
# 2. Process
|
||||
if max_cache_length == 0:
|
||||
inputs = _left_pad_prompts(inputs, max_length=max_length)
|
||||
prompt_cache = _make_cache(self.model, padding)
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
self.model(inputs[:, :n_to_process], cache=prompt_cache)
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
inputs = inputs[:, n_to_process:]
|
||||
processed_tokens += n_to_process
|
||||
self.prompt_progress_callback(
|
||||
[
|
||||
(uid, processed_tokens, length)
|
||||
for uid, length in zip(uids, lengths)
|
||||
]
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
# Further prompt processing so we need to
|
||||
# 1. Merge the KV caches and prepare for right padded prompts
|
||||
# 2. Right pad the inputs
|
||||
# 2. Process
|
||||
# 3. Finalize the KV caches so they are left padded again
|
||||
else:
|
||||
last_inputs = mx.array([p[-1:] for p in inputs])
|
||||
inputs = _right_pad_prompts(inputs, max_length=max_length)
|
||||
prompt_cache = _merge_caches(caches)
|
||||
|
||||
for c in prompt_cache:
|
||||
c.prepare(lengths=lengths, right_padding=padding)
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
self.model(inputs[:, :n_to_process], cache=prompt_cache)
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
inputs = inputs[:, n_to_process:]
|
||||
processed_tokens += n_to_process
|
||||
self.prompt_progress_callback(
|
||||
[
|
||||
(uid, processed_tokens, length)
|
||||
for uid, length in zip(uids, lengths)
|
||||
]
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
for c in prompt_cache:
|
||||
c.finalize()
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
inputs = inputs[:, n_to_process:]
|
||||
mx.clear_cache()
|
||||
inputs = last_inputs
|
||||
|
||||
y, logprobs = self._step(inputs, prompt_cache)
|
||||
mx.async_eval(y, logprobs)
|
||||
@@ -960,7 +1080,7 @@ class BatchGenerator:
|
||||
logits = logits[:, -1, :]
|
||||
logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
|
||||
sampled = self.sampler(logprobs)
|
||||
return sampled, logprobs
|
||||
return sampled, list(logprobs)
|
||||
|
||||
def stats(self):
|
||||
self._stats.prompt_tps = self._stats.prompt_tokens / self._stats.prompt_time
|
||||
@@ -1025,6 +1145,7 @@ class BatchGenerator:
|
||||
for e, (t, uid, num_tok, max_tok) in enumerate(
|
||||
zip(y, batch.uids, batch.num_tokens, batch.max_tokens)
|
||||
):
|
||||
cache = None
|
||||
num_tok += 1
|
||||
batch.num_tokens[e] = num_tok
|
||||
if t in self.stop_tokens:
|
||||
@@ -1036,7 +1157,9 @@ class BatchGenerator:
|
||||
else:
|
||||
finish_reason = None
|
||||
keep_idx.append(e)
|
||||
responses.append(self.Response(uid, t, logprobs[e], finish_reason))
|
||||
if finish_reason is not None:
|
||||
cache = batch.extract_cache(e)
|
||||
responses.append(self.Response(uid, t, logprobs[e], finish_reason, cache))
|
||||
|
||||
# Remove any finished completions
|
||||
if len(end_idx):
|
||||
@@ -1057,8 +1180,10 @@ def batch_generate(
|
||||
model,
|
||||
tokenizer,
|
||||
prompts: List[int],
|
||||
prompt_caches: Optional[List[List[Any]]] = None,
|
||||
max_tokens: Union[int, List[int]] = 128,
|
||||
verbose: bool = False,
|
||||
return_prompt_caches: bool = False,
|
||||
**kwargs,
|
||||
) -> BatchResponse:
|
||||
"""
|
||||
@@ -1068,10 +1193,15 @@ def batch_generate(
|
||||
model (nn.Module): The language model.
|
||||
tokenizer (PreTrainedTokenizer): The tokenizer.
|
||||
prompt (List[List[int]]): The input prompts.
|
||||
prompt_caches (List[List[Any]], optional): Pre-computed prompt-caches
|
||||
for each input prompt. Note, unlike ``generate_step``, the caches
|
||||
won't be updated in-place.
|
||||
verbose (bool): If ``True``, print tokens and timing information.
|
||||
Default: ``False``.
|
||||
max_tokens (Union[int, List[int]): Maximum number of output tokens. This
|
||||
can be per prompt if a list is provided.
|
||||
return_prompt_caches (bool): Return the prompt caches in the batch
|
||||
responses. Default: ``False``.
|
||||
kwargs: The remaining options get passed to :obj:`BatchGenerator`.
|
||||
See :obj:`BatchGenerator` for more details.
|
||||
"""
|
||||
@@ -1082,25 +1212,30 @@ def batch_generate(
|
||||
if verbose:
|
||||
print(f"[batch_generate] Finished processing 0/{num_samples} ...", end="\r")
|
||||
|
||||
with wired_limit(model, [generation_stream]):
|
||||
uids = gen.insert(prompts, max_tokens)
|
||||
results = {uid: [] for uid in uids}
|
||||
while responses := gen.next():
|
||||
for r in responses:
|
||||
if verbose and r.finish_reason != None:
|
||||
uids = gen.insert(prompts, max_tokens, caches=prompt_caches)
|
||||
results = {uid: [] for uid in uids}
|
||||
prompt_caches = {}
|
||||
while responses := gen.next():
|
||||
for r in responses:
|
||||
if r.finish_reason is not None:
|
||||
if return_prompt_caches:
|
||||
prompt_caches[r.uid] = r.prompt_cache
|
||||
if verbose:
|
||||
fin += 1
|
||||
print(
|
||||
f"[batch_generate] Finished processing {fin}/{num_samples} ...",
|
||||
end="\r",
|
||||
)
|
||||
if r.finish_reason != "stop":
|
||||
results[r.uid].append(r.token)
|
||||
if r.finish_reason != "stop":
|
||||
results[r.uid].append(r.token)
|
||||
gen.close()
|
||||
if verbose:
|
||||
print(f"[batch_generate] Finished processing {fin}/{num_samples}")
|
||||
|
||||
# Return results in correct order
|
||||
texts = [tokenizer.decode(results[uid]) for uid in uids]
|
||||
stats = gen.stats()
|
||||
caches = [prompt_caches[uid] for uid in uids] if return_prompt_caches else None
|
||||
if verbose:
|
||||
print(
|
||||
f"[batch_generate] Prompt: {stats.prompt_tokens} tokens, {stats.prompt_tps:.3f} tokens-per-sec"
|
||||
@@ -1110,7 +1245,7 @@ def batch_generate(
|
||||
f"{stats.generation_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
print(f"[batch_generate] Peak memory: {stats.peak_memory:.3f} GB")
|
||||
return BatchResponse(texts, stats)
|
||||
return BatchResponse(texts, stats, caches)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -50,7 +50,7 @@ class FusedLoRALinear(nn.Module):
|
||||
]
|
||||
self.lora_b = [mx.zeros((r, od)) for od in output_dims]
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
linear = self.linear
|
||||
weight = linear.weight
|
||||
is_quantized = isinstance(linear, FusedQuantizedLinear)
|
||||
@@ -79,7 +79,7 @@ class FusedLoRALinear(nn.Module):
|
||||
delta = mx.concatenate(deltas, axis=0)
|
||||
fused_linear.weight = weight + delta
|
||||
|
||||
if is_quantized and not de_quantize:
|
||||
if is_quantized and not dequantize:
|
||||
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
|
||||
|
||||
return fused_linear
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
layer_types: List[str]
|
||||
vocab_size: int = 200192
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 6144
|
||||
moe_intermediate_size: int = 1024
|
||||
num_hidden_layers: int = 32
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 4
|
||||
head_dim: int = 64
|
||||
max_position_embeddings: int = 131072
|
||||
rms_norm_eps: float = 1e-5
|
||||
rope_theta: float = 10000
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
# MoE config
|
||||
num_experts: int = 128
|
||||
num_experts_per_tok: int = 8
|
||||
num_shared_experts: int = 1
|
||||
num_dense_layers: int = 2
|
||||
route_norm: bool = True
|
||||
route_scale: float = 2.826
|
||||
score_func: str = "sigmoid"
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
sliding_window: int = 2048
|
||||
mup_enabled: bool = True
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, is_local_attention: bool = False):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.is_local_attention = is_local_attention
|
||||
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
|
||||
if is_local_attention:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False, # traditional
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
else:
|
||||
self.rope = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
if self.is_local_attention and self.rope is not None:
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
gate = mx.sigmoid(self.gate_proj(x))
|
||||
output = output * gate
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class MoERouter(nn.Module):
|
||||
"""Router module that wraps the gate for proper weight naming."""
|
||||
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.gate(x)
|
||||
|
||||
|
||||
class AfmoeMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts = args.num_experts
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.route_norm = args.route_norm
|
||||
self.route_scale = args.route_scale
|
||||
self.score_func = args.score_func
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
|
||||
self.router = MoERouter(args)
|
||||
|
||||
self.expert_bias = mx.zeros((args.num_experts,))
|
||||
|
||||
self.experts = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
if args.num_shared_experts > 0:
|
||||
shared_intermediate_size = (
|
||||
args.moe_intermediate_size * args.num_shared_experts
|
||||
)
|
||||
self.shared_experts = MLP(args, intermediate_size=shared_intermediate_size)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
|
||||
if self.score_func == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
|
||||
# Add expert bias for selection
|
||||
selection_scores = scores + self.expert_bias
|
||||
|
||||
# Group-based expert selection if n_group > 1
|
||||
if self.n_group > 1:
|
||||
selection_scores = mx.unflatten(
|
||||
selection_scores, axis=-1, shape=(self.n_group, -1)
|
||||
)
|
||||
group_scores = mx.topk(selection_scores, 2, axis=-1).sum(
|
||||
axis=-1, keepdims=True
|
||||
)
|
||||
k = self.n_group - self.topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
selection_scores = mx.put_along_axis(
|
||||
selection_scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
selection_scores = mx.flatten(selection_scores, -2, -1)
|
||||
|
||||
# Select top-k experts
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.argpartition(-selection_scores, kth=k - 1, axis=-1)[..., :k]
|
||||
|
||||
selected_scores = mx.take_along_axis(scores, inds, axis=-1)
|
||||
|
||||
if self.route_norm and self.num_experts_per_tok > 1:
|
||||
denominator = selected_scores.sum(axis=-1, keepdims=True)
|
||||
selected_scores = selected_scores / denominator
|
||||
|
||||
selected_scores = selected_scores * self.route_scale
|
||||
|
||||
y = self.experts(x, inds)
|
||||
y = (y * selected_scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
if self.args.num_shared_experts > 0:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.self_attn = Attention(args, is_local_attention=use_sliding)
|
||||
|
||||
if layer_idx < args.num_dense_layers:
|
||||
self.mlp = MLP(args)
|
||||
else:
|
||||
self.mlp = AfmoeMoE(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.pre_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
r = self.post_attention_layernorm(r)
|
||||
h = x + r
|
||||
|
||||
r = self.mlp(self.pre_mlp_layernorm(h))
|
||||
r = self.post_mlp_layernorm(r)
|
||||
return h + r
|
||||
|
||||
|
||||
class AfmoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
self.mup_enabled = args.mup_enabled
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(
|
||||
args=args, layer_idx=idx, use_sliding=layer_type == "sliding_attention"
|
||||
)
|
||||
for idx, layer_type in enumerate(self.layer_types)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for idx, layer in enumerate(self.layers):
|
||||
if layer.use_sliding:
|
||||
self.swa_idx = idx
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if self.mup_enabled:
|
||||
h = h * math.sqrt(self.hidden_size)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
swa_mask = None
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = AfmoeModel(args)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove unused precomputed rotary freqs
|
||||
weights = {k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
# Stack experts weights for SwitchGLU
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if l < self.args.num_dense_layers:
|
||||
continue
|
||||
prefix = f"model.layers.{l}"
|
||||
for n in ["up_proj", "down_proj", "gate_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{n}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{n}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.experts.{n}.{k}"] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if "router.gate" in path:
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,594 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
max_position_embeddings: int
|
||||
moe_intermediate_size: int
|
||||
num_experts: int
|
||||
num_shared_experts: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_experts_per_tok: int
|
||||
num_hidden_layers: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
vocab_size: int
|
||||
first_k_dense_replace: int
|
||||
layer_group_size: int
|
||||
group_norm_size: int
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
rope_traditional: bool = False
|
||||
use_bias: bool = False
|
||||
use_qkv_bias: bool = False
|
||||
norm_head: bool = False
|
||||
norm_softmax: bool = False
|
||||
use_qk_norm: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
moe_router_enable_expert_bias: bool = False
|
||||
moe_router_enable_routed_scaling: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
score_function: str = "softmax"
|
||||
n_group: int = 1
|
||||
topk_group: int = 4
|
||||
use_rmsnorm: bool = True
|
||||
moe_shared_expert_intermediate_size: Optional[int] = None
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
head_dim: Optional[int] = None
|
||||
|
||||
|
||||
def recurrent_gla(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
g: mx.array,
|
||||
scale: float,
|
||||
h: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
"""
|
||||
Recurrence per (b, h):
|
||||
h_t = h_{t-1} * exp(g_t)
|
||||
h_t = h_t + k_t^T @ v_t
|
||||
y_t = (q_t @ h_t) * scale
|
||||
Returns y with shape [B, H, T, Dv].
|
||||
"""
|
||||
B, Hq, L, K = q.shape
|
||||
Hv = k.shape[1]
|
||||
V = v.shape[-1]
|
||||
|
||||
outputs = []
|
||||
exp_g = mx.exp(g)[:, None, None].astype(q.dtype)
|
||||
q = q * scale
|
||||
for t in range(L):
|
||||
q_t = q[:, :, t : t + 1]
|
||||
k_t = k[:, :, t : t + 1]
|
||||
v_t = v[:, :, t : t + 1]
|
||||
h_up = k_t.transpose(0, 1, 3, 2) @ v_t
|
||||
if h is not None:
|
||||
h = h * exp_g + h_up
|
||||
else:
|
||||
h = h_up
|
||||
o_t = q_t @ h
|
||||
outputs.append(o_t)
|
||||
|
||||
return mx.concatenate(outputs, axis=2), h
|
||||
|
||||
|
||||
class GroupRMSNorm(nn.Module):
|
||||
def __init__(self, dims: int, eps: float = 1e-5, groups: int = 1):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((dims,))
|
||||
self.groups = groups
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
shape = x.shape
|
||||
x = mx.unflatten(x, axis=-1, shape=(self.groups, -1))
|
||||
x = mx.fast.rms_norm(x, weight=None, eps=self.eps)
|
||||
return self.weight * mx.flatten(x, -2)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.intermediate_size = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
self.intermediate_size, args.hidden_size, bias=args.use_bias
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim or args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
|
||||
q_size = self.num_attention_heads * self.head_dim
|
||||
kv_size = self.num_key_value_heads * self.head_dim
|
||||
q, k, v = mx.split(qkv, [q_size, q_size + kv_size], axis=-1)
|
||||
|
||||
queries = q.reshape(B, L, self.num_attention_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = k.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = v.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
class LinearAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_attention_heads
|
||||
self.head_dim = args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.num_key_value_groups = self.num_attention_heads // self.num_key_value_heads
|
||||
assert self.num_key_value_groups == 1, "Grouped linear not yet supported."
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
self.g_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.g_norm = GroupRMSNorm(
|
||||
args.num_attention_heads * self.head_dim,
|
||||
eps=args.rms_norm_eps,
|
||||
groups=args.group_norm_size,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
self._slope = self._get_slopes()
|
||||
|
||||
def _get_slopes(self) -> mx.array:
|
||||
n = self.num_attention_heads
|
||||
|
||||
def power_of_2_slopes(n):
|
||||
return [2 ** (-(2 ** -(math.log2(n) - 3)) * (i + 1)) for i in range(n)]
|
||||
|
||||
if math.log2(n).is_integer():
|
||||
slopes = power_of_2_slopes(n)
|
||||
else:
|
||||
p = 2 ** math.floor(math.log2(n))
|
||||
slopes = power_of_2_slopes(p) + power_of_2_slopes(2 * p)[::2][: n - p]
|
||||
|
||||
slopes = mx.array(slopes, dtype=mx.float32)
|
||||
denom = max(1, self.num_hidden_layers - 1)
|
||||
layer_pos = max(0, self.layer_idx - 1)
|
||||
layer_factor = 1 - (layer_pos / denom) + 1e-5
|
||||
return -slopes * layer_factor
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
qkv_mix = qkv.reshape(
|
||||
B,
|
||||
L,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads),
|
||||
self.head_dim,
|
||||
)
|
||||
q, k, v = mx.split(
|
||||
qkv_mix,
|
||||
[
|
||||
self.num_attention_heads,
|
||||
self.num_attention_heads + self.num_key_value_heads,
|
||||
],
|
||||
axis=2,
|
||||
)
|
||||
|
||||
queries = q.transpose(0, 2, 1, 3)
|
||||
keys = k.transpose(0, 2, 1, 3)
|
||||
values = v.transpose(0, 2, 1, 3)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
if cache is None:
|
||||
cache = [None]
|
||||
output, cache[0] = recurrent_gla(
|
||||
q=queries,
|
||||
k=keys,
|
||||
v=values,
|
||||
g=self._slope,
|
||||
scale=self.scale,
|
||||
h=cache[0],
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
output = self.g_norm(output) * mx.sigmoid(self.g_proj(x))
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
def group_expert_select(
|
||||
gates: mx.array,
|
||||
e_score_correction_bias: mx.array,
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
norm_topk_prob: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
in_type = gates.dtype
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
orig_scores = scores
|
||||
if e_score_correction_bias is not None:
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores.astype(in_type)
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.enable_routed_scaling = args.moe_router_enable_routed_scaling
|
||||
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.expert_bias = (
|
||||
mx.zeros((args.num_experts,))
|
||||
if args.moe_router_enable_expert_bias
|
||||
else None
|
||||
)
|
||||
self.score_function = args.score_function
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return group_expert_select(
|
||||
self.gate_proj(x),
|
||||
self.expert_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
self.score_function,
|
||||
)
|
||||
|
||||
|
||||
class SparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
self.gate = Gate(args)
|
||||
shared_dim = (
|
||||
args.moe_shared_expert_intermediate_size or args.moe_intermediate_size
|
||||
)
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args=args,
|
||||
intermediate_size=shared_dim * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts > 0 and args.moe_router_enable_shared_expert
|
||||
else None
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = (out * topk_weight[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_global = (
|
||||
(layer_idx + 1) % args.layer_group_size == 0
|
||||
or layer_idx
|
||||
>= args.num_hidden_layers // args.layer_group_size * args.layer_group_size
|
||||
)
|
||||
|
||||
if self.is_global:
|
||||
self.attention = Attention(args)
|
||||
else:
|
||||
self.attention = LinearAttention(args, layer_idx=layer_idx)
|
||||
|
||||
self.mlp = (
|
||||
SparseMoeBlock(args)
|
||||
if (
|
||||
args.num_experts is not None and layer_idx >= args.first_k_dense_replace
|
||||
)
|
||||
else MLP(args)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
if self.is_global:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache, offset=offset)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gla_idx = 0
|
||||
self.attn_idx = args.layer_group_size - 1
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.word_embeddings(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
offset = 0
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
gla_mask = create_ssm_mask(h, cache[self.gla_idx])
|
||||
if cache[self.attn_idx] is not None:
|
||||
offset = cache[self.attn_idx].offset
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_global else gla_mask
|
||||
h = layer(h, mask, c, offset=offset)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.norm_head = args.norm_head
|
||||
self.model_type = args.model_type
|
||||
self.model = LanguageModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.word_embeddings.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if self.norm_head:
|
||||
w = weights["lm_head.weight"]
|
||||
dtype = w.dtype
|
||||
weight_norm = (
|
||||
mx.linalg.norm(w.astype(mx.float32), axis=0, keepdims=True) + 1e-7
|
||||
)
|
||||
weights["lm_head.weight"] = (w / weight_norm).astype(dtype)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Handle MoE layers
|
||||
if l >= self.args.first_k_dense_replace:
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(
|
||||
to_join
|
||||
)
|
||||
|
||||
if f"{prefix}.mlp.gate.weight" in weights:
|
||||
gate_weight = weights.pop(f"{prefix}.mlp.gate.weight")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.weight"] = gate_weight
|
||||
|
||||
if f"{prefix}.mlp.gate.bias" in weights:
|
||||
gate_bias = weights.pop(f"{prefix}.mlp.gate.bias")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.bias"] = gate_bias
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate.gate_proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for l in self.layers:
|
||||
if l.is_global:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(ArraysCache(size=1))
|
||||
return caches
|
||||
@@ -109,6 +109,10 @@ def trim_prompt_cache(cache: List[Any], num_tokens: int) -> List[Any]:
|
||||
return [c.trim(num_tokens) for c in cache][0]
|
||||
|
||||
|
||||
def cache_length(cache: List[Any]):
|
||||
return max(len(c) for c in cache)
|
||||
|
||||
|
||||
def create_attention_mask(
|
||||
N: int, offset: int, return_array: bool, window_size: Optional[int]
|
||||
):
|
||||
@@ -142,6 +146,24 @@ class _BaseCache:
|
||||
def is_trimmable(self):
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
"""The length of a cache is meant to represent the number of elements
|
||||
that we need to process in the attention. For instance for KVCache it
|
||||
is the size of the state, for RotatingKVCache it would be up to
|
||||
max_size etc."""
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
"""When an object defines __len__ then python defines the bool operator
|
||||
as len(obj) != 0. This, for instance, doesn't allow us to write
|
||||
|
||||
cache = cache or make_cache()
|
||||
|
||||
which is why we are overriding that behaviour with a constant bool
|
||||
operator return True.
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state, meta_state):
|
||||
# Create an instance of cls without calling __init__
|
||||
@@ -314,6 +336,9 @@ class KVCache(_BaseCache):
|
||||
self.values[..., prev : self.offset, :] = values
|
||||
return self.keys[..., : self.offset, :], self.values[..., : self.offset, :]
|
||||
|
||||
def __len__(self):
|
||||
return self.offset
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.offset == self.keys.shape[2]:
|
||||
@@ -458,6 +483,9 @@ class RotatingKVCache(_BaseCache):
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
return min(self.offset, self.max_size)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.offset < self.keys.shape[2]:
|
||||
@@ -659,6 +687,15 @@ class CacheList(_BaseCache):
|
||||
c.extend(o)
|
||||
|
||||
|
||||
def dynamic_roll(x, shifts, axis):
|
||||
n = x.shape[axis]
|
||||
expand_shifts = (...,) + (None,) * (x.ndim - axis)
|
||||
expand_indices = expand_shifts[:-1]
|
||||
idx = (mx.arange(n)[expand_indices] - shifts[expand_shifts]) % n
|
||||
rolled = mx.take_along_axis(x, idx, axis=axis)
|
||||
return rolled
|
||||
|
||||
|
||||
class BatchKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
@@ -687,6 +724,8 @@ class BatchKVCache(_BaseCache):
|
||||
self.offset = mx.array([-l for l in left_padding])
|
||||
self._idx = 0
|
||||
|
||||
self._right_padding = None
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
prev = self._idx
|
||||
if self.keys is None or (prev + keys.shape[2]) > self.keys.shape[2]:
|
||||
@@ -712,6 +751,31 @@ class BatchKVCache(_BaseCache):
|
||||
self.values[..., prev : self._idx, :] = values
|
||||
return self.keys[..., : self._idx, :], self.values[..., : self._idx, :]
|
||||
|
||||
def __len__(self):
|
||||
return self._idx
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
raise ValueError(
|
||||
"Left padding can only be added to an empty BatchKVCache"
|
||||
)
|
||||
left_padding = mx.array(left_padding)
|
||||
self.left_padding += left_padding
|
||||
self.offset -= left_padding
|
||||
|
||||
if right_padding is not None and max(right_padding) > 0:
|
||||
self._right_padding = mx.array(right_padding)
|
||||
|
||||
def finalize(self):
|
||||
if self._right_padding is not None:
|
||||
padding = self._right_padding
|
||||
self.keys = dynamic_roll(self.keys, padding[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, padding[:, None], axis=2)
|
||||
self.offset -= padding
|
||||
self.left_padding += padding
|
||||
self._right_padding = None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
k, v = self.keys, self.values
|
||||
@@ -785,6 +849,39 @@ class BatchKVCache(_BaseCache):
|
||||
)
|
||||
self._idx = max_idx
|
||||
|
||||
def extract(self, idx):
|
||||
cache = KVCache()
|
||||
padding = self.left_padding[idx].item()
|
||||
cache.keys = mx.contiguous(self.keys[idx : idx + 1, :, padding : self._idx])
|
||||
cache.values = mx.contiguous(self.values[idx : idx + 1, :, padding : self._idx])
|
||||
cache.offset = cache.keys.shape[2]
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
lengths = [len(c) for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
H = max(c.keys.shape[1] for c in caches if c.keys is not None)
|
||||
Dk = max(c.keys.shape[3] for c in caches if c.keys is not None)
|
||||
Dv = max(c.values.shape[3] for c in caches if c.values is not None)
|
||||
dt = next(iter(c.keys.dtype for c in caches if c.keys is not None))
|
||||
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
keys[i : i + 1, :, p : p + c.offset] = c.keys[..., : c.offset, :]
|
||||
values[i : i + 1, :, p : p + c.offset] = c.values[..., : c.offset, :]
|
||||
|
||||
cache = cls(padding)
|
||||
cache.keys = keys
|
||||
cache.values = values
|
||||
cache.offset += keys.shape[2]
|
||||
cache._idx = keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
class BatchRotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
@@ -801,6 +898,10 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
self._offset = 0
|
||||
self.rotated = False
|
||||
|
||||
# Lengths for right_padded inputs to make sure that padding tokens do
|
||||
# not evict valid tokens.
|
||||
self._lengths = None
|
||||
|
||||
def _trim(self, trim_size, v, append=None):
|
||||
if trim_size > 0:
|
||||
v = v[..., trim_size:, :]
|
||||
@@ -832,6 +933,15 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
self.keys = self.keys[..., : self._idx, :]
|
||||
self.values = self.values[..., : self._idx, :]
|
||||
|
||||
# Roll right sequences that are padded to make sure that we don't
|
||||
# trim valid cache entries
|
||||
if self._lengths is not None:
|
||||
roll = mx.maximum(0, self.offset - self._lengths)
|
||||
self.keys = dynamic_roll(self.keys, roll[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, roll[:, None], axis=2)
|
||||
self.left_padding += roll
|
||||
self.offset -= roll
|
||||
|
||||
# The largest size is self.max_size + S - 1 to ensure
|
||||
# every token gets at least self.max_size context
|
||||
trim_size = self._idx - self.max_size + 1
|
||||
@@ -845,6 +955,11 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
return self.keys, self.values
|
||||
|
||||
def _update_in_place(self, keys, values):
|
||||
if self._lengths is not None:
|
||||
raise RuntimeError(
|
||||
"finalize() should be called before deocoding with BatchRotatingKVCache"
|
||||
)
|
||||
|
||||
# May not have hit the max size yet, so potentially
|
||||
# keep growing the cache
|
||||
B, n_kv_heads, S, k_head_dim = keys.shape
|
||||
@@ -900,6 +1015,31 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
return min(self._offset, self.max_size)
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
raise ValueError(
|
||||
"Left padding can only be added to an empty BatchRotatingKVCache"
|
||||
)
|
||||
left_padding = mx.array(left_padding)
|
||||
self.left_padding += left_padding
|
||||
self.offset -= left_padding
|
||||
|
||||
if right_padding is not None and max(right_padding) > 0:
|
||||
self._lengths = mx.array(lengths) + self.offset
|
||||
|
||||
def finalize(self):
|
||||
if self._lengths is not None:
|
||||
roll = mx.maximum(0, self.offset - self._lengths)
|
||||
self.keys = dynamic_roll(self.keys, roll[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, roll[:, None], axis=2)
|
||||
self.left_padding += roll
|
||||
self.offset -= roll
|
||||
self._lengths = None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
k, v = self.keys, self.values
|
||||
@@ -1005,3 +1145,54 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
)
|
||||
self._idx = max_idx
|
||||
self._offset = max(self._offset, other._offset)
|
||||
|
||||
def extract(self, idx):
|
||||
cache = RotatingKVCache(self.max_size)
|
||||
padding = self.left_padding[idx].item()
|
||||
offset = self.offset[idx].item()
|
||||
cache.keys = self.keys[idx : idx + 1]
|
||||
cache.values = self.values[idx : idx + 1]
|
||||
cache._idx = self._idx
|
||||
if self.rotated:
|
||||
cache.keys = mx.roll(cache.keys, -self._idx, axis=2)
|
||||
cache.values = mx.roll(cache.values, -self._idx, axis=2)
|
||||
cache._idx = self.max_size
|
||||
if padding > 0:
|
||||
cache.keys = mx.contiguous(cache.keys[:, :, padding : cache._idx])
|
||||
cache.values = mx.contiguous(cache.values[:, :, padding : cache._idx])
|
||||
cache.offset = offset
|
||||
cache._idx = cache.keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
if not all(c.max_size == caches[0].max_size for c in caches):
|
||||
raise ValueError(
|
||||
"BatchRotatingKVCache can only merge caches with the same maximum size"
|
||||
)
|
||||
|
||||
offsets = [c.offset for c in caches]
|
||||
lengths = [len(c) for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
H = max(c.keys.shape[1] for c in caches if c.keys is not None)
|
||||
Dk = max(c.keys.shape[3] for c in caches if c.keys is not None)
|
||||
Dv = max(c.values.shape[3] for c in caches if c.values is not None)
|
||||
dt = next(iter(c.keys.dtype for c in caches if c.keys is not None))
|
||||
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
keys[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.keys)
|
||||
values[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.values)
|
||||
|
||||
cache = cls(caches[0].max_size, padding)
|
||||
cache.keys = keys
|
||||
cache.values = values
|
||||
cache.offset = mx.array(offsets)
|
||||
cache._idx = keys.shape[2]
|
||||
cache._offset = keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
@@ -8,6 +8,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -355,7 +356,7 @@ class DeepseekV2DecoderLayer(nn.Module):
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV2Model(nn.Module):
|
||||
class DeepseekV2Model(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -364,32 +365,8 @@ class DeepseekV2Model(nn.Module):
|
||||
DeepseekV2DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.start_idx = 0
|
||||
self.end_idx = len(self.layers)
|
||||
self.num_layers = self.end_idx
|
||||
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.num_layers = layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
self.num_layers = len(self.layers) - self.start_idx
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
@@ -401,15 +378,15 @@ class DeepseekV2Model(nn.Module):
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
@@ -454,4 +431,4 @@ class Model(nn.Module):
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
return self.model.pipeline_layers
|
||||
|
||||
+34
-134
@@ -9,6 +9,8 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -45,85 +47,6 @@ class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool = False
|
||||
|
||||
|
||||
def yarn_find_correction_dim(
|
||||
num_rotations, dim, base=10000, max_position_embeddings=2048
|
||||
):
|
||||
return (dim * math.log(max_position_embeddings / (num_rotations * 2 * math.pi))) / (
|
||||
2 * math.log(base)
|
||||
)
|
||||
|
||||
|
||||
def yarn_find_correction_range(
|
||||
low_rot, high_rot, dim, base=10000, max_position_embeddings=2048
|
||||
):
|
||||
low = math.floor(
|
||||
yarn_find_correction_dim(low_rot, dim, base, max_position_embeddings)
|
||||
)
|
||||
high = math.ceil(
|
||||
yarn_find_correction_dim(high_rot, dim, base, max_position_embeddings)
|
||||
)
|
||||
return max(low, 0), min(high, dim - 1)
|
||||
|
||||
|
||||
def yarn_get_mscale(scale=1, mscale=1):
|
||||
if scale <= 1:
|
||||
return 1.0
|
||||
return 0.1 * mscale * math.log(scale) + 1.0
|
||||
|
||||
|
||||
def yarn_linear_ramp_mask(min_val, max_val, dim):
|
||||
if min_val == max_val:
|
||||
max_val += 0.001 # Prevent singularity
|
||||
|
||||
linear_func = (mx.arange(dim, dtype=mx.float32) - min_val) / (max_val - min_val)
|
||||
return mx.clip(linear_func, 0, 1)
|
||||
|
||||
|
||||
class DeepseekV3YarnRotaryEmbedding(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
max_position_embeddings=2048,
|
||||
base=10000,
|
||||
scaling_factor=1.0,
|
||||
original_max_position_embeddings=4096,
|
||||
beta_fast=32,
|
||||
beta_slow=1,
|
||||
mscale=1,
|
||||
mscale_all_dim=0,
|
||||
):
|
||||
super().__init__()
|
||||
self.mscale = yarn_get_mscale(scaling_factor, mscale) / yarn_get_mscale(
|
||||
scaling_factor, mscale_all_dim
|
||||
)
|
||||
freq_extra = base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)
|
||||
freq_inter = scaling_factor * freq_extra
|
||||
low, high = yarn_find_correction_range(
|
||||
beta_fast,
|
||||
beta_slow,
|
||||
dim,
|
||||
base,
|
||||
original_max_position_embeddings,
|
||||
)
|
||||
freq_mask = 1.0 - yarn_linear_ramp_mask(low, high, dim // 2)
|
||||
self._freqs = (freq_inter * freq_extra) / (
|
||||
freq_inter * freq_mask + freq_extra * (1 - freq_mask)
|
||||
)
|
||||
|
||||
def __call__(self, x, offset=0):
|
||||
if self.mscale != 1.0:
|
||||
x = self.mscale * x
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
x.shape[-1],
|
||||
traditional=True,
|
||||
base=None,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
freqs=self._freqs,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV3Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -175,35 +98,19 @@ class DeepseekV3Attention(nn.Module):
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if mscale_all_dim:
|
||||
mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
|
||||
self.scale = self.scale * mscale * mscale
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
rope_kwargs = {
|
||||
key: self.config.rope_scaling[key]
|
||||
for key in [
|
||||
"original_max_position_embeddings",
|
||||
"beta_fast",
|
||||
"beta_slow",
|
||||
"mscale",
|
||||
"mscale_all_dim",
|
||||
]
|
||||
if key in self.config.rope_scaling
|
||||
}
|
||||
self.rope = DeepseekV3YarnRotaryEmbedding(
|
||||
dim=self.qk_rope_head_dim,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_factor=scaling_factor,
|
||||
base=self.rope_theta,
|
||||
**rope_kwargs,
|
||||
)
|
||||
else:
|
||||
self.rope = nn.RoPE(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -389,7 +296,7 @@ class DeepseekV3DecoderLayer(nn.Module):
|
||||
return h + r
|
||||
|
||||
|
||||
class DeepseekV3Model(nn.Module):
|
||||
class DeepseekV3Model(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -398,28 +305,7 @@ class DeepseekV3Model(nn.Module):
|
||||
DeepseekV3DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.start_idx = 0
|
||||
self.end_idx = len(self.layers)
|
||||
self.num_layers = self.end_idx
|
||||
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
self.num_layers = len(self.layers) - self.start_idx
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -432,16 +318,15 @@ class DeepseekV3Model(nn.Module):
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
@@ -487,7 +372,22 @@ class Model(nn.Module):
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
# Remap for int4
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if k.endswith("weight_shape"):
|
||||
base = k.replace("weight_shape", "")
|
||||
new_weights[base + "weight"] = weights[base + "weight_packed"].view(
|
||||
mx.uint32
|
||||
)
|
||||
s = weights[base + "weight_scale"]
|
||||
new_weights[base + "scales"] = s
|
||||
new_weights[base + "biases"] = -8 * s
|
||||
elif not (k.endswith("weight_scale") or k.endswith("weight_packed")):
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Dequantize fp8
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
@@ -521,7 +421,7 @@ class Model(nn.Module):
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "deepseek_v32"
|
||||
vocab_size: int = 102400
|
||||
hidden_size: int = 4096
|
||||
index_head_dim: int = 128
|
||||
index_n_heads: int = 64
|
||||
index_topk: int = 2048
|
||||
intermediate_size: int = 11008
|
||||
moe_intermediate_size: int = 1407
|
||||
num_hidden_layers: int = 30
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 32
|
||||
n_shared_experts: Optional[int] = None
|
||||
n_routed_experts: Optional[int] = None
|
||||
routed_scaling_factor: float = 1.0
|
||||
kv_lora_rank: int = 512
|
||||
q_lora_rank: int = 1536
|
||||
qk_rope_head_dim: int = 64
|
||||
v_head_dim: int = 128
|
||||
qk_nope_head_dim: int = 128
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
num_experts_per_tok: int = 1
|
||||
moe_layer_freq: int = 1
|
||||
first_k_dense_replace: int = 0
|
||||
max_position_embeddings: int = 2048
|
||||
rms_norm_eps: float = 1e-6
|
||||
rope_theta: float = 10000.0
|
||||
rope_scaling: Dict = None
|
||||
attention_bias: bool = False
|
||||
|
||||
|
||||
class Indexer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.dim = args.hidden_size
|
||||
self.n_heads = args.index_n_heads
|
||||
self.head_dim = args.index_head_dim
|
||||
self.rope_head_dim = args.qk_rope_head_dim
|
||||
self.index_topk = args.index_topk
|
||||
self.q_lora_rank = args.q_lora_rank
|
||||
self.wq_b = nn.Linear(
|
||||
self.q_lora_rank, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.wk = nn.Linear(self.dim, self.head_dim, bias=False)
|
||||
self.k_norm = nn.LayerNorm(self.head_dim)
|
||||
self.weights_proj = nn.Linear(self.dim, self.n_heads, bias=False)
|
||||
self.softmax_scale = self.head_dim**-0.5
|
||||
self.rope = initialize_rope(
|
||||
dims=args.qk_rope_head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
qr: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
# Computes top_k indices for attention
|
||||
b, s, _ = x.shape
|
||||
q = self.wq_b(qr)
|
||||
q = q.reshape(b, s, self.n_heads, self.head_dim).swapaxes(1, 2)
|
||||
q_pe, q_nope = mx.split(q, [self.rope_head_dim], axis=-1)
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
|
||||
q_pe = self.rope(q_pe, offset=offset)
|
||||
q = mx.concatenate([q_pe, q_nope], axis=-1)
|
||||
|
||||
k = self.wk(x)
|
||||
k = self.k_norm(k)
|
||||
k = mx.reshape(k, (b, 1, s, self.head_dim))
|
||||
k_pe, k_nope = mx.split(k, [self.rope_head_dim], axis=-1)
|
||||
k_pe = self.rope(k_pe, offset=offset)
|
||||
k = mx.concatenate([k_pe, k_nope], axis=-1)
|
||||
if cache is not None:
|
||||
k, _ = cache.update_and_fetch(k, mx.zeros([b, 1, s, 0]))
|
||||
if k.shape[2] <= self.index_topk:
|
||||
return None
|
||||
scores = q @ k.swapaxes(-1, -2)
|
||||
scores = mx.maximum(scores, 0)
|
||||
weights = self.weights_proj(x) * (self.n_heads**-0.5 * self.softmax_scale)
|
||||
weights = weights.swapaxes(-1, -2)[..., None]
|
||||
scores = scores * weights
|
||||
scores = scores.sum(axis=1)
|
||||
if mask is not None:
|
||||
scores = mx.where(mask, scores, -float("inf"))
|
||||
return mx.argpartition(scores, kth=-self.index_topk, axis=-1)[
|
||||
..., -self.index_topk :
|
||||
]
|
||||
|
||||
|
||||
class DeepseekV32Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_heads = config.num_attention_heads
|
||||
self.max_position_embeddings = config.max_position_embeddings
|
||||
self.rope_theta = config.rope_theta
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.qk_rope_head_dim = config.qk_rope_head_dim
|
||||
self.kv_lora_rank = config.kv_lora_rank
|
||||
self.v_head_dim = config.v_head_dim
|
||||
self.qk_nope_head_dim = config.qk_nope_head_dim
|
||||
self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
|
||||
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=1e-6)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.indexer = Indexer(config)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qr = self.q_a_layernorm(self.q_a_proj(x))
|
||||
q = self.q_b_proj(qr)
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
|
||||
kv = kv.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if cache is not None:
|
||||
q_pe = self.rope(q_pe, cache[0].offset)
|
||||
k_pe = self.rope(k_pe, cache[0].offset)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys, values = cache[0].update_and_fetch(
|
||||
mx.concatenate([k_nope, k_pe], axis=-1), values
|
||||
)
|
||||
else:
|
||||
cache = [None] * 2
|
||||
q_pe = self.rope(q_pe)
|
||||
k_pe = self.rope(k_pe)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys = mx.concatenate([k_nope, k_pe], axis=-1)
|
||||
|
||||
queries = mx.concatenate([q_nope, q_pe], axis=-1)
|
||||
topk_indices = self.indexer(x, qr, mask, cache=cache[1])
|
||||
if topk_indices is not None:
|
||||
k_seq = keys.shape[2]
|
||||
sparse_mask = mx.zeros((B, L, k_seq), dtype=mx.bool_)
|
||||
sparse_mask = mx.put_along_axis(
|
||||
sparse_mask, topk_indices, mx.array(True), axis=-1
|
||||
)
|
||||
sparse_mask = sparse_mask[:, None, :, :]
|
||||
if mask is not None:
|
||||
sparse_mask = sparse_mask & mask
|
||||
mask = sparse_mask
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache[0], scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DeepseekV32MLP(nn.Module):
|
||||
def __init__(
|
||||
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV32MoE(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.num_experts_per_tok = config.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
if config.n_shared_experts is not None:
|
||||
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
|
||||
self.shared_experts = DeepseekV32MLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DeepseekV32DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = DeepseekV32Attention(config)
|
||||
self.mlp = (
|
||||
DeepseekV32MoE(config)
|
||||
if (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
and layer_idx % config.moe_layer_freq == 0
|
||||
)
|
||||
else DeepseekV32MLP(config)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class DeepseekV32Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.layers = [
|
||||
DeepseekV32DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.start_idx = 0
|
||||
self.end_idx = len(self.layers)
|
||||
self.num_layers = self.end_idx
|
||||
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
self.num_layers = len(self.layers) - self.start_idx
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
mask = create_attention_mask(
|
||||
h, cache[0][0] if cache[0] else None, return_array=True
|
||||
)
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = DeepseekV32Model(config)
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
pad_side = (-n) % bs
|
||||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||||
weight = weight.reshape(
|
||||
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
|
||||
)
|
||||
weight = (weight * scale_inv[:, None, :, None]).reshape(
|
||||
m + pad_bottom, n + pad_side
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
|
||||
# Remove multi-token prediction layer and any unused precomputed rotary freqs
|
||||
return {
|
||||
k: v
|
||||
for k, v in weights.items()
|
||||
if not k.startswith("model.layers.61") and "rotary_emb.inv_freq" not in k
|
||||
}
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def make_cache(self):
|
||||
return [CacheList(KVCache(), KVCache()) for _ in self.layers]
|
||||
@@ -7,15 +7,28 @@ import mlx.nn as nn
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def compute_g(A_log, a, dt_bias):
|
||||
return mx.exp(
|
||||
-mx.exp(A_log.astype(mx.float32)) * nn.softplus(a + dt_bias).astype(A_log.dtype)
|
||||
return mx.exp(-mx.exp(A_log.astype(mx.float32)) * nn.softplus(a + dt_bias)).astype(
|
||||
A_log.dtype
|
||||
)
|
||||
|
||||
|
||||
def _make_gated_delta_kernel(has_mask=False):
|
||||
def _make_gated_delta_kernel(has_mask=False, vectorized=False):
|
||||
if not mx.metal.is_available():
|
||||
return None
|
||||
mask_source = "mask[b_idx * T + t]" if has_mask else "true"
|
||||
|
||||
# Configure g indexing based on whether gating is vectorized
|
||||
if vectorized:
|
||||
g_comment = "// g: [B, T, Hv, Dk]"
|
||||
g_setup = "auto g_ = g + (b_idx * T * Hv + hv_idx) * Dk;"
|
||||
g_access = "g_[s_idx]"
|
||||
g_advance = "g_ += Hv * Dk;"
|
||||
else:
|
||||
g_comment = "// g: [B, T, Hv]"
|
||||
g_setup = "auto g_ = g + b_idx * T * Hv;"
|
||||
g_access = "g_[hv_idx]"
|
||||
g_advance = "g_ += Hv;"
|
||||
|
||||
source = f"""
|
||||
auto n = thread_position_in_grid.z;
|
||||
auto b_idx = n / Hv;
|
||||
@@ -44,8 +57,8 @@ def _make_gated_delta_kernel(has_mask=False):
|
||||
state[i] = static_cast<float>(i_state[s_idx]);
|
||||
}}
|
||||
|
||||
// beta, g: [B, T, Hv]
|
||||
auto g_ = g + b_idx * T * Hv;
|
||||
{g_comment}
|
||||
{g_setup}
|
||||
auto beta_ = beta + b_idx * T * Hv;
|
||||
|
||||
for (int t = 0; t < T; ++t) {{
|
||||
@@ -53,7 +66,7 @@ def _make_gated_delta_kernel(has_mask=False):
|
||||
float kv_mem = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] * g_[hv_idx];
|
||||
state[i] = state[i] * {g_access};
|
||||
kv_mem += state[i] * k_[s_idx];
|
||||
}}
|
||||
kv_mem = simd_sum(kv_mem);
|
||||
@@ -76,7 +89,7 @@ def _make_gated_delta_kernel(has_mask=False):
|
||||
k_ += Hk * Dk;
|
||||
v_ += Hv * Dv;
|
||||
y += Hv * Dv;
|
||||
g_ += Hv;
|
||||
{g_advance}
|
||||
beta_ += Hv;
|
||||
}}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
@@ -87,16 +100,27 @@ def _make_gated_delta_kernel(has_mask=False):
|
||||
inputs = ["q", "k", "v", "g", "beta", "state_in", "T"]
|
||||
if has_mask:
|
||||
inputs.append("mask")
|
||||
|
||||
suffix = ""
|
||||
if vectorized:
|
||||
suffix += "_vec"
|
||||
if has_mask:
|
||||
suffix += "_mask"
|
||||
|
||||
return mx.fast.metal_kernel(
|
||||
name="gated_delta_step" + "_mask" if has_mask else "",
|
||||
name=f"gated_delta_step{suffix}",
|
||||
input_names=inputs,
|
||||
output_names=["y", "state_out"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
_gated_delta_kernel = _make_gated_delta_kernel()
|
||||
_gated_delta_kernel_masked = _make_gated_delta_kernel(True)
|
||||
_gated_delta_kernel = _make_gated_delta_kernel(has_mask=False, vectorized=False)
|
||||
_gated_delta_kernel_masked = _make_gated_delta_kernel(has_mask=True, vectorized=False)
|
||||
_gated_delta_kernel_vec = _make_gated_delta_kernel(has_mask=False, vectorized=True)
|
||||
_gated_delta_kernel_vec_masked = _make_gated_delta_kernel(
|
||||
has_mask=True, vectorized=True
|
||||
)
|
||||
|
||||
|
||||
@mx.compile
|
||||
@@ -115,7 +139,8 @@ def _gated_delta_step_ops(
|
||||
Shapes:
|
||||
- q, k: [B, H, Dk]
|
||||
- v: [B, H, Dv]
|
||||
- g, beta: [B, H]
|
||||
- g: [B, H] or [B, H, Dk]
|
||||
- beta: [B, H]
|
||||
- state: [B, H, Dv, Dk]
|
||||
Returns:
|
||||
- y: [B, H, Dv]
|
||||
@@ -124,13 +149,23 @@ def _gated_delta_step_ops(
|
||||
|
||||
# Decay
|
||||
old_state = state
|
||||
state = state * g[..., None, None]
|
||||
if g.ndim == 2:
|
||||
decay = g[..., None, None]
|
||||
elif g.ndim == 3:
|
||||
decay = g[..., None, :]
|
||||
else:
|
||||
raise ValueError(f"Unsupported gating shape {g.shape}")
|
||||
state = state * decay
|
||||
kv_mem = (state * k[..., None, :]).sum(axis=-1) # [B, H, Dv]
|
||||
delta = (v - kv_mem) * beta[..., None] # [B, H, Dv]
|
||||
state = state + k[..., None, :] * delta[..., None]
|
||||
# Output projection along key dim with q
|
||||
y = (state * q[..., None, :]).sum(axis=-1) # [B, H, Dv]
|
||||
if mask is not None:
|
||||
if mask.ndim == 2:
|
||||
mask = mx.expand_dims(mask, axes=(2, 3))
|
||||
elif mask.ndim == 3:
|
||||
mask = mx.expand_dims(mask, axis=-1)
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y, state
|
||||
|
||||
@@ -147,11 +182,19 @@ def gated_delta_kernel(
|
||||
B, T, Hk, Dk = k.shape
|
||||
Hv, Dv = v.shape[2:]
|
||||
input_type = q.dtype
|
||||
kernel = _gated_delta_kernel
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_masked
|
||||
inputs.append(mask)
|
||||
if g.ndim == 4:
|
||||
kernel = _gated_delta_kernel_vec
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_vec_masked
|
||||
inputs.append(mask)
|
||||
else:
|
||||
kernel = _gated_delta_kernel
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_masked
|
||||
inputs.append(mask)
|
||||
|
||||
return kernel(
|
||||
inputs=inputs,
|
||||
template=[
|
||||
@@ -179,15 +222,17 @@ def gated_delta_ops(
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""
|
||||
Ops-based reference implementation for prompt prefill (sequential loop).
|
||||
Supports both scalar and vectorized gating.
|
||||
|
||||
Shapes:
|
||||
- q, k: [B, T, Hk, Dk]
|
||||
- v: [B, T, Hv, Dv]
|
||||
- g, beta: [B, T, Hv]
|
||||
- state: [B, Hv, Dk, Dv]
|
||||
- g: [B, T, Hv] (scalar) or [B, T, Hv, Dk] (vectorized)
|
||||
- beta: [B, T, Hv]
|
||||
- state: [B, Hv, Dv, Dk]
|
||||
Returns:
|
||||
- y: [B, T, Hv, Dv]
|
||||
- state: [B, Hv, Dk, Dv]
|
||||
- state: [B, Hv, Dv, Dk]
|
||||
"""
|
||||
B, T, Hk, Dk = q.shape
|
||||
Hv, Dv = v.shape[-2:]
|
||||
@@ -200,25 +245,15 @@ def gated_delta_ops(
|
||||
|
||||
ys = []
|
||||
for t in range(T):
|
||||
if mask is not None:
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
mask[:, t],
|
||||
)
|
||||
else:
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
)
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
None if mask is None else mask[:, t],
|
||||
)
|
||||
ys.append(y)
|
||||
y = mx.stack(ys, axis=1)
|
||||
return y, state
|
||||
@@ -242,10 +277,8 @@ def gated_delta_update(
|
||||
if state is None:
|
||||
B, _, Hk, Dk = q.shape
|
||||
Hv, Dv = v.shape[-2:]
|
||||
if state is None:
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
|
||||
|
||||
if not use_kernel or mx.default_device() != mx.gpu or not mx.metal.is_available():
|
||||
return gated_delta_ops(q, k, v, g, beta, state, mask)
|
||||
else:
|
||||
return gated_delta_kernel(q, k, v, g, beta, state, mask)
|
||||
return gated_delta_kernel(q, k, v, g, beta, state, mask)
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,12 +23,13 @@ class ModelArgs(BaseModelArgs):
|
||||
rms_norm_eps: float = 1.0e-6
|
||||
vocab_size: int = 262144
|
||||
num_key_value_heads: int = 1
|
||||
rope_global_base_freq: float = 1_000_000.0
|
||||
rope_theta: float = 1_000_000.0
|
||||
rope_local_base_freq: float = 10_000.0
|
||||
rope_traditional: bool = False
|
||||
query_pre_attn_scalar: float = 256
|
||||
sliding_window: int = 512
|
||||
sliding_window_pattern: int = 6
|
||||
max_position_embeddings: int = 32768
|
||||
rope_scaling: Dict = None
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
@@ -52,14 +54,12 @@ class Attention(nn.Module):
|
||||
self.k_norm = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
|
||||
self.is_sliding = (layer_idx + 1) % args.sliding_window_pattern != 0
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
head_dim,
|
||||
traditional=args.rope_traditional,
|
||||
base=(
|
||||
args.rope_local_base_freq
|
||||
if self.is_sliding
|
||||
else args.rope_global_base_freq
|
||||
),
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=(args.rope_local_base_freq if self.is_sliding else args.rope_theta),
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
@@ -187,11 +187,14 @@ class Gemma3Model(nn.Module):
|
||||
|
||||
global_mask = create_attention_mask(h, cache[self.sliding_window_pattern - 1])
|
||||
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h,
|
||||
cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
if self.sliding_window_pattern > 1:
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h,
|
||||
cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
else:
|
||||
sliding_window_mask = None
|
||||
for i, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
is_global = (
|
||||
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
|
||||
|
||||
@@ -9,6 +9,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -243,7 +244,7 @@ class DecoderLayer(nn.Module):
|
||||
return h + r
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
class LanguageModel(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -264,13 +265,28 @@ class LanguageModel(nn.Module):
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -315,7 +331,7 @@ class Model(nn.Module):
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
|
||||
@@ -23,6 +23,7 @@ class ModelArgs(BaseModelArgs):
|
||||
vocab_size: int
|
||||
rotary_emb_base: int
|
||||
rotary_pct: float
|
||||
use_parallel_residual: bool = True
|
||||
num_key_value_heads: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -107,6 +108,7 @@ class TransformerBlock(nn.Module):
|
||||
self.layer_norm_eps = args.layer_norm_eps
|
||||
self.attention = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.use_parallel_residual = args.use_parallel_residual
|
||||
self.input_layernorm = nn.LayerNorm(
|
||||
self.hidden_size,
|
||||
eps=self.layer_norm_eps,
|
||||
@@ -121,12 +123,20 @@ class TransformerBlock(nn.Module):
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
residual = x
|
||||
# NeoX runs attention and feedforward network in parallel.
|
||||
attn = self.attention(self.input_layernorm(x), mask, cache)
|
||||
ffn = self.mlp(self.post_attention_layernorm(x))
|
||||
out = attn + ffn + residual
|
||||
return out
|
||||
if self.use_parallel_residual:
|
||||
residual = x
|
||||
# Run attention and feedforward network in parallel.
|
||||
attn = self.attention(self.input_layernorm(x), mask, cache)
|
||||
ffn = self.mlp(self.post_attention_layernorm(x))
|
||||
out = attn + ffn + residual
|
||||
return out
|
||||
else:
|
||||
# Run attention and feedforward network sequentially.
|
||||
attn_output = self.attention(self.input_layernorm(x), mask, cache)
|
||||
x = x + attn_output
|
||||
ffn_output = self.mlp(self.post_attention_layernorm(x))
|
||||
x = x + ffn_output
|
||||
return x
|
||||
|
||||
|
||||
class GPTNeoXModel(nn.Module):
|
||||
|
||||
+28
-19
@@ -247,14 +247,20 @@ class JambaSparseMoeBlock(nn.Module):
|
||||
|
||||
|
||||
class JambaDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_type: str):
|
||||
def __init__(self, args: ModelArgs, layer_type: str, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_attn = layer_type == "attention"
|
||||
if self.is_attn:
|
||||
self.self_attn = JambaAttention(args)
|
||||
else:
|
||||
self.mamba = JambaMambaMixer(args)
|
||||
ffn_layer_class = JambaSparseMoeBlock if args.num_experts > 1 else JambaMLP
|
||||
if (
|
||||
args.num_experts > 1
|
||||
and (layer_idx + args.expert_layer_offset) % args.expert_layer_period == 0
|
||||
):
|
||||
ffn_layer_class = JambaSparseMoeBlock
|
||||
else:
|
||||
ffn_layer_class = JambaMLP
|
||||
self.feed_forward = ffn_layer_class(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_ff_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
@@ -279,7 +285,10 @@ class JambaModel(nn.Module):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [JambaDecoderLayer(args, t) for t in args.layers_block_type]
|
||||
self.layers = [
|
||||
JambaDecoderLayer(args, t, idx)
|
||||
for idx, t in enumerate(args.layers_block_type)
|
||||
]
|
||||
self.final_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.attn_idx = args.layers_block_type.index("attention")
|
||||
self.ssm_idx = args.layers_block_type.index("mamba")
|
||||
@@ -335,30 +344,30 @@ class Model(nn.Module):
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
for k, v in list(weights.items()):
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
|
||||
return weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.block_sparse_moe.experts.0.{n}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(
|
||||
f"{prefix}.block_sparse_moe.experts.{e}.{n}.{k}"
|
||||
)
|
||||
for e in range(self.args.num_local_experts)
|
||||
]
|
||||
weights[f"{prefix}.block_sparse_moe.switch_mlp.{m}.{k}"] = (
|
||||
mx.stack(to_join)
|
||||
base = f"model.layers.{l}.feed_forward"
|
||||
if not any(key.startswith(f"{base}.experts.") for key in weights.keys()):
|
||||
continue
|
||||
|
||||
for proj in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for name in ["weight", "bias", "scales", "biases"]:
|
||||
expert_tensors = [
|
||||
weights.pop(f"{base}.experts.{e}.{proj}.{name}")
|
||||
for e in range(len(weights))
|
||||
if f"{base}.experts.{e}.{proj}.{name}" in weights
|
||||
]
|
||||
if expert_tensors:
|
||||
weights[f"{base}.switch_mlp.{proj}.{name}"] = mx.stack(
|
||||
expert_tensors
|
||||
)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .gated_delta import gated_delta_update
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
intermediate_size: int
|
||||
head_dim: int
|
||||
rope_theta: float
|
||||
rms_norm_eps: float
|
||||
linear_attn_config: Dict[str, Any]
|
||||
model_max_length: int
|
||||
num_experts: int
|
||||
moe_intermediate_size: int
|
||||
kv_lora_rank: int
|
||||
rope_scaling: Optional[Dict[str, Any]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
qk_nope_head_dim: Optional[int] = None
|
||||
qk_rope_head_dim: Optional[int] = None
|
||||
v_head_dim: Optional[int] = None
|
||||
mla_use_nope: bool = False
|
||||
num_experts_per_token: int = 1
|
||||
num_shared_experts: int = 0
|
||||
moe_router_activation_func: str = "sigmoid"
|
||||
moe_renormalize: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
first_k_dense_replace: int = 0
|
||||
moe_layer_freq: int = 1
|
||||
use_grouped_topk: bool = True
|
||||
num_expert_group: int = 1
|
||||
topk_group: int = 1
|
||||
|
||||
|
||||
class KimiMLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
hidden_size: Optional[int] = None,
|
||||
intermediate_size: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
dim = hidden_size or args.hidden_size
|
||||
hidden = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.down_proj = nn.Linear(hidden, dim, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
@mx.compile
|
||||
def _group_expert_select(
|
||||
gates: mx.array,
|
||||
bias: Optional[mx.array],
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
renormalize: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates)
|
||||
elif score_function == "softmax":
|
||||
scores = mx.softmax(gates, axis=-1, precise=True)
|
||||
else:
|
||||
raise ValueError(f"Unsupported MoE router activation '{score_function}'")
|
||||
|
||||
orig_scores = scores
|
||||
if bias is not None:
|
||||
scores = scores + bias.astype(scores.dtype)
|
||||
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores,
|
||||
mx.stop_gradient(group_idx),
|
||||
mx.array(0.0, dtype=scores.dtype),
|
||||
axis=-2,
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
inds = mx.argpartition(-scores, kth=top_k - 1, axis=-1)[..., :top_k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
if top_k > 1 and renormalize:
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
|
||||
return inds, scores * routed_scaling_factor
|
||||
|
||||
|
||||
class KimiSparseMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
hidden = args.hidden_size
|
||||
experts = args.num_experts
|
||||
if experts is None:
|
||||
raise ValueError("num_experts must be specified for MoE layers")
|
||||
|
||||
self.gate = nn.Linear(hidden, experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(hidden, args.moe_intermediate_size, experts)
|
||||
self.e_score_correction_bias = mx.zeros((experts,), dtype=mx.float32)
|
||||
|
||||
if args.num_shared_experts:
|
||||
shared_hidden = args.moe_intermediate_size * args.num_shared_experts
|
||||
self.shared_experts = KimiMLP(args, intermediate_size=shared_hidden)
|
||||
else:
|
||||
self.shared_experts = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
scores = self.gate(x)
|
||||
inds, weights = _group_expert_select(
|
||||
scores,
|
||||
self.e_score_correction_bias,
|
||||
self.args.num_experts_per_token,
|
||||
self.args.num_expert_group,
|
||||
self.args.topk_group,
|
||||
self.args.routed_scaling_factor,
|
||||
self.args.moe_renormalize,
|
||||
self.args.moe_router_activation_func,
|
||||
)
|
||||
out = self.switch_mlp(x, inds)
|
||||
out = (out * weights[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class KimiMLAAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.qk_nope_head_dim = args.qk_nope_head_dim or args.head_dim
|
||||
self.qk_rope_head_dim = args.qk_rope_head_dim or 0
|
||||
self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
self.v_head_dim = args.v_head_dim or args.head_dim
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
hidden = args.hidden_size
|
||||
self.q_proj = nn.Linear(hidden, self.num_heads * self.q_head_dim, bias=False)
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
hidden,
|
||||
args.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=False,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(args.kv_lora_rank, eps=args.rms_norm_eps)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
args.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, hidden, bias=False)
|
||||
|
||||
rope_dim = self.qk_rope_head_dim or self.q_head_dim
|
||||
self.rope = initialize_rope(
|
||||
rope_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.model_max_length,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
q_states = self.q_proj(x).reshape(B, L, self.num_heads, self.q_head_dim)
|
||||
q_pass, q_rot = mx.split(q_states, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
compressed = self.kv_a_proj_with_mqa(x)
|
||||
k_pass, k_rot = mx.split(
|
||||
compressed, [compressed.shape[-1] - self.qk_rope_head_dim], axis=-1
|
||||
)
|
||||
k_pass = self.kv_a_layernorm(k_pass)
|
||||
kv = self.kv_b_proj(k_pass)
|
||||
kv = kv.reshape(
|
||||
B,
|
||||
L,
|
||||
self.num_heads,
|
||||
self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim,
|
||||
)
|
||||
k_pass, v_states = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if self.qk_rope_head_dim:
|
||||
k_rot = mx.reshape(k_rot, (B, L, 1, self.qk_rope_head_dim))
|
||||
k_rot = mx.broadcast_to(k_rot, (*k_pass.shape[:-1], self.qk_rope_head_dim))
|
||||
else:
|
||||
k_rot = mx.zeros((*k_pass.shape[:-1], 0), dtype=k_pass.dtype)
|
||||
|
||||
queries = mx.concatenate([q_pass, q_rot], axis=-1).transpose(0, 2, 1, 3)
|
||||
keys = mx.concatenate([k_pass, k_rot], axis=-1).transpose(0, 2, 1, 3)
|
||||
values = v_states.transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
out = scaled_dot_product_attention(
|
||||
queries,
|
||||
keys,
|
||||
values,
|
||||
cache,
|
||||
scale=self.scale,
|
||||
mask=mask,
|
||||
)
|
||||
out = out.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(out)
|
||||
|
||||
|
||||
class ShortConv1d(nn.Module):
|
||||
def __init__(self, channels: int, kernel_size: int):
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=channels,
|
||||
out_channels=channels,
|
||||
kernel_size=kernel_size,
|
||||
bias=False,
|
||||
groups=channels,
|
||||
padding=0,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, cache: Optional[mx.array]
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if cache is None:
|
||||
pad = mx.zeros(
|
||||
(x.shape[0], self.kernel_size - 1, x.shape[-1]), dtype=x.dtype
|
||||
)
|
||||
else:
|
||||
pad = cache
|
||||
conv_input = mx.concatenate([pad, x], axis=1)
|
||||
out = nn.silu(self.conv(conv_input))
|
||||
new_cache = conv_input[:, -self.kernel_size + 1 :, :]
|
||||
return out, new_cache
|
||||
|
||||
|
||||
class KimiDeltaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
cfg = args.linear_attn_config
|
||||
|
||||
self.layer_idx = layer_idx
|
||||
self.num_heads = cfg["num_heads"]
|
||||
self.head_dim = cfg["head_dim"]
|
||||
self.conv_kernel = cfg.get("short_conv_kernel_size", 4)
|
||||
|
||||
self.projection_dim = self.num_heads * self.head_dim
|
||||
hidden = args.hidden_size
|
||||
|
||||
self.scale = float(self.head_dim) ** -0.5
|
||||
|
||||
self.q_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.k_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.v_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
|
||||
self.q_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.k_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.v_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
|
||||
self.f_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.f_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
self.b_proj = nn.Linear(hidden, self.num_heads, bias=False)
|
||||
|
||||
self.g_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.g_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
|
||||
self.A_log = mx.expand_dims(
|
||||
mx.log(mx.random.uniform(low=1.0, high=16.0, shape=(self.num_heads,))),
|
||||
(0, 1, 3),
|
||||
)
|
||||
self.dt_bias = mx.zeros((self.projection_dim,))
|
||||
|
||||
self.o_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.o_proj = nn.Linear(self.projection_dim, hidden, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, T, _ = x.shape
|
||||
dtype = x.dtype
|
||||
|
||||
if cache is not None:
|
||||
conv_state, ssm_state = cache
|
||||
else:
|
||||
conv_state = None
|
||||
ssm_state = None
|
||||
|
||||
if conv_state is None:
|
||||
s = mx.zeros((B, self.conv_kernel - 1, self.projection_dim), dtype=dtype)
|
||||
q_state = s
|
||||
k_state = s
|
||||
v_state = s
|
||||
else:
|
||||
q_state, k_state, v_state = conv_state
|
||||
|
||||
q_conv, q_state = self.q_conv(self.q_proj(x), q_state)
|
||||
k_conv, k_state = self.k_conv(self.k_proj(x), k_state)
|
||||
v_conv, v_state = self.v_conv(self.v_proj(x), v_state)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = (q_state, k_state, v_state)
|
||||
|
||||
q = q_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
k = k_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
v = v_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
|
||||
def _l2norm(x, eps=1e-6):
|
||||
norm = mx.linalg.norm(x, axis=-1, keepdims=True)
|
||||
return x / (norm + eps)
|
||||
|
||||
q = _l2norm(q)
|
||||
k = _l2norm(k)
|
||||
q = q * self.scale
|
||||
|
||||
a_logits = self.f_b_proj(self.f_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
b_logits = self.b_proj(x).reshape(B, T, self.num_heads)
|
||||
|
||||
out, ssm_state = gated_delta_update(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a_logits,
|
||||
b_logits,
|
||||
self.A_log.reshape(self.num_heads, 1),
|
||||
self.dt_bias.reshape(self.num_heads, self.head_dim),
|
||||
state=ssm_state,
|
||||
mask=mask,
|
||||
use_kernel=not self.training,
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[1] = ssm_state
|
||||
|
||||
gate = self.g_b_proj(self.g_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
out = (
|
||||
self.o_norm(out.reshape(B, T, self.num_heads, self.head_dim))
|
||||
* mx.sigmoid(gate)
|
||||
).reshape(B, T, -1)
|
||||
return self.o_proj(out)
|
||||
|
||||
|
||||
class KimiDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.is_linear = (layer_idx + 1) in kda_layers
|
||||
|
||||
if self.is_linear:
|
||||
self.self_attn = KimiDeltaAttention(args, layer_idx)
|
||||
else:
|
||||
self.self_attn = KimiMLAAttention(args)
|
||||
|
||||
if (
|
||||
args.num_experts > 0
|
||||
and layer_idx >= args.first_k_dense_replace
|
||||
and layer_idx % args.moe_layer_freq == 0
|
||||
):
|
||||
self.mlp = KimiSparseMoE(args)
|
||||
else:
|
||||
self.mlp = KimiMLP(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
attn_cache = None if cache is None else cache
|
||||
y = self.self_attn(self.input_layernorm(x), mask, attn_cache)
|
||||
h = x + y
|
||||
z = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + z
|
||||
|
||||
|
||||
class KimiLinearModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [KimiDecoderLayer(args, i) for i in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.ssm_idx = kda_layers[0] - 1
|
||||
for i in range(len(self.layers)):
|
||||
if (i + 1) not in kda_layers:
|
||||
self.attn_idx = i
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
|
||||
for layer, layer_cache in zip(self.layers, cache):
|
||||
mask = ssm_mask if layer.is_linear else attn_mask
|
||||
h = layer(h, mask=mask, cache=layer_cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = KimiLinearModel(args)
|
||||
if args.tie_word_embeddings:
|
||||
self.lm_head = None
|
||||
else:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.lm_head is None:
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches: List[Any] = []
|
||||
for layer in self.layers:
|
||||
if layer.is_linear:
|
||||
caches.append(MambaCache())
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
||||
weights = {k: v for k, v in weights.items() if not k.startswith("model.mtp")}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
prefix = f"model.layers.{layer_idx}"
|
||||
|
||||
if isinstance(layer.mlp, KimiSparseMoE):
|
||||
src_prefix = f"{prefix}.block_sparse_moe"
|
||||
dst_prefix = f"{prefix}.mlp"
|
||||
for src, dst in [
|
||||
("w1", "gate_proj"),
|
||||
("w2", "down_proj"),
|
||||
("w3", "up_proj"),
|
||||
]:
|
||||
key = f"{src_prefix}.experts.0.{src}.weight"
|
||||
if key in weights:
|
||||
stacked = [
|
||||
weights.pop(f"{src_prefix}.experts.{i}.{src}.weight")
|
||||
for i in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{dst_prefix}.switch_mlp.{dst}.weight"] = mx.stack(
|
||||
stacked
|
||||
)
|
||||
|
||||
for name in ("gate_proj", "up_proj", "down_proj"):
|
||||
src_key = f"{src_prefix}.shared_experts.{name}.weight"
|
||||
if src_key in weights:
|
||||
weights[f"{dst_prefix}.shared_experts.{name}.weight"] = (
|
||||
weights.pop(src_key)
|
||||
)
|
||||
|
||||
gate_key = f"{src_prefix}.gate.weight"
|
||||
if gate_key in weights:
|
||||
weights[f"{dst_prefix}.gate.weight"] = weights.pop(gate_key)
|
||||
|
||||
bias_key = f"{src_prefix}.gate.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{dst_prefix}.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
attn = getattr(layer, "self_attn", None)
|
||||
if isinstance(attn, KimiDeltaAttention):
|
||||
attn_prefix = f"{prefix}.self_attn"
|
||||
for src_name, dst_name in (
|
||||
("q_conv1d", "q_conv"),
|
||||
("k_conv1d", "k_conv"),
|
||||
("v_conv1d", "v_conv"),
|
||||
):
|
||||
src_key = f"{attn_prefix}.{src_name}.weight"
|
||||
if src_key in weights:
|
||||
w = weights.pop(src_key)
|
||||
if w.ndim == 3:
|
||||
w = w.moveaxis(2, 1)
|
||||
weights[f"{attn_prefix}.{dst_name}.conv.weight"] = w
|
||||
dt_key = f"{attn_prefix}.dt_bias"
|
||||
if dt_key in weights:
|
||||
if weights[dt_key].ndim > 1:
|
||||
weights[dt_key] = mx.reshape(weights[dt_key], (-1,))
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(path: str):
|
||||
if "e_score_correction_bias" in path:
|
||||
return False
|
||||
if path.endswith("A_log") or path.endswith("dt_bias"):
|
||||
return False
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,287 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
num_experts_per_tok: int
|
||||
num_local_experts: int
|
||||
shared_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
rotary_dim: int
|
||||
vocab_size: int
|
||||
tie_word_embeddings: bool = False
|
||||
scoring_func: str = "sigmoid"
|
||||
head_dim: Optional[int] = None
|
||||
use_qk_norm: bool = True
|
||||
|
||||
|
||||
class MiniMaxAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_dim = hidden_size = args.hidden_size
|
||||
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = head_dim = (
|
||||
args.head_dim or hidden_size // args.num_attention_heads
|
||||
)
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size, self.num_attention_heads * head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_attention_heads * head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.use_qk_norm = args.use_qk_norm if hasattr(args, "use_qk_norm") else False
|
||||
if self.use_qk_norm:
|
||||
self.q_norm = nn.RMSNorm(
|
||||
head_dim * self.num_attention_heads, eps=args.rms_norm_eps
|
||||
)
|
||||
self.k_norm = nn.RMSNorm(
|
||||
head_dim * self.num_key_value_heads, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
self.rope = nn.RoPE(args.rotary_dim, traditional=False, base=args.rope_theta)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MiniMaxSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_local_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, args.num_local_experts
|
||||
)
|
||||
self.e_score_correction_bias = mx.zeros((args.num_local_experts,))
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.gate(x.astype(mx.float32))
|
||||
|
||||
scores = mx.sigmoid(gates)
|
||||
orig_scores = scores
|
||||
scores = scores + self.e_score_correction_bias
|
||||
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
scores = scores / (mx.sum(scores, axis=-1, keepdims=True) + 1e-20)
|
||||
scores = scores.astype(x.dtype)
|
||||
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
return y
|
||||
|
||||
|
||||
class MiniMaxDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.self_attn = MiniMaxAttention(args)
|
||||
|
||||
self.block_sparse_moe = MiniMaxSparseMoeBlock(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = x + self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
r = r + self.block_sparse_moe(self.post_attention_layernorm(r))
|
||||
return r
|
||||
|
||||
|
||||
class MiniMaxModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [
|
||||
MiniMaxDecoderLayer(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = MiniMaxModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs=inputs, mask=mask, cache=cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
"""Dequantize FP8 weights and restructure MoE experts."""
|
||||
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
pad_side = (-n) % bs
|
||||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||||
weight = weight.reshape(
|
||||
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
|
||||
)
|
||||
weight = (weight * scale_inv[:, None, :, None]).reshape(
|
||||
m + pad_bottom, n + pad_side
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Step 2: Handle MoE expert weights restructuring
|
||||
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
|
||||
return weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
mapping = {"w1": "gate_proj", "w2": "down_proj", "w3": "up_proj"}
|
||||
for orig_name, new_name in mapping.items():
|
||||
if f"{prefix}.block_sparse_moe.experts.0.{orig_name}.weight" in weights:
|
||||
to_join = [
|
||||
weights.pop(
|
||||
f"{prefix}.block_sparse_moe.experts.{e}.{orig_name}.weight"
|
||||
)
|
||||
for e in range(self.args.num_local_experts)
|
||||
]
|
||||
weights[
|
||||
f"{prefix}.block_sparse_moe.switch_mlp.{new_name}.weight"
|
||||
] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("block_sparse_moe.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,264 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
head_dim: Optional[int] = None
|
||||
max_position_embeddings: Optional[int] = None
|
||||
num_key_value_heads: Optional[int] = None
|
||||
rope_parameters: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
layer_types: Optional[List[str]] = None
|
||||
sliding_window: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
|
||||
if self.layer_types is None:
|
||||
self.layer_types = ["full_attention"] * self.num_hidden_layers
|
||||
|
||||
|
||||
def _get_llama_4_attn_scale(
|
||||
start: int, stop: int, beta: float, max_position_embeddings: int
|
||||
):
|
||||
scaling = 1 + beta * mx.log(
|
||||
1 + mx.floor(mx.arange(start, stop) / max_position_embeddings)
|
||||
)
|
||||
return scaling[:, None]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.head_dim or args.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_parameters["rope_theta"],
|
||||
False,
|
||||
args.rope_parameters,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
attn_scale: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
# Prepare the queries, keys and values for the attention computation
|
||||
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
offset = 0
|
||||
if cache is not None:
|
||||
offset = cache.offset
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
queries = queries * attn_scale
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.args = args
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
attn_scale: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), attn_scale, mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
|
||||
for layer_type in self.layer_types
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for e, l in enumerate(self.layers):
|
||||
if l.use_sliding:
|
||||
self.swa_idx = e
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
offset = 0
|
||||
else:
|
||||
offset = cache[0].offset
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
attn_scale = _get_llama_4_attn_scale(
|
||||
offset,
|
||||
offset + inputs.shape[1],
|
||||
self.args.rope_parameters["llama_4_scaling_beta"],
|
||||
self.args.rope_parameters["original_max_position_embeddings"],
|
||||
).astype(h.dtype)
|
||||
|
||||
for layer, cache in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, attn_scale, mask, cache=cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = LanguageModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove unused precomputed rotary freqs
|
||||
weights = {
|
||||
k: v for k, v in weights.items() if "self_attn.rotary_emb.inv_freq" not in k
|
||||
}
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
new_weights[wk] = weight * scale_inv
|
||||
elif "activation_scale" in k:
|
||||
continue
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
@@ -7,7 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import llama
|
||||
from . import llama, ministral3
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class ModelArgs(BaseModelArgs):
|
||||
text_config: dict
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
if "tie_word_embeddings" not in self.text_config:
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
@@ -25,7 +26,14 @@ class Model(nn.Module):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = llama.Model(llama.ModelArgs.from_dict(args.text_config))
|
||||
if args.text_config.get("model_type") == "ministral3":
|
||||
self.language_model = ministral3.Model(
|
||||
ministral3.ModelArgs.from_dict(args.text_config)
|
||||
)
|
||||
else:
|
||||
self.language_model = llama.Model(
|
||||
llama.ModelArgs.from_dict(args.text_config)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -41,6 +49,8 @@ class Model(nn.Module):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights.pop("multi_modal_projector", None)
|
||||
lm_weights = dict(tree_flatten(weights["language_model"]))
|
||||
weights["language_model"] = self.language_model.sanitize(lm_weights)
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
@property
|
||||
|
||||
+11
-2
@@ -7,6 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@@ -76,9 +77,8 @@ class Olmo3Attention(nn.Module):
|
||||
self.k_norm = nn.RMSNorm(
|
||||
args.num_key_value_heads * self.head_dim, eps=args.rms_norm_eps
|
||||
)
|
||||
self.is_full = args.layer_types[layer_idx] == "full_attention"
|
||||
|
||||
if self.is_full:
|
||||
if args.layer_types[layer_idx] != "full_attention":
|
||||
self.rope = nn.RoPE(self.head_dim, traditional=False, base=args.rope_theta)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
@@ -224,3 +224,12 @@ class Model(nn.Module):
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for lt in self.model.layer_types:
|
||||
if lt == "full_attention":
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
|
||||
class PipelineMixin:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
self.start_idx = 0
|
||||
self.end_idx = None
|
||||
|
||||
@property
|
||||
def pipeline_layers(self):
|
||||
return self.layers[self.start_idx : self.end_idx]
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
# Keep the layer numbers the same for model loading
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
@@ -168,9 +168,7 @@ class YarnRoPE(nn.Module):
|
||||
scaling_factor, mscale_all_dim
|
||||
)
|
||||
freq_extra = base ** (mx.arange(0, dims, 2, dtype=mx.float32) / dims)
|
||||
freq_inter = scaling_factor * base ** (
|
||||
mx.arange(0, dims, 2, dtype=mx.float32) / dims
|
||||
)
|
||||
freq_inter = scaling_factor * freq_extra
|
||||
low, high = yarn_find_correction_range()
|
||||
freq_mask = 1.0 - yarn_linear_ramp_mask(low, high, dims // 2)
|
||||
self._freqs = (freq_inter * freq_extra) / (
|
||||
|
||||
@@ -13,8 +13,7 @@ import mlx.nn as nn
|
||||
import numpy as np
|
||||
|
||||
from mlx_lm.tuner.datasets import load_dataset
|
||||
from mlx_lm.tuner.utils import get_total_parameters
|
||||
from mlx_lm.utils import load
|
||||
from mlx_lm.utils import get_total_parameters, load
|
||||
|
||||
|
||||
def load_data(
|
||||
|
||||
+132
-23
@@ -4,6 +4,7 @@ import argparse
|
||||
import copy
|
||||
import time
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
@@ -18,19 +19,62 @@ from mlx_lm.tuner.trainer import grad_checkpoint, iterate_batches
|
||||
from mlx_lm.tuner.utils import print_trainable_parameters
|
||||
from mlx_lm.utils import (
|
||||
load,
|
||||
load_tokenizer,
|
||||
pipeline_load,
|
||||
quantize_model,
|
||||
save,
|
||||
)
|
||||
|
||||
|
||||
def compute_dwq_targets(
|
||||
model,
|
||||
save_dir,
|
||||
train_data,
|
||||
valid_data,
|
||||
batch_size,
|
||||
max_seq_length,
|
||||
seed,
|
||||
):
|
||||
rank = mx.distributed.init().rank()
|
||||
|
||||
def _compute_targets(data, path, split):
|
||||
|
||||
if rank == 0:
|
||||
path = path / split
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
for i, (batch, _) in (
|
||||
pbar := tqdm(
|
||||
enumerate(iterate_batches(data, batch_size, max_seq_length, seed=seed)),
|
||||
total=len(data) // batch_size,
|
||||
desc=f"Computing targets for {split}",
|
||||
disable=rank != 0,
|
||||
)
|
||||
):
|
||||
batch = batch[:, :-1]
|
||||
logits = model(batch)
|
||||
# Hack to make the last op pre-eval on the CPU to avoid even timeout
|
||||
logits = mx.stop_gradient(logits, stream=mx.cpu)
|
||||
mx.eval(logits)
|
||||
if rank == 0:
|
||||
idx = mx.argpartition(logits, kth=-1024, axis=-1)[..., -1024:]
|
||||
logits = mx.take_along_axis(logits, idx, axis=-1)
|
||||
|
||||
file = path / f"{i:010d}.safetensors"
|
||||
mx.save_safetensors(file, {"logits": logits, "indices": idx})
|
||||
|
||||
_compute_targets(valid_data, save_dir, "valid")
|
||||
_compute_targets(train_data, save_dir, "train")
|
||||
|
||||
|
||||
def dwq_quantize(
|
||||
model,
|
||||
q_model,
|
||||
target_fn,
|
||||
opt,
|
||||
train_data,
|
||||
valid_data,
|
||||
batch_size: int = 2,
|
||||
max_seq_length: int = 2048,
|
||||
batch_size,
|
||||
max_seq_length,
|
||||
seed,
|
||||
dtype: mx.Dtype = mx.bfloat16,
|
||||
gradient_checkpoint: bool = False,
|
||||
temperature: float = 2.0,
|
||||
@@ -52,18 +96,21 @@ def dwq_quantize(
|
||||
):
|
||||
m.unfreeze(keys=["scales", "biases"], recurse=False)
|
||||
|
||||
q_model.train()
|
||||
q_model.apply_to_modules(unfreeze)
|
||||
print_trainable_parameters(q_model)
|
||||
model.train()
|
||||
model.apply_to_modules(unfreeze)
|
||||
print_trainable_parameters(model)
|
||||
|
||||
if gradient_checkpoint:
|
||||
grad_checkpoint(q_model.layers[0])
|
||||
grad_checkpoint(model.layers[0])
|
||||
|
||||
scale = 1 / temperature
|
||||
|
||||
def loss_fn(params, x, targets, lengths):
|
||||
q_model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
logits = q_model(x)
|
||||
model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
logits = model(x)
|
||||
if isinstance(targets, tuple):
|
||||
targets, ids = targets
|
||||
logits = mx.take_along_axis(logits, ids, axis=-1)
|
||||
losses = kl_div_loss(scale * logits, scale * targets)
|
||||
mask = mx.arange(1, 1 + targets.shape[1]) < lengths[:, 1:]
|
||||
ntoks = mask.sum()
|
||||
@@ -81,14 +128,16 @@ def dwq_quantize(
|
||||
def validate(params, it):
|
||||
v_loss = 0.0
|
||||
v_tokens = 0
|
||||
for batch, lengths in tqdm(
|
||||
iterate_batches(valid_data, batch_size, max_seq_length),
|
||||
for i, (batch, lengths) in tqdm(
|
||||
enumerate(
|
||||
iterate_batches(valid_data, batch_size, max_seq_length, seed=seed)
|
||||
),
|
||||
total=len(valid_data) // batch_size,
|
||||
desc="Computing validation loss",
|
||||
leave=False,
|
||||
):
|
||||
batch = batch[:, :-1]
|
||||
targets = model(batch)
|
||||
targets = target_fn(batch, i, split="valid")
|
||||
mx.eval(targets)
|
||||
loss, ntoks = loss_fn(params, batch, targets, lengths)
|
||||
mx.eval(loss, ntoks)
|
||||
@@ -103,7 +152,7 @@ def dwq_quantize(
|
||||
# Accumulate learned weights in higher precision
|
||||
params = tree_map(
|
||||
lambda x: x.astype(mx.float32),
|
||||
q_model.trainable_parameters(),
|
||||
model.trainable_parameters(),
|
||||
)
|
||||
|
||||
total_loss = 0.0
|
||||
@@ -117,12 +166,14 @@ def dwq_quantize(
|
||||
|
||||
for it, (batch, lengths) in (
|
||||
pbar := tqdm(
|
||||
enumerate(iterate_batches(train_data, batch_size, max_seq_length)),
|
||||
enumerate(
|
||||
iterate_batches(train_data, batch_size, max_seq_length, seed=seed)
|
||||
),
|
||||
total=len(train_data) // batch_size,
|
||||
)
|
||||
):
|
||||
batch = batch[:, :-1]
|
||||
targets = model(batch)
|
||||
targets = target_fn(batch, it, split="train")
|
||||
mx.eval(targets)
|
||||
loss, ntoks, params = step(batch, targets, lengths, params)
|
||||
mx.eval(loss, params)
|
||||
@@ -155,7 +206,7 @@ def dwq_quantize(
|
||||
" Model quality will likely be degraded.\n❌❌❌"
|
||||
)
|
||||
|
||||
q_model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
|
||||
|
||||
def load_data(
|
||||
@@ -196,10 +247,12 @@ def main():
|
||||
help="A model to distill from for DWQ. If `quantized-model` is not"
|
||||
" given the student model will be this model quantized according"
|
||||
" to `bits` and `group-size`.",
|
||||
type=str,
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quantized-model",
|
||||
type=str,
|
||||
default=None,
|
||||
help="An already quantized model (the student model) to improve with DWQ.",
|
||||
)
|
||||
@@ -236,27 +289,78 @@ def main():
|
||||
action="store_true",
|
||||
help="Use gradient checkpointing to reduce memory use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-dir", type=str, default=None, help="Directory to save/load targets."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--targets-only", action="store_true", help="Compute the targets and exit."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipeline parallel instead of data parallel.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
|
||||
num_samples = args.num_samples
|
||||
if num_samples % group.size() > 0:
|
||||
if not args.pipeline and num_samples % group.size() > 0:
|
||||
num_samples += group.size() - num_samples % group.size()
|
||||
|
||||
np.random.seed(args.seed)
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model, tokenizer, config = load(
|
||||
args.model,
|
||||
lazy=True,
|
||||
return_config=True,
|
||||
)
|
||||
if args.target_dir is not None:
|
||||
target_dir = Path(args.target_dir)
|
||||
has_targets = target_dir.exists()
|
||||
else:
|
||||
has_targets = False
|
||||
target_dir = None
|
||||
|
||||
tokenizer = load_tokenizer(args.model)
|
||||
|
||||
train_data, valid_data = load_data(
|
||||
tokenizer, args.data_path, args.num_samples, args.max_seq_length
|
||||
)
|
||||
|
||||
# Load the base model if we need it
|
||||
if not has_targets or args.quantized_model is None:
|
||||
if args.pipeline and group.size() > 1:
|
||||
model, _, config = pipeline_load(args.model, return_config=True)
|
||||
else:
|
||||
model, _, config = load(args.model, return_config=True, lazy=True)
|
||||
else:
|
||||
model = None
|
||||
|
||||
# Pre-compute the targets
|
||||
if not has_targets and target_dir is not None:
|
||||
compute_dwq_targets(
|
||||
model,
|
||||
target_dir,
|
||||
train_data,
|
||||
valid_data,
|
||||
batch_size=args.batch_size,
|
||||
max_seq_length=args.max_seq_length,
|
||||
seed=args.seed,
|
||||
)
|
||||
has_targets = True
|
||||
|
||||
if args.targets_only:
|
||||
exit(0)
|
||||
|
||||
if has_targets:
|
||||
|
||||
def target_fn(_, idx, split):
|
||||
targets = mx.load(target_dir / split / f"{idx:010d}.safetensors")
|
||||
return targets["logits"], targets["indices"]
|
||||
|
||||
else:
|
||||
|
||||
def target_fn(batch, idx, split):
|
||||
return model(batch)
|
||||
|
||||
if args.quantized_model is not None:
|
||||
q_model, tokenizer, config = load(
|
||||
args.quantized_model,
|
||||
@@ -274,19 +378,24 @@ def main():
|
||||
bits=args.bits,
|
||||
)
|
||||
|
||||
# Delete the base model if it's not needed
|
||||
if has_targets and model is not None:
|
||||
del model
|
||||
|
||||
if mx.metal.is_available():
|
||||
max_rec_size = mx.metal.device_info()["max_recommended_working_set_size"]
|
||||
mx.set_wired_limit(max_rec_size)
|
||||
|
||||
opt = optimizers.Adam(learning_rate=args.learning_rate, bias_correction=True)
|
||||
dwq_quantize(
|
||||
model,
|
||||
q_model,
|
||||
target_fn,
|
||||
opt,
|
||||
train_data,
|
||||
valid_data,
|
||||
batch_size=args.batch_size,
|
||||
max_seq_length=args.max_seq_length,
|
||||
seed=args.seed,
|
||||
gradient_checkpoint=args.grad_checkpoint,
|
||||
)
|
||||
save(
|
||||
|
||||
+738
-263
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
from functools import partial
|
||||
from json import JSONDecodeError
|
||||
from typing import List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from transformers import AutoTokenizer, PreTrainedTokenizerFast
|
||||
|
||||
@@ -210,7 +210,7 @@ class BPEStreamingDetokenizer(StreamingDetokenizer):
|
||||
# For multi-byte utf-8 wait until they are complete
|
||||
# For single spaces wait until the next token to clean it if needed
|
||||
if not text.endswith("\ufffd") and not (
|
||||
len(v) == 1 and self._byte_decoder[v[0]] == 32
|
||||
len(v) == 1 and self._byte_decoder.get(v[0]) == 32
|
||||
):
|
||||
self.text += self._maybe_trim_space(text)
|
||||
self._unflushed = ""
|
||||
@@ -423,9 +423,12 @@ def _is_bpe_decoder(decoder):
|
||||
return isinstance(decoder, dict) and decoder.get("type", None) == "ByteLevel"
|
||||
|
||||
|
||||
def load_tokenizer(
|
||||
model_path, tokenizer_config_extra={}, return_tokenizer=True, eos_token_ids=None
|
||||
):
|
||||
def load(
|
||||
model_path,
|
||||
tokenizer_config_extra: Optional[Dict[str, Any]] = None,
|
||||
return_tokenizer=True,
|
||||
eos_token_ids=None,
|
||||
) -> TokenizerWrapper:
|
||||
"""Load a huggingface tokenizer and try to infer the type of streaming
|
||||
detokenizer to use.
|
||||
|
||||
@@ -435,6 +438,7 @@ def load_tokenizer(
|
||||
detokenizer_class = NaiveStreamingDetokenizer
|
||||
|
||||
tokenizer_file = model_path / "tokenizer.json"
|
||||
|
||||
if tokenizer_file.exists():
|
||||
with open(tokenizer_file, "r", encoding="utf-8") as fid:
|
||||
try:
|
||||
@@ -454,8 +458,9 @@ def load_tokenizer(
|
||||
eos_token_ids = [eos_token_ids]
|
||||
|
||||
if return_tokenizer:
|
||||
kwargs = tokenizer_config_extra or {}
|
||||
return TokenizerWrapper(
|
||||
AutoTokenizer.from_pretrained(model_path, **tokenizer_config_extra),
|
||||
AutoTokenizer.from_pretrained(model_path, **kwargs),
|
||||
detokenizer_class,
|
||||
eos_token_ids=eos_token_ids,
|
||||
)
|
||||
|
||||
+16
-10
@@ -39,7 +39,7 @@ class TextDataset:
|
||||
class ChatDataset:
|
||||
"""
|
||||
A dataset for chat data in the format of {"messages": [...]}
|
||||
https://platform.openai.com/docs/guides/fine-tuning/example-format
|
||||
https://platform.openai.com/docs/guides/supervised-fine-tuning#formatting-your-data
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -59,8 +59,14 @@ class ChatDataset:
|
||||
tools = d.get("tools", None)
|
||||
tokens = self.tokenizer.apply_chat_template(messages, tools=tools)
|
||||
if self.mask_prompt:
|
||||
messages = messages[:-1]
|
||||
offset = len(self.tokenizer.apply_chat_template(messages, tools=tools))
|
||||
add_generation_prompt = messages[-1].get("role") == "assistant"
|
||||
offset = len(
|
||||
self.tokenizer.apply_chat_template(
|
||||
messages[:-1],
|
||||
tools=tools,
|
||||
add_generation_prompt=add_generation_prompt,
|
||||
)
|
||||
)
|
||||
return (tokens, offset)
|
||||
else:
|
||||
return (tokens, 0)
|
||||
@@ -94,16 +100,16 @@ class CompletionsDataset:
|
||||
self.tokenizer = tokenizer
|
||||
|
||||
def process(self, d):
|
||||
tokens = self.tokenizer.apply_chat_template(
|
||||
[
|
||||
{"role": "user", "content": d[self.prompt_key]},
|
||||
{"role": "assistant", "content": d[self.completion_key]},
|
||||
],
|
||||
)
|
||||
tools = d.get("tools", None)
|
||||
messages = [
|
||||
{"role": "user", "content": d[self.prompt_key]},
|
||||
{"role": "assistant", "content": d[self.completion_key]},
|
||||
]
|
||||
tokens = self.tokenizer.apply_chat_template(messages, tools=tools)
|
||||
if self.mask_prompt:
|
||||
offset = len(
|
||||
self.tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": d[self.prompt_key]}]
|
||||
messages[0], tools=tools, add_generation_prompt=True
|
||||
)
|
||||
)
|
||||
return (tokens, offset)
|
||||
|
||||
+15
-19
@@ -29,31 +29,29 @@ class DoRALinear(nn.Module):
|
||||
dora_lin.set_linear(linear)
|
||||
return dora_lin
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
linear = self.linear
|
||||
bias = "bias" in linear
|
||||
weight = self._dequantized_weight()
|
||||
|
||||
# Use the same type as the linear weight
|
||||
dtype = weight.dtype
|
||||
|
||||
output_dims, input_dims = weight.shape
|
||||
fused_linear = nn.Linear(input_dims, output_dims, bias=False)
|
||||
|
||||
lora_b = (self.scale * self.lora_b.T).astype(dtype)
|
||||
lora_a = self.lora_a.T.astype(dtype)
|
||||
weight = weight + lora_b @ lora_a
|
||||
lora_b = self.scale * self.lora_b.T
|
||||
lora_a = self.lora_a.T
|
||||
weight = weight + (lora_b @ lora_a).astype(weight.dtype)
|
||||
norm_scale = self.m / mx.linalg.norm(weight, axis=1)
|
||||
fused_linear.weight = norm_scale[:, None] * weight
|
||||
|
||||
if bias:
|
||||
fused_linear.bias = linear.bias
|
||||
|
||||
if self._is_quantized() and not de_quantize:
|
||||
if self._is_quantized() and not dequantize:
|
||||
fused_linear = nn.QuantizedLinear.from_linear(
|
||||
fused_linear,
|
||||
linear.group_size,
|
||||
linear.bits,
|
||||
group_size=linear.group_size,
|
||||
bits=linear.bits,
|
||||
mode=linear.mode,
|
||||
)
|
||||
return fused_linear
|
||||
|
||||
@@ -101,8 +99,9 @@ class DoRALinear(nn.Module):
|
||||
weight,
|
||||
self.linear.scales,
|
||||
self.linear.biases,
|
||||
self.linear.group_size,
|
||||
self.linear.bits,
|
||||
group_size=self.linear.group_size,
|
||||
bits=self.linear.bits,
|
||||
mode=self.linear.mode,
|
||||
)
|
||||
return weight
|
||||
|
||||
@@ -151,19 +150,16 @@ class DoRAEmbedding(nn.Module):
|
||||
dora_embedding.set_embedding(embedding)
|
||||
return dora_embedding
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
embedding = self.embedding
|
||||
weight = embedding.weight
|
||||
|
||||
# Use the same type as the linear weight if not quantized
|
||||
dtype = weight.dtype
|
||||
|
||||
num_embeddings, dims = weight.shape
|
||||
fused_embedding = nn.Embedding(num_embeddings, dims)
|
||||
|
||||
lora_a = (self.scale * self.lora_a).astype(dtype)
|
||||
lora_b = self.lora_b.astype(dtype)
|
||||
weight = weight + lora_a @ lora_b
|
||||
lora_a = self.scale * self.lora_a
|
||||
lora_b = self.lora_b
|
||||
weight = weight + (lora_a @ lora_b).astype(weight.dtype)
|
||||
norm_scale = self.m / mx.linalg.norm(weight, axis=1)
|
||||
fused_embedding.weight = norm_scale[:, None] * weight
|
||||
|
||||
|
||||
+29
-34
@@ -31,37 +31,35 @@ class LoRALinear(nn.Module):
|
||||
lora_lin.linear = linear
|
||||
return lora_lin
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
linear = self.linear
|
||||
bias = "bias" in linear
|
||||
weight = linear.weight
|
||||
is_quantized = isinstance(linear, nn.QuantizedLinear)
|
||||
|
||||
# Use the same type as the linear weight if not quantized
|
||||
dtype = weight.dtype
|
||||
|
||||
if is_quantized:
|
||||
dtype = linear.scales.dtype
|
||||
weight = mx.dequantize(
|
||||
weight,
|
||||
linear.scales,
|
||||
linear.biases,
|
||||
linear.group_size,
|
||||
linear.bits,
|
||||
group_size=linear.group_size,
|
||||
bits=linear.bits,
|
||||
mode=linear.mode,
|
||||
)
|
||||
output_dims, input_dims = weight.shape
|
||||
fused_linear = nn.Linear(input_dims, output_dims, bias=bias)
|
||||
|
||||
delta = ((self.scale * self.lora_b.T) @ self.lora_a.T).astype(dtype)
|
||||
delta = ((self.scale * self.lora_b.T) @ self.lora_a.T).astype(weight.dtype)
|
||||
fused_linear.weight = weight + delta
|
||||
if bias:
|
||||
fused_linear.bias = linear.bias
|
||||
|
||||
if is_quantized and not de_quantize:
|
||||
if is_quantized and not dequantize:
|
||||
fused_linear = nn.QuantizedLinear.from_linear(
|
||||
fused_linear,
|
||||
linear.group_size,
|
||||
linear.bits,
|
||||
mode=linear.mode,
|
||||
)
|
||||
|
||||
return fused_linear
|
||||
@@ -119,35 +117,34 @@ class LoRASwitchLinear(nn.Module):
|
||||
lora_lin.linear = linear
|
||||
return lora_lin
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
linear = self.linear
|
||||
bias = "bias" in linear
|
||||
weight = linear.weight
|
||||
is_quantized = isinstance(linear, QuantizedSwitchLinear)
|
||||
|
||||
# Use the same type as the linear weight if not quantized
|
||||
dtype = weight.dtype
|
||||
|
||||
if is_quantized:
|
||||
dtype = mx.float16
|
||||
weight = mx.dequantize(
|
||||
weight,
|
||||
linear.scales,
|
||||
linear.biases,
|
||||
linear.group_size,
|
||||
linear.bits,
|
||||
group_size=linear.group_size,
|
||||
bits=linear.bits,
|
||||
mode=linear.mode,
|
||||
)
|
||||
num_experts, output_dims, input_dims = weight.shape
|
||||
fused_linear = SwitchLinear(input_dims, output_dims, num_experts, bias=bias)
|
||||
|
||||
lora_b = (self.scale * self.lora_b).astype(dtype)
|
||||
lora_a = self.lora_a.reshape(num_experts, -1, input_dims).astype(dtype)
|
||||
fused_linear.weight = weight + lora_b @ lora_a
|
||||
lora_b = self.scale * self.lora_b
|
||||
lora_a = self.lora_a.reshape(num_experts, -1, input_dims)
|
||||
fused_linear.weight = weight + (lora_b @ lora_a).astype(weight.dtype)
|
||||
if bias:
|
||||
fused_linear.bias = linear.bias
|
||||
|
||||
if is_quantized and not de_quantize:
|
||||
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
|
||||
if is_quantized and not dequantize:
|
||||
fused_linear = fused_linear.to_quantized(
|
||||
group_size=linear.group_size, bits=linear.bits, mode=linear.mode
|
||||
)
|
||||
|
||||
return fused_linear
|
||||
|
||||
@@ -219,35 +216,33 @@ class LoRAEmbedding(nn.Module):
|
||||
lora_embedding.embedding = embedding
|
||||
return lora_embedding
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
embedding = self.embedding
|
||||
weight = embedding.weight
|
||||
is_quantized = isinstance(embedding, nn.QuantizedEmbedding)
|
||||
|
||||
# Use the same type as the linear weight if not quantized
|
||||
dtype = weight.dtype
|
||||
|
||||
if is_quantized:
|
||||
dtype = embedding.scales.dtype
|
||||
weight = mx.dequantize(
|
||||
weight,
|
||||
embedding.scales,
|
||||
embedding.biases,
|
||||
embedding.group_size,
|
||||
embedding.bits,
|
||||
group_size=embedding.group_size,
|
||||
bits=embedding.bits,
|
||||
mode=embedding.mode,
|
||||
)
|
||||
num_embeddings, dims = weight.shape
|
||||
fused_embedding = nn.Embedding(num_embeddings, dims)
|
||||
|
||||
lora_a = (self.scale * self.lora_a).astype(dtype)
|
||||
lora_b = self.lora_b.astype(dtype)
|
||||
fused_embedding.weight = weight + lora_a @ lora_b
|
||||
lora_a = self.scale * self.lora_a
|
||||
lora_b = self.lora_b
|
||||
fused_embedding.weight = weight + (lora_a @ lora_b).astype(weight.dtype)
|
||||
|
||||
if is_quantized and not de_quantize:
|
||||
if is_quantized and not dequantize:
|
||||
fused_embedding = nn.QuantizedEmbedding.from_embedding(
|
||||
fused_embedding,
|
||||
embedding.group_size,
|
||||
embedding.bits,
|
||||
group_size=embedding.group_size,
|
||||
bits=embedding.bits,
|
||||
mode=embedding.mode,
|
||||
)
|
||||
|
||||
return fused_embedding
|
||||
|
||||
+15
-6
@@ -92,7 +92,9 @@ def iterate_batches(
|
||||
dataset,
|
||||
batch_size,
|
||||
max_seq_length,
|
||||
train=False,
|
||||
loop=False,
|
||||
seed=None,
|
||||
comm_group=None,
|
||||
):
|
||||
# Sort by length:
|
||||
if isinstance(dataset, CacheDataset):
|
||||
@@ -108,8 +110,12 @@ def iterate_batches(
|
||||
|
||||
# If running in distributed mode (N machines) then each one should skip N-1
|
||||
# samples
|
||||
offset = mx.distributed.init().rank()
|
||||
step = mx.distributed.init().size()
|
||||
if comm_group is not None:
|
||||
offset = comm_group.rank()
|
||||
step = comm_group.size()
|
||||
else:
|
||||
offset = 0
|
||||
step = 1
|
||||
if batch_size % step != 0:
|
||||
raise ValueError("The batch size must be divisible by the number of workers")
|
||||
|
||||
@@ -118,7 +124,8 @@ def iterate_batches(
|
||||
idx[i + offset : i + offset + batch_size : step]
|
||||
for i in range(0, len(idx) - batch_size + 1, batch_size)
|
||||
]
|
||||
|
||||
if seed:
|
||||
np.random.seed(seed)
|
||||
while True:
|
||||
indices = np.random.permutation(len(batch_idx))
|
||||
for i in indices:
|
||||
@@ -151,7 +158,7 @@ def iterate_batches(
|
||||
batch = mx.array(batch_arr)
|
||||
yield batch, mx.array(list(zip(offsets, lengths)))
|
||||
|
||||
if not train:
|
||||
if not loop:
|
||||
break
|
||||
|
||||
|
||||
@@ -177,6 +184,7 @@ def evaluate(
|
||||
dataset=dataset,
|
||||
batch_size=batch_size,
|
||||
max_seq_length=max_seq_length,
|
||||
comm_group=mx.distributed.init(),
|
||||
),
|
||||
),
|
||||
desc="Calculating loss...",
|
||||
@@ -254,7 +262,8 @@ def train(
|
||||
dataset=train_dataset,
|
||||
batch_size=args.batch_size,
|
||||
max_seq_length=args.max_seq_length,
|
||||
train=True,
|
||||
loop=True,
|
||||
comm_group=world,
|
||||
),
|
||||
):
|
||||
tic = time.perf_counter()
|
||||
|
||||
+2
-58
@@ -7,9 +7,10 @@ from typing import Dict
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import mlx.optimizers as opt
|
||||
from mlx.utils import tree_flatten, tree_map_with_path, tree_unflatten
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from ..models.switch_layers import QuantizedSwitchLinear, SwitchLinear
|
||||
from ..utils import get_total_parameters
|
||||
from .dora import DoRAEmbedding, DoRALinear
|
||||
from .lora import LoRAEmbedding, LoRALinear, LoRASwitchLinear
|
||||
|
||||
@@ -137,49 +138,6 @@ def load_adapters(model: nn.Module, adapter_path: str) -> nn.Module:
|
||||
return model
|
||||
|
||||
|
||||
def dequantize(model: nn.Module) -> nn.Module:
|
||||
"""
|
||||
Dequantize the quantized linear layers in the model.
|
||||
|
||||
Args:
|
||||
model (nn.Module): The model with quantized linear layers.
|
||||
|
||||
Returns:
|
||||
nn.Module: The model with dequantized layers.
|
||||
"""
|
||||
dequantize_layers = []
|
||||
for name, module in model.named_modules():
|
||||
bias = "bias" in module
|
||||
if isinstance(module, nn.QuantizedLinear):
|
||||
cls = nn.Linear
|
||||
kwargs = {"bias": bias}
|
||||
elif isinstance(module, nn.QuantizedEmbedding):
|
||||
kwargs = {}
|
||||
cls = nn.Embedding
|
||||
elif isinstance(module, QuantizedSwitchLinear):
|
||||
kwargs = {"bias": bias}
|
||||
cls = SwitchLinear
|
||||
else:
|
||||
continue
|
||||
weight = mx.dequantize(
|
||||
module.weight,
|
||||
module.scales,
|
||||
module.biases,
|
||||
module.group_size,
|
||||
module.bits,
|
||||
)
|
||||
args = weight.shape[::-1]
|
||||
m = cls(*args, **kwargs)
|
||||
if bias:
|
||||
m.bias = module.bias
|
||||
m.weight = weight
|
||||
dequantize_layers.append((name, m))
|
||||
|
||||
if len(dequantize_layers) > 0:
|
||||
model.update_modules(tree_unflatten(dequantize_layers))
|
||||
return model
|
||||
|
||||
|
||||
def remove_lora_layers(model: nn.Module) -> nn.Module:
|
||||
"""
|
||||
Remove the LoRA layers from the model.
|
||||
@@ -199,20 +157,6 @@ def remove_lora_layers(model: nn.Module) -> nn.Module:
|
||||
return model
|
||||
|
||||
|
||||
def get_total_parameters(model):
|
||||
leaf_modules = tree_flatten(
|
||||
model.leaf_modules(), is_leaf=lambda m: isinstance(m, nn.Module)
|
||||
)
|
||||
|
||||
def nparams(m):
|
||||
if hasattr(m, "bits"):
|
||||
n = 0 if not hasattr(m, "bias") else m.bias.size
|
||||
return n + m.weight.size * 32 // m.bits
|
||||
return sum(v.size for _, v in tree_flatten(m.parameters()))
|
||||
|
||||
return sum(nparams(m) for _, m in leaf_modules)
|
||||
|
||||
|
||||
def print_trainable_parameters(model):
|
||||
total_p = get_total_parameters(model) / 1e6
|
||||
trainable_p = (
|
||||
|
||||
+180
-22
@@ -7,6 +7,7 @@ import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import resource
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
@@ -14,6 +15,7 @@ from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
@@ -31,13 +33,14 @@ if os.getenv("MLXLM_USE_MODELSCOPE", "False").lower() == "true":
|
||||
else:
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
from mlx.utils import tree_flatten, tree_map, tree_reduce
|
||||
from transformers import PreTrainedTokenizer
|
||||
# For large models with lots of files
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
|
||||
|
||||
from mlx.utils import tree_flatten, tree_map, tree_reduce, tree_unflatten
|
||||
|
||||
# Local imports
|
||||
from .tokenizer_utils import TokenizerWrapper, load_tokenizer
|
||||
from .tuner.utils import dequantize as dequantize_model
|
||||
from .tuner.utils import get_total_parameters, load_adapters
|
||||
from .tokenizer_utils import TokenizerWrapper
|
||||
from .tokenizer_utils import load as _load_tokenizer
|
||||
|
||||
# Constants
|
||||
MODEL_REMAPPING = {
|
||||
@@ -74,6 +77,20 @@ def _get_classes(config: dict):
|
||||
return arch.Model, arch.ModelArgs
|
||||
|
||||
|
||||
def get_total_parameters(model):
|
||||
leaf_modules = tree_flatten(
|
||||
model.leaf_modules(), is_leaf=lambda m: isinstance(m, nn.Module)
|
||||
)
|
||||
|
||||
def nparams(m):
|
||||
if hasattr(m, "bits"):
|
||||
n = 0 if not hasattr(m, "bias") else m.bias.size
|
||||
return n + m.weight.size * 32 // m.bits
|
||||
return sum(v.size for _, v in tree_flatten(m.parameters()))
|
||||
|
||||
return sum(nparams(m) for _, m in leaf_modules)
|
||||
|
||||
|
||||
def compute_bits_per_weight(model):
|
||||
model_bytes = tree_reduce(
|
||||
lambda acc, x: acc + x.nbytes if isinstance(x, mx.array) else acc, model, 0
|
||||
@@ -82,7 +99,11 @@ def compute_bits_per_weight(model):
|
||||
return model_bytes * 8 / model_params
|
||||
|
||||
|
||||
def _download(path_or_hf_repo: str, revision: Optional[str] = None) -> Path:
|
||||
def _download(
|
||||
path_or_hf_repo: str,
|
||||
revision: Optional[str] = None,
|
||||
allow_patterns: List[str] = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Ensures the model is available locally. If the path does not exist locally,
|
||||
it is downloaded from the Hugging Face Hub.
|
||||
@@ -97,21 +118,22 @@ def _download(path_or_hf_repo: str, revision: Optional[str] = None) -> Path:
|
||||
model_path = Path(path_or_hf_repo)
|
||||
|
||||
if not model_path.exists():
|
||||
allow_patterns = allow_patterns or [
|
||||
"*.json",
|
||||
"model*.safetensors",
|
||||
"*.py",
|
||||
"tokenizer.model",
|
||||
"*.tiktoken",
|
||||
"tiktoken.model",
|
||||
"*.txt",
|
||||
"*.jsonl",
|
||||
"*.jinja",
|
||||
]
|
||||
model_path = Path(
|
||||
snapshot_download(
|
||||
path_or_hf_repo,
|
||||
revision=revision,
|
||||
allow_patterns=[
|
||||
"*.json",
|
||||
"model*.safetensors",
|
||||
"*.py",
|
||||
"tokenizer.model",
|
||||
"*.tiktoken",
|
||||
"tiktoken.model",
|
||||
"*.txt",
|
||||
"*.jsonl",
|
||||
"*.jinja",
|
||||
],
|
||||
allow_patterns=allow_patterns,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -136,7 +158,7 @@ def load_model(
|
||||
model_path: Path,
|
||||
lazy: bool = False,
|
||||
strict: bool = True,
|
||||
model_config: dict = {},
|
||||
model_config: Optional[Dict[str, Any]] = None,
|
||||
get_model_classes: Callable[[dict], Tuple[Type[nn.Module], Type]] = _get_classes,
|
||||
) -> Tuple[nn.Module, dict]:
|
||||
"""
|
||||
@@ -163,7 +185,8 @@ def load_model(
|
||||
ValueError: If the model class or args class are not found or cannot be instantiated.
|
||||
"""
|
||||
config = load_config(model_path)
|
||||
config.update(model_config)
|
||||
if model_config is not None:
|
||||
config.update(model_config)
|
||||
|
||||
weight_files = glob.glob(str(model_path / "model*.safetensors"))
|
||||
|
||||
@@ -215,6 +238,11 @@ def load_model(
|
||||
config["quantization"] = quantization
|
||||
config["quantization_config"] = quantization
|
||||
_quantize(quantization)
|
||||
elif quant_method == "compressed-tensors":
|
||||
quantization = {"group_size": 32, "bits": 4, "mode": "affine"}
|
||||
config["quantization"] = quantization
|
||||
config["quantization_config"] = quantization
|
||||
_quantize(quantization)
|
||||
|
||||
model.load_weights(list(weights.items()), strict=strict)
|
||||
|
||||
@@ -225,14 +253,42 @@ def load_model(
|
||||
return model, config
|
||||
|
||||
|
||||
def load_adapters(model: nn.Module, adapter_path: str) -> nn.Module:
|
||||
from .tuner.utils import load_adapters as _load_adapters
|
||||
|
||||
return _load_adapters(model, adapter_path)
|
||||
|
||||
|
||||
def load_tokenizer(model_path, tokenizer_config_extra=None, eos_token_ids=None):
|
||||
"""Load a huggingface tokenizer and try to infer the type of streaming
|
||||
detokenizer to use.
|
||||
"""
|
||||
model_path = _download(
|
||||
model_path,
|
||||
allow_patterns=[
|
||||
"*.json",
|
||||
"*.py",
|
||||
"tokenizer.model",
|
||||
"*.tiktoken",
|
||||
"tiktoken.model",
|
||||
"*.txt",
|
||||
"*.jsonl",
|
||||
"*.jinja",
|
||||
],
|
||||
)
|
||||
return _load_tokenizer(
|
||||
model_path, tokenizer_config_extra, eos_token_ids=eos_token_ids
|
||||
)
|
||||
|
||||
|
||||
def load(
|
||||
path_or_hf_repo: str,
|
||||
tokenizer_config={},
|
||||
model_config={},
|
||||
tokenizer_config: Optional[Dict[str, Any]] = None,
|
||||
model_config: Optional[Dict[str, Any]] = None,
|
||||
adapter_path: Optional[str] = None,
|
||||
lazy: bool = False,
|
||||
return_config: bool = False,
|
||||
revision: str = None,
|
||||
revision: Optional[str] = None,
|
||||
) -> Union[
|
||||
Tuple[nn.Module, TokenizerWrapper],
|
||||
Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]],
|
||||
@@ -277,6 +333,62 @@ def load(
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def pipeline_load(repo, return_config=False):
|
||||
# Get model path with everything but weight safetensors
|
||||
model_path = _download(
|
||||
repo,
|
||||
allow_patterns=[
|
||||
"*.json",
|
||||
"*.py",
|
||||
"tokenizer.model",
|
||||
"*.tiktoken",
|
||||
"tiktoken.model",
|
||||
"*.txt",
|
||||
"*.jsonl",
|
||||
"*.jinja",
|
||||
],
|
||||
)
|
||||
|
||||
# Lazy load and shard model to figure out which weights we need
|
||||
model, config = load_model(model_path, lazy=True, strict=False)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
model.model.pipeline(group)
|
||||
|
||||
# Figure out which files we need for the local shard
|
||||
with open(model_path / "model.safetensors.index.json", "r") as fid:
|
||||
weight_index = json.load(fid)["weight_map"]
|
||||
|
||||
local_files = set()
|
||||
for k, _ in tree_flatten(model.parameters()):
|
||||
if file_name := weight_index.get(k, None) is None:
|
||||
raise ValueError(
|
||||
"Pipeline loading is only supported for MLX converted models."
|
||||
)
|
||||
local_files.add(weight_index[k])
|
||||
|
||||
# Download weights for local shard
|
||||
_download(repo, allow_patterns=local_files)
|
||||
|
||||
# Load and shard the model, and load the weights
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
{"trust_remote_code": True},
|
||||
eos_token_ids=config.get("eos_token_id", None),
|
||||
)
|
||||
model, _ = load_model(model_path, lazy=True, strict=False)
|
||||
model.model.pipeline(group)
|
||||
mx.eval(model.parameters())
|
||||
|
||||
# Synchronize processes to avoid timeout
|
||||
mx.eval(mx.distributed.all_sum(mx.array(1.0), stream=mx.cpu))
|
||||
if return_config:
|
||||
return model, tokenizer, config
|
||||
else:
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def make_shards(weights: dict, max_file_size_gb: int = MAX_FILE_SIZE_GB) -> list:
|
||||
"""
|
||||
Splits the weights into smaller shards.
|
||||
@@ -520,6 +632,52 @@ def quantize_model(
|
||||
return model, quantized_config
|
||||
|
||||
|
||||
def dequantize_model(model: nn.Module) -> nn.Module:
|
||||
"""
|
||||
Dequantize the quantized layers in the model.
|
||||
|
||||
Args:
|
||||
model (nn.Module): The model with quantized layers.
|
||||
|
||||
Returns:
|
||||
nn.Module: The model with dequantized layers.
|
||||
"""
|
||||
from .models.switch_layers import QuantizedSwitchLinear, SwitchLinear
|
||||
|
||||
dequantize_layers = []
|
||||
for name, module in model.named_modules():
|
||||
bias = "bias" in module
|
||||
if isinstance(module, nn.QuantizedLinear):
|
||||
cls = nn.Linear
|
||||
kwargs = {"bias": bias}
|
||||
elif isinstance(module, nn.QuantizedEmbedding):
|
||||
kwargs = {}
|
||||
cls = nn.Embedding
|
||||
elif isinstance(module, QuantizedSwitchLinear):
|
||||
kwargs = {"bias": bias}
|
||||
cls = SwitchLinear
|
||||
else:
|
||||
continue
|
||||
weight = mx.dequantize(
|
||||
module.weight,
|
||||
module.scales,
|
||||
module.biases,
|
||||
module.group_size,
|
||||
module.bits,
|
||||
module.mode,
|
||||
)
|
||||
args = weight.shape[::-1]
|
||||
m = cls(*args, **kwargs)
|
||||
if bias:
|
||||
m.bias = module.bias
|
||||
m.weight = weight
|
||||
dequantize_layers.append((name, m))
|
||||
|
||||
if len(dequantize_layers) > 0:
|
||||
model.update_modules(tree_unflatten(dequantize_layers))
|
||||
return model
|
||||
|
||||
|
||||
def save_config(
|
||||
config: dict,
|
||||
config_path: Union[str, Path],
|
||||
|
||||
@@ -27,6 +27,7 @@ setup(
|
||||
f"mlx>={MIN_MLX_VERSION}; platform_system == 'Darwin'",
|
||||
"numpy",
|
||||
"transformers>=4.39.3",
|
||||
"sentencepiece",
|
||||
"protobuf",
|
||||
"pyyaml",
|
||||
"jinja2",
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from io import StringIO
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import ANY, MagicMock
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
@@ -123,7 +123,7 @@ class TestLora(unittest.TestCase):
|
||||
embedding.bits,
|
||||
)
|
||||
lora_emb = LoRAEmbedding.from_base(embedding, r=8, dropout=0, scale=10)
|
||||
new_embedding = lora_emb.fuse(de_quantize=True)
|
||||
new_embedding = lora_emb.fuse(dequantize=True)
|
||||
self.assertTrue(mx.array_equal(dequantized_weight, new_embedding.weight))
|
||||
self.assertTrue(mx.array_equal(embedding(tokens), lora_emb(tokens)))
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestLora(unittest.TestCase):
|
||||
|
||||
# change the value of lora_b and the embeddings will no longer be equal
|
||||
lora_emb.lora_b = mx.random.uniform(shape=lora_emb.lora_b.shape)
|
||||
new_embedding = lora_emb.fuse(de_quantize=True)
|
||||
new_embedding = lora_emb.fuse(dequantize=True)
|
||||
self.assertFalse(mx.array_equal(dequantized_weight, new_embedding.weight))
|
||||
self.assertFalse(mx.array_equal(embedding(tokens), lora_emb(tokens)))
|
||||
|
||||
@@ -300,7 +300,7 @@ class TestDora(unittest.TestCase):
|
||||
quantized_linear = nn.QuantizedLinear(in_dims, out_dims, bias=True)
|
||||
dora_quantized_linear = DoRALinear.from_base(quantized_linear, r)
|
||||
# Dequantize
|
||||
to_linear_from_quantized = dora_quantized_linear.fuse(de_quantize=True)
|
||||
to_linear_from_quantized = dora_quantized_linear.fuse(dequantize=True)
|
||||
self.assertTrue(
|
||||
mx.allclose(quantized_linear.bias, to_linear_from_quantized.bias)
|
||||
)
|
||||
@@ -405,6 +405,7 @@ class TestScheduleConfig(unittest.TestCase):
|
||||
dataset=mock_dataset,
|
||||
batch_size=2,
|
||||
max_seq_length=2048,
|
||||
comm_group=ANY,
|
||||
)
|
||||
self.assertEqual(mock_default_loss.call_count, 2)
|
||||
|
||||
@@ -441,6 +442,7 @@ class TestScheduleConfig(unittest.TestCase):
|
||||
dataset=mock_dataset,
|
||||
batch_size=2,
|
||||
max_seq_length=2048,
|
||||
comm_group=ANY,
|
||||
)
|
||||
self.assertEqual(mock_default_loss.call_count, 3)
|
||||
|
||||
|
||||
@@ -352,6 +352,86 @@ class TestGenerate(unittest.TestCase):
|
||||
|
||||
del self.model.make_cache
|
||||
|
||||
def test_batch_continued_generation(self):
|
||||
for rotating in [False, True]:
|
||||
if rotating:
|
||||
self.model.make_cache = lambda: [
|
||||
RotatingKVCache(max_size=4) for _ in self.model.layers
|
||||
]
|
||||
|
||||
# Make the prompts
|
||||
prompts_a = [
|
||||
"Write a story about Einstein",
|
||||
"Hi",
|
||||
"What time is it?",
|
||||
"How tall is Mt Everest?",
|
||||
]
|
||||
prompts_a = [
|
||||
self.tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts_a
|
||||
]
|
||||
prompts_b = [
|
||||
"Another one",
|
||||
"sup?",
|
||||
"And how about the date?",
|
||||
"Mt Olympus?",
|
||||
]
|
||||
prompts_b = [
|
||||
self.tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts_b
|
||||
]
|
||||
|
||||
# Generate once
|
||||
batch_gen = BatchGenerator(
|
||||
self.model,
|
||||
stop_tokens=self.tokenizer.eos_token_ids,
|
||||
max_tokens=10,
|
||||
prefill_batch_size=1,
|
||||
prefill_step_size=8,
|
||||
completion_batch_size=2,
|
||||
)
|
||||
uids = batch_gen.insert(prompts_a)
|
||||
caches = {uid: None for uid in uids}
|
||||
while responses := batch_gen.next():
|
||||
for r in responses:
|
||||
if r.finish_reason is not None:
|
||||
caches[r.uid] = r.prompt_cache
|
||||
caches = [caches[uid] for uid in uids]
|
||||
|
||||
# Generate the 2nd time
|
||||
uids = batch_gen.insert(prompts_b, caches=caches)
|
||||
batch_responses = {uid: [] for uid in uids}
|
||||
while responses := batch_gen.next():
|
||||
for r in responses:
|
||||
batch_responses[r.uid].append(r.logprobs)
|
||||
|
||||
for e, uid in enumerate(uids):
|
||||
for i, response in enumerate(
|
||||
stream_generate(
|
||||
self.model,
|
||||
self.tokenizer,
|
||||
prompts_b[e],
|
||||
max_tokens=10,
|
||||
prompt_cache=caches[e],
|
||||
)
|
||||
):
|
||||
batch_logprobs = batch_responses[uid][i]
|
||||
logprobs = response.logprobs
|
||||
self.assertTrue(
|
||||
mx.allclose(batch_logprobs, logprobs, rtol=1e-4, atol=1e-4)
|
||||
)
|
||||
|
||||
if rotating:
|
||||
del self.model.make_cache
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -11,6 +11,7 @@ class TestLosses(unittest.TestCase):
|
||||
|
||||
def test_kl_div_loss(self):
|
||||
self.assertTrue(can_run_metal())
|
||||
mx.random.seed(0)
|
||||
|
||||
logits_q = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
logits_p = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
@@ -23,6 +24,7 @@ class TestLosses(unittest.TestCase):
|
||||
|
||||
def test_js_div_loss(self):
|
||||
self.assertTrue(can_run_metal())
|
||||
mx.random.seed(0)
|
||||
|
||||
logits_q = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
logits_p = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
@@ -35,6 +37,7 @@ class TestLosses(unittest.TestCase):
|
||||
|
||||
def test_kl_div_loss_vjp(self):
|
||||
self.assertTrue(can_run_metal())
|
||||
mx.random.seed(0)
|
||||
|
||||
logits_q = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
logits_p = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
@@ -48,6 +51,7 @@ class TestLosses(unittest.TestCase):
|
||||
|
||||
def test_js_div_loss_vjp(self):
|
||||
self.assertTrue(can_run_metal())
|
||||
mx.random.seed(0)
|
||||
|
||||
logits_q = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
logits_p = mx.random.uniform(shape=(4, 8, 4000), dtype=mx.float32)
|
||||
|
||||
@@ -1939,6 +1939,112 @@ class TestModels(unittest.TestCase):
|
||||
"vocab_size": 32,
|
||||
"intermediate_size": 128,
|
||||
},
|
||||
{
|
||||
"model_type": "minimax",
|
||||
"hidden_size": 128,
|
||||
"intermediate_size": 128,
|
||||
"num_attention_heads": 8,
|
||||
"num_key_value_heads": 8,
|
||||
"max_position_embeddings": 1000,
|
||||
"num_experts_per_tok": 2,
|
||||
"num_local_experts": 8,
|
||||
"shared_intermediate_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"rms_norm_eps": 1e-4,
|
||||
"rope_theta": 1000,
|
||||
"rotary_dim": 16,
|
||||
"vocab_size": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "bailing_moe_linear",
|
||||
"hidden_size": 1024,
|
||||
"num_hidden_layers": 4,
|
||||
"intermediate_size": 2048,
|
||||
"moe_intermediate_size": 1024,
|
||||
"num_experts_per_tok": 2,
|
||||
"num_experts": 4,
|
||||
"norm_topk_prob": True,
|
||||
"num_shared_experts": 2,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 4,
|
||||
"rms_norm_eps": 1e-5,
|
||||
"vocab_size": 10_000,
|
||||
"rope_theta": 1000,
|
||||
"first_k_dense_replace": 0,
|
||||
"layer_group_size": 2,
|
||||
"group_norm_size": 1,
|
||||
"max_position_embeddings": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "kimi_linear",
|
||||
"vocab_size": 1000,
|
||||
"hidden_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"num_attention_heads": 8,
|
||||
"num_key_value_heads": 4,
|
||||
"intermediate_size": 128,
|
||||
"head_dim": 32,
|
||||
"rope_theta": 100.0,
|
||||
"rms_norm_eps": 1e-6,
|
||||
"linear_attn_config": {
|
||||
"num_heads": 8,
|
||||
"head_dim": 32,
|
||||
"kda_layers": [1],
|
||||
},
|
||||
"model_max_length": 1000,
|
||||
"num_experts": 2,
|
||||
"moe_intermediate_size": 128,
|
||||
"kv_lora_rank": 8,
|
||||
},
|
||||
{
|
||||
"model_type": "afmoe",
|
||||
"vocab_size": 1000,
|
||||
"hidden_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"num_attention_heads": 8,
|
||||
"num_key_value_heads": 4,
|
||||
"intermediate_size": 128,
|
||||
"head_dim": 32,
|
||||
"rope_theta": 100.0,
|
||||
"layer_types": [
|
||||
"full_attention",
|
||||
"sliding_attention",
|
||||
"sliding_attention",
|
||||
"full_attention",
|
||||
],
|
||||
"num_experts": 4,
|
||||
"num_experts_per_tok": 2,
|
||||
"moe_intermediate_size": 128,
|
||||
},
|
||||
{
|
||||
"model_type": "deepseek_v32",
|
||||
"vocab_size": 1024,
|
||||
"hidden_size": 128,
|
||||
"intermediate_size": 256,
|
||||
"moe_intermediate_size": 256,
|
||||
"num_hidden_layers": 4,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 2,
|
||||
"n_routed_experts": 4,
|
||||
"n_group": 2,
|
||||
"topk_group": 1,
|
||||
"num_experts_per_tok": 2,
|
||||
"n_shared_experts": 1,
|
||||
"kv_lora_rank": 4,
|
||||
"q_lora_rank": 4,
|
||||
"qk_rope_head_dim": 32,
|
||||
"v_head_dim": 16,
|
||||
"qk_nope_head_dim": 32,
|
||||
"rope_scaling": {
|
||||
"beta_fast": 32,
|
||||
"beta_slow": 1,
|
||||
"factor": 40,
|
||||
"mscale": 1.0,
|
||||
"mscale_all_dim": 1.0,
|
||||
"original_max_position_embeddings": 4096,
|
||||
"type": "yarn",
|
||||
},
|
||||
},
|
||||
]
|
||||
for config in test_configs:
|
||||
model_type = config["model_type"]
|
||||
|
||||
+88
-181
@@ -6,9 +6,11 @@ import json
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
import mlx.core as mx
|
||||
import requests
|
||||
|
||||
from mlx_lm.server import APIHandler
|
||||
from mlx_lm.models.cache import KVCache
|
||||
from mlx_lm.server import APIHandler, LRUPromptCache, ResponseGenerator
|
||||
from mlx_lm.utils import load
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class DummyModelProvider:
|
||||
"chat_template": None,
|
||||
"use_default_chat_template": False,
|
||||
"trust_remote_code": False,
|
||||
"draft_model": None,
|
||||
"num_draft_tokens": 3,
|
||||
"temp": 0.0,
|
||||
"top_p": 1.0,
|
||||
@@ -43,6 +46,7 @@ class DummyModelProvider:
|
||||
# Use the same model as the draft model for testing
|
||||
self.draft_model, _ = load(HF_MODEL_PATH)
|
||||
self.draft_model_key = HF_MODEL_PATH
|
||||
self.cli_args.draft_model = HF_MODEL_PATH
|
||||
|
||||
def load(self, model, adapter=None, draft_model=None):
|
||||
assert model in ["default_model", "chat_model"]
|
||||
@@ -52,11 +56,13 @@ class DummyModelProvider:
|
||||
class TestServer(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model_provider = DummyModelProvider()
|
||||
cls.response_generator = ResponseGenerator(
|
||||
DummyModelProvider(), LRUPromptCache()
|
||||
)
|
||||
cls.server_address = ("localhost", 0)
|
||||
cls.httpd = http.server.HTTPServer(
|
||||
cls.server_address,
|
||||
lambda *args, **kwargs: APIHandler(cls.model_provider, *args, **kwargs),
|
||||
lambda *args, **kwargs: APIHandler(cls.response_generator, *args, **kwargs),
|
||||
)
|
||||
cls.port = cls.httpd.server_port
|
||||
cls.server_thread = threading.Thread(target=cls.httpd.serve_forever)
|
||||
@@ -68,6 +74,7 @@ class TestServer(unittest.TestCase):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
cls.server_thread.join()
|
||||
cls.response_generator.stop_and_join()
|
||||
|
||||
def test_handle_completions(self):
|
||||
url = f"http://localhost:{self.port}/v1/completions"
|
||||
@@ -198,11 +205,13 @@ class TestServer(unittest.TestCase):
|
||||
class TestServerWithDraftModel(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model_provider = DummyModelProvider(with_draft=True)
|
||||
cls.response_generator = ResponseGenerator(
|
||||
DummyModelProvider(with_draft=True), LRUPromptCache()
|
||||
)
|
||||
cls.server_address = ("localhost", 0)
|
||||
cls.httpd = http.server.HTTPServer(
|
||||
cls.server_address,
|
||||
lambda *args, **kwargs: APIHandler(cls.model_provider, *args, **kwargs),
|
||||
lambda *args, **kwargs: APIHandler(cls.response_generator, *args, **kwargs),
|
||||
)
|
||||
cls.port = cls.httpd.server_port
|
||||
cls.server_thread = threading.Thread(target=cls.httpd.serve_forever)
|
||||
@@ -214,6 +223,7 @@ class TestServerWithDraftModel(unittest.TestCase):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
cls.server_thread.join()
|
||||
cls.response_generator.stop_and_join()
|
||||
|
||||
def test_handle_completions_with_draft_model(self):
|
||||
url = f"http://localhost:{self.port}/v1/completions"
|
||||
@@ -339,182 +349,6 @@ class TestServerWithDraftModel(unittest.TestCase):
|
||||
self.assertIsNotNone(second_response_body["choices"][0]["message"]["content"])
|
||||
|
||||
|
||||
# --- Tests for get_prompt_cache ---
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from mlx_lm.server import PromptCache
|
||||
|
||||
|
||||
class TestGetPromptCache(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up mocks and a handler instance for each test."""
|
||||
self.mock_model_provider = MagicMock()
|
||||
# Simulate tokenizer needed for decoding in original debug logs (though not strictly needed for cache logic)
|
||||
self.mock_model_provider.tokenizer = MagicMock()
|
||||
self.mock_model_provider.tokenizer.decode = lambda x: f"decoded({x})"
|
||||
self.mock_model_provider.model_key = ("model_v1", None, None)
|
||||
self.mock_model_provider.draft_model = None # Start without draft model
|
||||
|
||||
# --- Prevent BaseHTTPRequestHandler.__init__ from running ---
|
||||
# It tries to handle a request immediately, which fails with mocks.
|
||||
# We only need the APIHandler instance with its attributes set.
|
||||
with patch(
|
||||
"http.server.BaseHTTPRequestHandler.__init__", lambda *args, **kwargs: None
|
||||
):
|
||||
# APIHandler init still requires args for BaseHTTPRequestHandler signature,
|
||||
# but they won't be used by the patched __init__.
|
||||
mock_request = MagicMock()
|
||||
mock_client_address = ("127.0.0.1", 8080)
|
||||
mock_server = MagicMock()
|
||||
|
||||
self.prompt_cache_instance = PromptCache()
|
||||
self.handler = APIHandler(
|
||||
self.mock_model_provider,
|
||||
mock_request,
|
||||
mock_client_address,
|
||||
mock_server,
|
||||
prompt_cache=self.prompt_cache_instance, # Inject our cache instance
|
||||
)
|
||||
# Manually set attributes usually set by APIHandler.__init__ if needed
|
||||
# self.handler.created = MagicMock()
|
||||
# self.handler.system_fingerprint = MagicMock()
|
||||
# (Not strictly necessary for get_prompt_cache testing)
|
||||
|
||||
@patch("mlx_lm.server.make_prompt_cache")
|
||||
def test_initial_request_empty_cache(self, mock_make_cache):
|
||||
"""Test first request when the cache is empty."""
|
||||
mock_make_cache.return_value = "new_cache_obj"
|
||||
prompt = [1, 2, 3]
|
||||
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
self.assertEqual(processed_prompt, [1, 2, 3])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3])
|
||||
self.assertEqual(self.handler.prompt_cache.cache, "new_cache_obj")
|
||||
self.assertEqual(self.handler.prompt_cache.model_key, ("model_v1", None, None))
|
||||
mock_make_cache.assert_called_once()
|
||||
|
||||
@patch("mlx_lm.server.trim_prompt_cache")
|
||||
@patch("mlx_lm.server.can_trim_prompt_cache", return_value=True)
|
||||
def test_identical_request_full_hit(self, mock_can_trim, mock_trim_cache):
|
||||
"""Test when the new prompt is identical to the cached one."""
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None)
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
prompt = [1, 2, 3]
|
||||
|
||||
# Mock common_prefix_len to return the full length
|
||||
with patch("mlx_lm.server.common_prefix_len", return_value=3):
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
mock_trim_cache.assert_called_once_with("existing_cache_obj", 1)
|
||||
self.assertEqual(processed_prompt, [3])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3])
|
||||
|
||||
def test_cache_is_prefix(self):
|
||||
"""Test when the cached prompt is a prefix of the new prompt."""
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None)
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
prompt = [1, 2, 3, 4, 5]
|
||||
|
||||
with patch("mlx_lm.server.common_prefix_len", return_value=3):
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
# Should process the suffix, cache tokens updated
|
||||
self.assertEqual(processed_prompt, [4, 5])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3, 4, 5])
|
||||
self.assertEqual(self.handler.prompt_cache.cache, "existing_cache_obj")
|
||||
|
||||
@patch("mlx_lm.server.trim_prompt_cache")
|
||||
@patch("mlx_lm.server.can_trim_prompt_cache", return_value=True)
|
||||
def test_partial_match_trim_success(self, mock_can_trim, mock_trim_cache):
|
||||
"""Test partial match where cache is longer and trimming succeeds."""
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3, 4, 5]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None)
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
prompt = [1, 2, 3, 6, 7] # Diverges after token 3
|
||||
|
||||
with patch("mlx_lm.server.common_prefix_len", return_value=3):
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
# Should process the new suffix, cache trimmed and updated
|
||||
self.assertEqual(processed_prompt, [6, 7])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3, 6, 7])
|
||||
mock_can_trim.assert_called_once_with("existing_cache_obj")
|
||||
# Called with cache object and num_to_trim (5 - 3 = 2)
|
||||
mock_trim_cache.assert_called_once_with("existing_cache_obj", 2)
|
||||
self.assertEqual(
|
||||
self.handler.prompt_cache.cache, "existing_cache_obj"
|
||||
) # Cache obj itself isn't changed by mock
|
||||
|
||||
@patch("mlx_lm.server.make_prompt_cache")
|
||||
@patch("mlx_lm.server.trim_prompt_cache")
|
||||
@patch("mlx_lm.server.can_trim_prompt_cache", return_value=False)
|
||||
def test_partial_match_trim_fail(
|
||||
self, mock_can_trim, mock_trim_cache, mock_make_cache
|
||||
):
|
||||
"""Test partial match where cache is longer but trimming fails."""
|
||||
mock_make_cache.return_value = "new_cache_obj_on_reset"
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3, 4, 5]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None)
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
prompt = [1, 2, 3, 6, 7] # Diverges after token 3
|
||||
|
||||
with patch("mlx_lm.server.common_prefix_len", return_value=3):
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
# Should process the full prompt, cache reset
|
||||
self.assertEqual(processed_prompt, [1, 2, 3, 6, 7])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3, 6, 7])
|
||||
mock_can_trim.assert_called_once_with("existing_cache_obj")
|
||||
mock_trim_cache.assert_not_called()
|
||||
mock_make_cache.assert_called_once() # Cache was reset
|
||||
self.assertEqual(self.handler.prompt_cache.cache, "new_cache_obj_on_reset")
|
||||
|
||||
@patch("mlx_lm.server.make_prompt_cache")
|
||||
def test_no_common_prefix(self, mock_make_cache):
|
||||
"""Test when there is no common prefix between cache and prompt."""
|
||||
mock_make_cache.return_value = "new_cache_obj"
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None)
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
prompt = [4, 5, 6]
|
||||
|
||||
with patch("mlx_lm.server.common_prefix_len", return_value=0):
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
# Should process the full prompt, cache reset
|
||||
self.assertEqual(processed_prompt, [4, 5, 6])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [4, 5, 6])
|
||||
mock_make_cache.assert_called_once()
|
||||
self.assertEqual(self.handler.prompt_cache.cache, "new_cache_obj")
|
||||
|
||||
@patch("mlx_lm.server.make_prompt_cache")
|
||||
def test_model_changed(self, mock_make_cache):
|
||||
"""Test cache reset when the model key changes."""
|
||||
mock_make_cache.return_value = "new_cache_obj_model_change"
|
||||
self.handler.prompt_cache.tokens = [1, 2, 3]
|
||||
self.handler.prompt_cache.model_key = ("model_v1", None, None) # Original key
|
||||
self.handler.prompt_cache.cache = "existing_cache_obj"
|
||||
|
||||
# Simulate model provider having a new key
|
||||
self.mock_model_provider.model_key = ("model_v2", None, None)
|
||||
prompt = [1, 2, 3, 4]
|
||||
|
||||
# No need to mock common_prefix_len, model check happens first
|
||||
processed_prompt = self.handler.get_prompt_cache(prompt)
|
||||
|
||||
# Should process the full prompt, cache reset
|
||||
self.assertEqual(processed_prompt, [1, 2, 3, 4])
|
||||
self.assertEqual(self.handler.prompt_cache.tokens, [1, 2, 3, 4])
|
||||
mock_make_cache.assert_called_once()
|
||||
self.assertEqual(self.handler.prompt_cache.cache, "new_cache_obj_model_change")
|
||||
self.assertEqual(self.handler.prompt_cache.model_key, ("model_v2", None, None))
|
||||
|
||||
|
||||
class TestKeepalive(unittest.TestCase):
|
||||
|
||||
def test_keepalive_callback(self):
|
||||
@@ -565,5 +399,78 @@ class TestKeepalive(unittest.TestCase):
|
||||
self.fail(f"Callback should handle BrokenPipeError: {e}")
|
||||
|
||||
|
||||
class TestLRUPromptCache(unittest.TestCase):
|
||||
|
||||
def test_caching(self):
|
||||
cache = LRUPromptCache(max_size=10)
|
||||
|
||||
def get_kv(n):
|
||||
keys = mx.arange(n).reshape(1, 1, n, 1)
|
||||
return keys, keys
|
||||
|
||||
model = ("test", None, None)
|
||||
tokens = [10] * 24
|
||||
|
||||
c, t = cache.fetch_nearest_cache(model, tokens)
|
||||
self.assertTrue(c is None)
|
||||
self.assertEqual(t, tokens)
|
||||
|
||||
c = [KVCache()]
|
||||
c[0].update_and_fetch(*get_kv(24))
|
||||
cache.insert_cache(model, t, c)
|
||||
|
||||
tokens = tokens + [20] * 5
|
||||
c, t = cache.fetch_nearest_cache(model, tokens)
|
||||
k, v = c[0].state
|
||||
self.assertTrue((k == v).all().item())
|
||||
self.assertTrue((k.flatten() == mx.arange(24)).all().item())
|
||||
self.assertEqual(t, [20] * 5)
|
||||
self.assertEqual(len(cache._lru), 0)
|
||||
|
||||
tokens = tokens + [30] * 3
|
||||
c[0].update_and_fetch(*get_kv(8))
|
||||
cache.insert_cache(model, tokens, c)
|
||||
|
||||
tokens = tokens[:26] + [40] * 8
|
||||
c, t = cache.fetch_nearest_cache(model, tokens)
|
||||
k, v = c[0].state
|
||||
self.assertTrue((k == v).all().item())
|
||||
self.assertTrue(
|
||||
(k.flatten() == mx.concatenate([mx.arange(24), mx.arange(2)])).all().item()
|
||||
)
|
||||
self.assertEqual(t, [40] * 8)
|
||||
self.assertEqual(len(cache._lru), 1)
|
||||
|
||||
def test_lru(self):
|
||||
cache = LRUPromptCache(max_size=2)
|
||||
model = ("test", None, None)
|
||||
cache.insert_cache(model, [1, 2], ["test1"])
|
||||
cache.insert_cache(model, [1, 2], ["test1"])
|
||||
|
||||
c, t = cache.fetch_nearest_cache(model, [1, 2])
|
||||
self.assertEqual(c, ["test1"])
|
||||
self.assertEqual(t, [])
|
||||
c, t = cache.fetch_nearest_cache(model, [1, 2])
|
||||
self.assertEqual(c, ["test1"])
|
||||
self.assertEqual(t, [])
|
||||
c, t = cache.fetch_nearest_cache(model, [1, 2])
|
||||
self.assertEqual(c, None)
|
||||
self.assertEqual(t, [1, 2])
|
||||
|
||||
cache.insert_cache(model, [1, 2], ["test1"])
|
||||
cache.insert_cache(model, [2, 3], ["test2"])
|
||||
cache.insert_cache(model, [3, 4], ["test3"])
|
||||
|
||||
c, t = cache.fetch_nearest_cache(model, [1, 2])
|
||||
self.assertEqual(c, None)
|
||||
self.assertEqual(t, [1, 2])
|
||||
c, t = cache.fetch_nearest_cache(model, [2, 3])
|
||||
self.assertEqual(c, ["test2"])
|
||||
self.assertEqual(t, [])
|
||||
c, t = cache.fetch_nearest_cache(model, [3, 4])
|
||||
self.assertEqual(c, ["test3"])
|
||||
self.assertEqual(t, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -9,27 +9,12 @@ from mlx_lm.tokenizer_utils import (
|
||||
BPEStreamingDetokenizer,
|
||||
NaiveStreamingDetokenizer,
|
||||
SPMStreamingDetokenizer,
|
||||
load_tokenizer,
|
||||
)
|
||||
from mlx_lm.utils import load_tokenizer
|
||||
|
||||
|
||||
class TestTokenizers(unittest.TestCase):
|
||||
|
||||
def download_tokenizer(self, repo):
|
||||
path = Path(
|
||||
snapshot_download(
|
||||
repo_id=repo,
|
||||
allow_patterns=[
|
||||
"tokenizer.json",
|
||||
"tokenizer_config.json",
|
||||
"special_tokens_map.json",
|
||||
"tokenizer.model",
|
||||
"chat_template.jinja",
|
||||
],
|
||||
)
|
||||
)
|
||||
return load_tokenizer(path)
|
||||
|
||||
def check_tokenizer(self, tokenizer):
|
||||
def check(tokens):
|
||||
expected_text = tokenizer.decode(tokens)
|
||||
@@ -77,19 +62,19 @@ class TestTokenizers(unittest.TestCase):
|
||||
]
|
||||
for tokenizer_repo, expected_detokenizer in tokenizer_repos:
|
||||
with self.subTest(tokenizer=tokenizer_repo):
|
||||
tokenizer = self.download_tokenizer(tokenizer_repo)
|
||||
tokenizer = load_tokenizer(tokenizer_repo)
|
||||
tokenizer.decode([0, 1, 2])
|
||||
self.assertTrue(isinstance(tokenizer.detokenizer, expected_detokenizer))
|
||||
self.check_tokenizer(tokenizer)
|
||||
|
||||
# Try one with a naive detokenizer
|
||||
tokenizer = self.download_tokenizer("mlx-community/Llama-3.2-1B-Instruct-4bit")
|
||||
tokenizer = load_tokenizer("mlx-community/Llama-3.2-1B-Instruct-4bit")
|
||||
tokenizer._detokenizer = NaiveStreamingDetokenizer(tokenizer)
|
||||
self.check_tokenizer(tokenizer)
|
||||
|
||||
def test_special_tokens(self):
|
||||
tokenizer_repo = "mlx-community/DeepSeek-Coder-V2-Lite-Instruct-4bit-mlx"
|
||||
tokenizer = self.download_tokenizer(tokenizer_repo)
|
||||
tokenizer = load_tokenizer(tokenizer_repo)
|
||||
|
||||
detokenizer = tokenizer.detokenizer
|
||||
detokenizer.reset()
|
||||
@@ -100,18 +85,18 @@ class TestTokenizers(unittest.TestCase):
|
||||
|
||||
def test_tool_calling(self):
|
||||
tokenizer_repo = "mlx-community/Qwen3-4B-4bit"
|
||||
tokenizer = self.download_tokenizer(tokenizer_repo)
|
||||
tokenizer = load_tokenizer(tokenizer_repo)
|
||||
self.assertTrue(tokenizer.has_tool_calling)
|
||||
self.assertEqual(tokenizer.tool_call_start, "<tool_call>")
|
||||
self.assertEqual(tokenizer.tool_call_end, "</tool_call>")
|
||||
|
||||
tokenizer_repo = "mlx-community/Llama-3.2-1B-Instruct-4bit"
|
||||
tokenizer = self.download_tokenizer(tokenizer_repo)
|
||||
tokenizer = load_tokenizer(tokenizer_repo)
|
||||
self.assertFalse(tokenizer.has_tool_calling)
|
||||
|
||||
def test_thinking(self):
|
||||
tokenizer_repo = "mlx-community/Qwen3-4B-4bit"
|
||||
tokenizer = self.download_tokenizer(tokenizer_repo)
|
||||
tokenizer = load_tokenizer(tokenizer_repo)
|
||||
self.assertTrue(tokenizer.has_thinking)
|
||||
self.assertEqual(tokenizer.think_start, "<think>")
|
||||
self.assertEqual(tokenizer.think_end, "</think>")
|
||||
|
||||
+22
-34
@@ -19,47 +19,35 @@ class MockDistributedGroup:
|
||||
return self._size
|
||||
|
||||
|
||||
class MockDistributed:
|
||||
def __init__(self):
|
||||
self.rank = 0
|
||||
self.size = 1
|
||||
|
||||
def init(self):
|
||||
return MockDistributedGroup(self.rank, self.size)
|
||||
|
||||
|
||||
class TestTunerTrainer(unittest.TestCase):
|
||||
def test_iterate_batches_ddp(self):
|
||||
olddist = mx.distributed
|
||||
try:
|
||||
mx.distributed = MockDistributed()
|
||||
group = MockDistributedGroup(0, 1)
|
||||
|
||||
def run(rank, size, batch):
|
||||
mx.distributed.rank = rank
|
||||
mx.distributed.size = size
|
||||
def run(rank, size, batch):
|
||||
group._rank = rank
|
||||
group._size = size
|
||||
|
||||
data = mx.arange(128).reshape(-1, 1).tolist()
|
||||
data = [(d, 0) for d in data]
|
||||
data = mx.arange(128).reshape(-1, 1).tolist()
|
||||
data = [(d, 0) for d in data]
|
||||
|
||||
samples = set()
|
||||
for i, (b, l) in enumerate(iterate_batches(data, batch, 1)):
|
||||
samples.add(tuple(mx.flatten(b).tolist()))
|
||||
samples = set()
|
||||
for i, (b, l) in enumerate(
|
||||
iterate_batches(data, batch, 1, comm_group=group)
|
||||
):
|
||||
samples.add(tuple(mx.flatten(b).tolist()))
|
||||
|
||||
ref_batches = mx.arange(128).reshape(-1, batch).tolist()
|
||||
for b in ref_batches:
|
||||
self.assertTrue(tuple(b[rank::size]) in samples)
|
||||
ref_batches = mx.arange(128).reshape(-1, batch).tolist()
|
||||
for b in ref_batches:
|
||||
self.assertTrue(tuple(b[rank::size]) in samples)
|
||||
|
||||
run(0, 1, 4)
|
||||
run(0, 1, 8)
|
||||
run(0, 2, 8)
|
||||
run(1, 2, 8)
|
||||
run(0, 4, 8)
|
||||
run(1, 4, 8)
|
||||
run(2, 4, 8)
|
||||
run(3, 4, 8)
|
||||
|
||||
finally:
|
||||
mx.distributed = olddist
|
||||
run(0, 1, 4)
|
||||
run(0, 1, 8)
|
||||
run(0, 2, 8)
|
||||
run(1, 2, 8)
|
||||
run(0, 4, 8)
|
||||
run(1, 4, 8)
|
||||
run(2, 4, 8)
|
||||
run(3, 4, 8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user