Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a716f040b7 | |||
| df1d3f3c9a | |||
| ed1fca4cef | |||
| 4f5cbd2a4f | |||
| 3cd9a52df2 | |||
| 2f1ab85aec | |||
| f3bb10c141 | |||
| e1c24b3237 | |||
| f39cb8e934 | |||
| a9856b485d | |||
| e92138cb01 | |||
| a401730941 | |||
| 6d114686e5 | |||
| aa4f880fb3 | |||
| 62f38aeb51 | |||
| d9c63fff67 | |||
| dcbf6e33d1 | |||
| f26fddfd3b | |||
| f56d99712c | |||
| c65c27b450 | |||
| 3257c3df17 | |||
| d4eb136d44 | |||
| 4469ad4647 | |||
| f79dba7832 | |||
| 3f9d179fd1 | |||
| 9dc023beed | |||
| 9dcefa5272 | |||
| bdeac59767 | |||
| 6ddfdda1ac | |||
| 4d3af3cebc | |||
| ed7884cb80 | |||
| f8019f7769 | |||
| 564281f793 | |||
| 73c8550478 | |||
| ed69f837e6 | |||
| cc393b2862 | |||
| 2146e4ed18 | |||
| 735a43b275 | |||
| 332d94ca6f | |||
| 480934402d | |||
| ab157c2d18 | |||
| 5a8ced697e | |||
| 760c5abcc8 | |||
| 43ee5455d3 | |||
| 23af85703e | |||
| 89c430a9c2 | |||
| 4a21ffdf4b | |||
| 852119b774 | |||
| 044474bc80 | |||
| 2105aaf9c3 | |||
| cff7273a55 | |||
| fc7d84448b | |||
| 47be7150a6 | |||
| 35fa620279 | |||
| 8162aaad56 | |||
| 834fac934c | |||
| 179da774b1 | |||
| 720f2369ba | |||
| 65725dcec2 | |||
| d4701ba513 | |||
| 321e764e0a | |||
| 83ff9c96d5 | |||
| 9c113f7019 | |||
| 7d6c5e4af7 | |||
| ad067ea627 | |||
| d7b91e80f0 | |||
| 1fd521c3c7 | |||
| 572ada278c | |||
| fb47f8fb99 | |||
| 7a720882a7 | |||
| 014ebc6a46 | |||
| c6d9d3c9f5 | |||
| bcf630614f | |||
| 1974376d70 | |||
| 7e67225e1d | |||
| 0fd3126496 | |||
| ca0d1c9630 | |||
| 82edd51a1e | |||
| aca4c149a1 | |||
| 8f1c56ec83 | |||
| 84ae19e675 | |||
| 645a326a2e | |||
| fd6959dca7 | |||
| f18526f8d6 | |||
| 25a4c8369e | |||
| e08ec15b72 | |||
| b77ec6b951 | |||
| ab050d1fac | |||
| 942b3ed4b6 | |||
| 11ebc98ada | |||
| 1630f9bf16 | |||
| b7cc3aa5e5 | |||
| 7afcfac51a | |||
| 1ecd27a31a | |||
| 1fe1b3c901 | |||
| 8a0f3781e9 | |||
| 56b8c0f383 | |||
| ad9434bef0 | |||
| 04fd7ccb97 | |||
| 7f1b7fe6bc | |||
| c645a55582 | |||
| 1bdc8afca3 | |||
| 96699e6dad | |||
| b012e1a1e9 | |||
| f53a9b0689 | |||
| beceb5c16d | |||
| 12073b1db4 | |||
| d9846d37cb | |||
| 1b76e3d580 | |||
| e5ddaff99b | |||
| 14b9faf1bc | |||
| 1423702019 | |||
| 5ed1e48a3c | |||
| 4100703f1a | |||
| 8c08a46da4 | |||
| bc891dca4c | |||
| 02228601cd | |||
| 8daabcc7c1 | |||
| cd7d9a536e | |||
| 25246632cf | |||
| 6651d2e0bf | |||
| 43dcf2f0c0 | |||
| 769069d66b | |||
| 5261ab85ee | |||
| c27c94a0ff | |||
| fd80ac89fb | |||
| edbf61dd8b | |||
| c2a716c871 | |||
| 63c9873617 | |||
| 7585c142a6 | |||
| 44d12e5d6f | |||
| 7a86c1289e | |||
| a20eefd7c2 | |||
| 3eb6ecf2b6 | |||
| 39a96ab18b | |||
| 43082feafa | |||
| 5cce1495e0 | |||
| 509f5aef89 | |||
| 0f76343ea4 | |||
| 298b67c755 | |||
| 94497d5255 | |||
| 4c80c68ea6 | |||
| ac8ae2c05a | |||
| 7a4d137df6 | |||
| 90db1e6266 | |||
| d3dc2e3f33 | |||
| 7423bf6752 | |||
| 5dec49a12f | |||
| 3727e01cd7 | |||
| 09579644ac | |||
| 0081085a91 | |||
| fed582eede | |||
| 7973b8cfe8 | |||
| 7096618d50 | |||
| 1e0c0f3985 | |||
| 68f18bae14 | |||
| f5ae09a807 | |||
| 08c8c0a5ea | |||
| a9311cca23 | |||
| 9fe5f43abf | |||
| 1b2d11b5c7 | |||
| 657a66c5c4 | |||
| 595fb4bdbf | |||
| 79a0721c9a | |||
| cc3264c22e | |||
| a227a9e9f3 | |||
| cd9ca9f068 | |||
| 7744d0f40b | |||
| f3ed856610 | |||
| ede65a1484 | |||
| 3d3e0751a3 | |||
| 085e36e6ab | |||
| eea2e5f5de | |||
| cb763947ee | |||
| b343a0556f | |||
| 82dfd39ef2 | |||
| 84996808a2 | |||
| 99f8fd6cc8 | |||
| 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 | |||
| 5e6a7f6895 | |||
| edc656a85c | |||
| a4c6470390 | |||
| 1d114498f2 | |||
| b1fc49a9f2 | |||
| 4781244aaf | |||
| 49ba6dff08 | |||
| c3b4a15851 | |||
| a4e32ef5a5 | |||
| cceb45d6b6 | |||
| 0b8c1668d2 | |||
| f876b3f775 | |||
| 373c63c08f | |||
| 344755a1f6 | |||
| abb185cb66 | |||
| 49919e8563 | |||
| 7168556aaa | |||
| 1e71de53e3 | |||
| f318741784 | |||
| 0edd0a0cd1 | |||
| 44c74e1d04 | |||
| 81c3c193cf | |||
| b264da7602 | |||
| cf8cfd0a1b | |||
| f96344dfe6 | |||
| 9a4039a518 | |||
| 16ca0b3a17 | |||
| e1e66c0d64 | |||
| 380789d067 | |||
| a1d079e930 | |||
| 0c0b72221f | |||
| dcb4b9ba6d | |||
| 358b4d2ab5 | |||
| 1a4d24ed5f | |||
| 47e1710f23 | |||
| 50012d153d | |||
| eaf1748ea5 | |||
| ffc0ecc1ca | |||
| 4096aabdba | |||
| 36963eec80 | |||
| f22120ef83 |
@@ -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,44 @@
|
||||
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: |
|
||||
curl -o test_data.zip -L https://github.com/ml-explore/mlx-lm/releases/download/test_data/test_data.zip
|
||||
unzip test_data.zip
|
||||
METAL_DEVICE_WRAPPER_TYPE=1 METAL_DEBUG_ERROR_MODE=0 HF_HOME="." python -m xmlrunner discover -v tests -o test-results/
|
||||
mlx.launch -n 2 tests/model_parallel_tests.py
|
||||
@@ -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/
|
||||
+22
-13
@@ -8,17 +8,26 @@ 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`,
|
||||
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`;
|
||||
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`, `GLM5 (GLM MoE DSA)`, 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`, Tele-AI's `TeleChat3`, 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`.
|
||||
- Tarjei Mandt: Added support for the following architectures: `Step 3.5 Flash`,
|
||||
MoonshotAI's `Kimi K2.5`, Upstage's `Solar Open`, LG AI Research's `K-Exaone MoE`,
|
||||
Meituan's `LongCat Flash Lite` Helped add support for the following model architectures:
|
||||
Z.ai & THUKEG's `GLM5 (GLM MoE DSA)`
|
||||
@@ -71,7 +71,7 @@ prompt = "Write a story about Einstein"
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages, add_generation_prompt=True,
|
||||
)
|
||||
|
||||
text = generate(model, tokenizer, prompt=prompt, verbose=True)
|
||||
@@ -130,7 +130,7 @@ prompt = "Write a story about Einstein"
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages, add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for response in stream_generate(model, tokenizer, prompt, max_tokens=512):
|
||||
@@ -170,7 +170,7 @@ mlx_lm.generate --help
|
||||
To quantize a model from the command line run:
|
||||
|
||||
```
|
||||
mlx_lm.convert --hf-path mistralai/Mistral-7B-Instruct-v0.3 -q
|
||||
mlx_lm.convert --model mistralai/Mistral-7B-Instruct-v0.3 -q
|
||||
```
|
||||
|
||||
For more options run:
|
||||
@@ -185,7 +185,7 @@ You can upload new models to Hugging Face by specifying `--upload-repo` to
|
||||
|
||||
```
|
||||
mlx_lm.convert \
|
||||
--hf-path mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
--model mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
-q \
|
||||
--upload-repo mlx-community/my-4bit-mistral
|
||||
```
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
Spin up the local server:
|
||||
|
||||
mlx_lm.server
|
||||
|
||||
Then run the benchmark:
|
||||
|
||||
python server_benchmark.py --concurrency 4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from itertools import cycle
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
# Default prompts if no file is provided
|
||||
DEFAULT_PROMPTS = [
|
||||
"Explain quantum computing in simple terms.",
|
||||
"What are the main differences between Python and JavaScript?",
|
||||
"Describe the process of photosynthesis in plants.",
|
||||
"How does a neural network learn from data?",
|
||||
"What is the significance of the Turing test in AI?",
|
||||
"Explain the concept of blockchain technology.",
|
||||
"What causes seasons on Earth?",
|
||||
"How do vaccines work in the human body?",
|
||||
"Describe the water cycle and its importance.",
|
||||
"What is the theory of relativity proposed by Einstein?",
|
||||
"How do electric cars help reduce carbon emissions?",
|
||||
"What are the key features of a market economy?",
|
||||
"Explain how DNA replication works in cells.",
|
||||
"What is machine learning and its real-world applications?",
|
||||
"Describe the structure and function of the human heart.",
|
||||
]
|
||||
|
||||
|
||||
def tokens_per_second(tokens):
|
||||
start = math.floor(tokens[0])
|
||||
stop = math.ceil(tokens[-1])
|
||||
n_bins = int(stop - start) * 10
|
||||
bins = [0] * n_bins
|
||||
for t in tokens:
|
||||
bins[int(n_bins * (t - start) / (stop - start))] += 1
|
||||
|
||||
result = []
|
||||
|
||||
ms = 0
|
||||
cnt = 0
|
||||
for i, b in enumerate(bins):
|
||||
ms += b
|
||||
if cnt == 10:
|
||||
ms -= bins[i - 10]
|
||||
else:
|
||||
cnt += 1
|
||||
|
||||
result.append(10 * ms / cnt)
|
||||
|
||||
times = [start]
|
||||
while times[-1] < stop:
|
||||
times.append(times[-1] + 0.1)
|
||||
|
||||
return times, result
|
||||
|
||||
|
||||
def plot_generation(times, tokens_per_sec, start=None, interval=1.0, width=50):
|
||||
c = "█"
|
||||
start = start or times[0]
|
||||
stop = times[-1]
|
||||
|
||||
bar_times = [start]
|
||||
while bar_times[-1] < stop:
|
||||
bar_times.append(bar_times[-1] + interval)
|
||||
|
||||
bar_values = [[] for _ in bar_times]
|
||||
bar_idx = 0
|
||||
|
||||
for t, v in zip(times, tokens_per_sec):
|
||||
while t > bar_times[bar_idx] + interval:
|
||||
bar_idx += 1
|
||||
bar_values[bar_idx].append(v)
|
||||
|
||||
bar_values = [sum(v) / len(v) if v else 0 for v in bar_values]
|
||||
m = max(bar_values)
|
||||
|
||||
for t, v in zip(bar_times, bar_values):
|
||||
t = t - start
|
||||
b = c * int(v * width / m)
|
||||
print(f"{t:3.2f} {b} ({v})")
|
||||
|
||||
|
||||
def percentile(data, percent):
|
||||
if not data:
|
||||
return 0
|
||||
data = sorted(data)
|
||||
k = (len(data) - 1) * percent / 100
|
||||
f = math.floor(k)
|
||||
c = math.ceil(k)
|
||||
return (
|
||||
data[int(f)]
|
||||
if f == c
|
||||
else data[int(f)] + (data[int(c)] - data[int(f)]) * (k - f)
|
||||
)
|
||||
|
||||
|
||||
def median(data):
|
||||
return percentile(data, 50)
|
||||
|
||||
|
||||
async def make_request(
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
max_tokens: int,
|
||||
) -> Tuple[bool, float, list]:
|
||||
"""
|
||||
Make a single streaming API request and return
|
||||
|
||||
- whether the request succeeded
|
||||
- the request start time
|
||||
- the time of every generated token
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"stream": True,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
start_time = time.perf_counter()
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_body = await response.text()
|
||||
print(f"Error {response.status}: {error_body}")
|
||||
return (False, 0, [])
|
||||
|
||||
# Process streaming response
|
||||
async for chunk in response.content:
|
||||
if chunk:
|
||||
chunk_str = chunk.decode("utf-8").strip()
|
||||
if chunk_str.startswith("data:"):
|
||||
data_str = chunk_str[5:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
if choices := data.get("choices", False):
|
||||
if choices[0].get("finish_reason") != "length":
|
||||
tokens.append(time.perf_counter())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return (bool(tokens), start_time, tokens)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {str(e)}")
|
||||
return (False, 0, [])
|
||||
|
||||
|
||||
async def run_benchmark(
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
concurrency: int,
|
||||
total_requests: int,
|
||||
prompts: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
prompt_cycle = cycle(prompts)
|
||||
semaphore = asyncio.Semaphore(concurrency)
|
||||
results = []
|
||||
request_times = []
|
||||
bar = tqdm(total=total_requests)
|
||||
|
||||
async def worker():
|
||||
async with semaphore:
|
||||
prompt = next(prompt_cycle)
|
||||
result = await make_request(
|
||||
session, url, api_key, model, prompt, max_tokens
|
||||
)
|
||||
bar.update(1)
|
||||
return result
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for _ in range(total_requests):
|
||||
task = asyncio.create_task(worker())
|
||||
tasks.append(task)
|
||||
await asyncio.sleep(0.01) # Stagger requests slightly
|
||||
|
||||
for task in tasks:
|
||||
result = await task
|
||||
results.append(result)
|
||||
bar.close()
|
||||
|
||||
successful_requests = [r for r in results if r[0]]
|
||||
total_tokens = sum(len(r[2]) for r in successful_requests)
|
||||
|
||||
# Gather all the tokens generated with their corresponding timestamps
|
||||
all_tokens = []
|
||||
for r in successful_requests:
|
||||
all_tokens.extend(r[2])
|
||||
all_tokens.sort()
|
||||
full_generation = tokens_per_second(all_tokens)
|
||||
start = min(r[1] for r in successful_requests)
|
||||
|
||||
# Aggregate metrics
|
||||
metrics = {
|
||||
"total_requests": total_requests,
|
||||
"successful_requests": len(successful_requests),
|
||||
"failed_requests": total_requests - len(successful_requests),
|
||||
"total_tokens": total_tokens,
|
||||
"total_time": all_tokens[-1] - start,
|
||||
"aggregate_tokens_per_sec": median(full_generation[1]),
|
||||
"per_request": [],
|
||||
"start": start,
|
||||
"full_generation": full_generation,
|
||||
}
|
||||
|
||||
# Per-request metrics
|
||||
for i, (_, start, tokens) in enumerate(successful_requests):
|
||||
metrics["per_request"].append(
|
||||
{
|
||||
"request_id": i + 1,
|
||||
"time_to_first_token": tokens[0] - start,
|
||||
"total_time": tokens[-1] - start,
|
||||
"tokens_received": len(tokens),
|
||||
"tokens_per_sec": median(tokens_per_second(tokens)[1]),
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate percentiles
|
||||
ttft_values = [m["time_to_first_token"] for m in metrics["per_request"]]
|
||||
tps_values = [m["tokens_per_sec"] for m in metrics["per_request"]]
|
||||
|
||||
metrics["aggregate_metrics"] = {
|
||||
"time_to_first_token": {
|
||||
"min": min(ttft_values) if ttft_values else 0,
|
||||
"max": max(ttft_values) if ttft_values else 0,
|
||||
"avg": sum(ttft_values) / len(ttft_values) if ttft_values else 0,
|
||||
"p95": percentile(ttft_values, 95) if ttft_values else 0,
|
||||
},
|
||||
"tokens_per_sec": {
|
||||
"min": min(tps_values) if tps_values else 0,
|
||||
"max": max(tps_values) if tps_values else 0,
|
||||
"avg": sum(tps_values) / len(tps_values) if tps_values else 0,
|
||||
"p95": percentile(tps_values, 95) if tps_values else 0,
|
||||
},
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LLM API Benchmark Tool")
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
default="http://localhost:8080/v1/chat/completions",
|
||||
help="Chat completions API endpoint URL",
|
||||
)
|
||||
parser.add_argument("--api-key", default="none", help="API key")
|
||||
parser.add_argument("--model", default="default_model", help="Model name")
|
||||
parser.add_argument(
|
||||
"--max-tokens", type=int, default=100, help="Max tokens to generate"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--concurrency", type=int, default=1, help="Number of concurrent requests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-requests", type=int, default=10, help="Total requests to make"
|
||||
)
|
||||
parser.add_argument("--prompt-file", help="File containing prompts (one per line)")
|
||||
parser.add_argument("--output", help="Output file for results (JSON format)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load prompts
|
||||
if args.prompt_file:
|
||||
with open(args.prompt_file, "r") as f:
|
||||
prompts = [line.strip() for line in f if line.strip()]
|
||||
else:
|
||||
prompts = DEFAULT_PROMPTS
|
||||
|
||||
print(
|
||||
f"Starting benchmark with {args.concurrency} concurrency and {args.total_requests} total requests..."
|
||||
)
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Run benchmark
|
||||
results = asyncio.run(
|
||||
run_benchmark(
|
||||
url=args.url,
|
||||
api_key=args.api_key,
|
||||
model=args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
concurrency=args.concurrency,
|
||||
total_requests=args.total_requests,
|
||||
prompts=prompts,
|
||||
)
|
||||
)
|
||||
|
||||
duration = time.perf_counter() - start_time
|
||||
print(f"\nBenchmark completed in {duration:.2f} seconds")
|
||||
print(
|
||||
f"Successful requests: {results['successful_requests']}/{args.total_requests}"
|
||||
)
|
||||
print(f"Total tokens generated: {results['total_tokens']}")
|
||||
print(f"Aggregate tokens/sec: {results['aggregate_tokens_per_sec']:.2f}")
|
||||
|
||||
# Print summary
|
||||
if results["successful_requests"] > 0:
|
||||
ttft = results["aggregate_metrics"]["time_to_first_token"]
|
||||
tps = results["aggregate_metrics"]["tokens_per_sec"]
|
||||
|
||||
print("\nTime to First Token (seconds):")
|
||||
print(
|
||||
f" Min: {ttft['min']:.4f} | Max: {ttft['max']:.4f} | Avg: {ttft['avg']:.4f} | P95: {ttft['p95']:.4f}"
|
||||
)
|
||||
|
||||
print("\nTokens per Second (per request):")
|
||||
print(
|
||||
f" Min: {tps['min']:.2f} | Max: {tps['max']:.2f} | Avg: {tps['avg']:.2f} | P95: {tps['p95']:.2f}"
|
||||
)
|
||||
|
||||
print()
|
||||
plot_generation(*results["full_generation"], results["start"])
|
||||
|
||||
# Save results
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"\nResults saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# Benchmarks
|
||||
|
||||
## Commands
|
||||
|
||||
The command for evaluating on MMLU Pro:
|
||||
|
||||
```
|
||||
mlx_lm.evaluate --model model/repo --task mmlu_pro
|
||||
```
|
||||
|
||||
The command for efficiency benchmarks:
|
||||
|
||||
```
|
||||
mlx_lm.benchmark --model model/repo -p 2048 -g 128
|
||||
```
|
||||
|
||||
To get the package versions run:
|
||||
|
||||
```
|
||||
python -m mlx --version && python -m mlx_lm --version
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
<details>
|
||||
|
||||
<summary> Qwen/Qwen3-4B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 64.05 | 1780.63 | 52.47 | 9.02 | Qwen/Qwen3-4B-Instruct-2507
|
||||
q8 | 63.85 | 1606.573| 86.907 | 5.254 | mlx-community/Qwen3-4B-Instruct-2507-8bit
|
||||
q6 | 63.53 | 1576.73 | 104.68 | 4.25 | mlx-community/Qwen3-4B-Instruct-2507-6bit
|
||||
q5 g32 | 63.16 | 1570.80 | 110.29 | 4.00 | mlx-community/Qwen3-4B-Instruct-2507-5bit-g32
|
||||
q5 | 62.38 | 1584.33 | 116.39 | 3.86 | mlx-community/Qwen3-4B-Instruct-2507-5bit
|
||||
q4 g32 | 61.46 | 1610.03 | 126.00 | 3.603 | mlx-community/Qwen3-4B-Instruct-2507-4bit-g32
|
||||
q4 | 60.72 | 1622.27 | 134.52 | 3.35 | mlx-community/Qwen3-4B-Instruct-2507-4bit
|
||||
|
||||
- Performance benchmark on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> Qwen/Qwen3-30B-A3B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 72.62 | :skull: | :skull: | :skull: | Qwen/Qwen3-30B-A3B-Instruct-2507
|
||||
q8 | 72.46 | 1719.47 | 83.16 | 33.46 | mlx-community/Qwen3-30B-A3B-Instruct-2507-8bit
|
||||
q6 | 72.41 | 1667.45 | 94.14 | 25.82 | mlx-community/Qwen3-30B-A3B-Instruct-2507-6bit
|
||||
q5 | 71.97 | 1664.24 | 101.00 |22.01 | mlx-community/Qwen3-30B-A3B-Instruct-2507-5bit
|
||||
q4 | 70.71 | 1753.90 | 113.33 |18.20 | mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit
|
||||
|
||||
|
||||
- Performance benchmarks on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
@@ -129,7 +129,7 @@ mlx_lm.awq --help
|
||||
Use `mlx_lm.gptq` to run GPTQ on a given model. For example:
|
||||
|
||||
```bash
|
||||
mlx_lm.awq --model Qwen/Qwen3-0.6B
|
||||
mlx_lm.gptq --model Qwen/Qwen3-0.6B
|
||||
```
|
||||
|
||||
The script can take anywhere from a few minutes to several hours depending on
|
||||
|
||||
+12
-7
@@ -66,9 +66,10 @@ mlx_lm.lora \
|
||||
To fine-tune the full model weights, add the `--fine-tune-type full` flag.
|
||||
Currently supported fine-tuning types are `lora` (default), `dora`, and `full`.
|
||||
|
||||
The `--data` argument must specify a path to a `train.jsonl`, `valid.jsonl`
|
||||
when using `--train` and a path to a `test.jsonl` when using `--test`. For more
|
||||
details on the data format see the section on [Data](#Data).
|
||||
The `--data` argument must specify a path to a `train.jsonl` when using
|
||||
`--train` and a path to a `test.jsonl` when using `--test`. A `valid.jsonl` is
|
||||
optional; if provided, validation loss will be reported during training. For
|
||||
more details on the data format see the section on [Data](#Data).
|
||||
|
||||
For example, to fine-tune a Mistral 7B you can use `--model
|
||||
mistralai/Mistral-7B-v0.1`.
|
||||
@@ -184,9 +185,10 @@ Face.
|
||||
|
||||
### Local Datasets
|
||||
|
||||
For fine-tuning (`--train`), the data loader expects a `train.jsonl` and a
|
||||
`valid.jsonl` to be in the data directory. For evaluation (`--test`), the data
|
||||
loader expects a `test.jsonl` in the data directory.
|
||||
For fine-tuning (`--train`), the data loader expects a `train.jsonl` to be in
|
||||
the data directory. A `valid.jsonl` is optional; if present, validation loss
|
||||
will be reported periodically during training. For evaluation (`--test`), the
|
||||
data loader expects a `test.jsonl` in the data directory.
|
||||
|
||||
Currently, `*.jsonl` files support `chat`, `tools`, `completions`, and `text`
|
||||
data formats. Here are examples of these formats:
|
||||
@@ -371,7 +373,10 @@ of memory. Here are some tips to reduce memory use should you need to do so:
|
||||
|
||||
2. Try using a smaller batch size with `--batch-size`. The default is `4` so
|
||||
setting this to `2` or `1` will reduce memory consumption. This may slow
|
||||
things down a little, but will also reduce the memory use.
|
||||
things down a little, but will also reduce the memory use. You can increase
|
||||
the effective batch size without increasing the memory use by accumulating
|
||||
gradients using `--grad-accumulation-steps <N>` which will accumulate the
|
||||
gradient of `<N>` batches before updating the parameters.
|
||||
|
||||
3. Reduce the number of layers to fine-tune with `--num-layers`. The default
|
||||
is `16`, so you can try `8` or `4`. This reduces the amount of memory
|
||||
|
||||
+14
-2
@@ -72,12 +72,24 @@ curl localhost:8080/v1/chat/completions \
|
||||
- `min_p`: (Optional) A float specifying the min-p sampling parameter.
|
||||
Defaults to `0.0` (disabled).
|
||||
|
||||
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
|
||||
Defaults to `1.0`.
|
||||
- `repetition_penalty`: (Optional) Applies a multiplicative penalty to repeated
|
||||
tokens. Defaults to `0.0` (disabled).
|
||||
|
||||
- `repetition_context_size`: (Optional) The size of the context window for
|
||||
applying repetition penalty. Defaults to `20`.
|
||||
|
||||
- `presence_penalty`: (Optional) Applies an additive penalty to tokens
|
||||
that appeared before. Defaults to `0.0` (disabled).
|
||||
|
||||
- `presence_context_size`: (Optional) The size of the context window for
|
||||
applying presence penalty. Defaults to `20`.
|
||||
|
||||
- `frequency_penalty`: (Optional) Applies an additive penalty proportional to
|
||||
how many times a token appeared previously. Defaults to `0.0` (disabled).
|
||||
|
||||
- `frequency_context_size`: (Optional) The size of the context window for
|
||||
applying frequency penalty. Defaults to `20`.
|
||||
|
||||
- `logit_bias`: (Optional) A dictionary mapping token IDs to their bias
|
||||
values. Defaults to `None`.
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
+3
-28
@@ -1,31 +1,6 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
subcommands = {
|
||||
"quant.awq",
|
||||
"quant.dwq",
|
||||
"quant.dynamic_quant",
|
||||
"quant.gptq",
|
||||
"benchmark",
|
||||
"cache_prompt",
|
||||
"chat",
|
||||
"convert",
|
||||
"evaluate",
|
||||
"fuse",
|
||||
"generate",
|
||||
"lora",
|
||||
"perplexity",
|
||||
"server",
|
||||
"manage",
|
||||
"upload",
|
||||
}
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
subcommand = sys.argv.pop(1)
|
||||
if subcommand not in subcommands:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
from . import cli
|
||||
|
||||
cli.main()
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.28.0"
|
||||
__version__ = "0.31.3"
|
||||
|
||||
+74
-28
@@ -1,16 +1,13 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import batch_generate, stream_generate
|
||||
from mlx_lm import batch_generate, load, stream_generate
|
||||
from mlx_lm.generate import DEFAULT_MODEL
|
||||
from mlx_lm.tokenizer_utils import load_tokenizer
|
||||
from mlx_lm.utils import (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
)
|
||||
from mlx_lm.utils import pipeline_load, sharded_load
|
||||
|
||||
|
||||
def setup_arg_parser():
|
||||
@@ -25,11 +22,6 @@ def setup_arg_parser():
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
action="store_true",
|
||||
help="Enable trusting remote code for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt-tokens",
|
||||
"-p",
|
||||
@@ -58,6 +50,29 @@ def setup_arg_parser():
|
||||
help="Number of timing trials",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quantize-activations",
|
||||
"-qa",
|
||||
action="store_true",
|
||||
help="Quantize activations using the same quantization config as the corresponding layer.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefill-step-size",
|
||||
type=int,
|
||||
default=2048,
|
||||
help="Step size for prefill processing (default: 2048)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Delay between each test in seconds (default: 0)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -66,50 +81,81 @@ def main():
|
||||
args = parser.parse_args()
|
||||
mx.random.seed(0)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
model_path, _ = get_model_path(model_path, revision=None)
|
||||
model, config, _ = fetch_from_hub(model_path, trust_remote_code=True)
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
eos_token_ids=[], # Empty to avoid early stopping
|
||||
tokenizer_config_extra={"trust_remote_code": True},
|
||||
)
|
||||
if group.size() > 1:
|
||||
model, tokenizer, config = sharded_load(
|
||||
model_path, pipeline_group, tensor_group, return_config=True
|
||||
)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
model_path,
|
||||
return_config=True,
|
||||
tokenizer_config={"trust_remote_code": True},
|
||||
model_config={"quantize_activations": args.quantize_activations},
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
tokenizer._eos_token_ids = {}
|
||||
|
||||
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():
|
||||
for response in stream_generate(
|
||||
model, tokenizer, prompt, max_tokens=generation_tokens
|
||||
model,
|
||||
tokenizer,
|
||||
prompt,
|
||||
max_tokens=generation_tokens,
|
||||
prefill_step_size=args.prefill_step_size,
|
||||
):
|
||||
pass
|
||||
return response
|
||||
|
||||
def batch_bench():
|
||||
return batch_generate(
|
||||
model, tokenizer, prompts, max_tokens=generation_tokens
|
||||
model,
|
||||
tokenizer,
|
||||
prompts,
|
||||
max_tokens=generation_tokens,
|
||||
prefill_step_size=args.prefill_step_size,
|
||||
).stats
|
||||
|
||||
_bench = batch_bench
|
||||
if batch_size == 1:
|
||||
_bench = single_bench
|
||||
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):
|
||||
if args.delay > 0:
|
||||
time.sleep(args.delay)
|
||||
tic = time.perf_counter()
|
||||
response = _bench()
|
||||
toc = time.perf_counter()
|
||||
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))
|
||||
results.append(f"total_time={toc - tic:.3f}")
|
||||
rprint(f"Trial {i+1}: " + ", ".join(results))
|
||||
|
||||
def avg(k):
|
||||
vals = (getattr(response, k) for response in responses)
|
||||
@@ -117,7 +163,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__":
|
||||
|
||||
+4
-17
@@ -41,16 +41,6 @@ def setup_arg_parser():
|
||||
default=None,
|
||||
help="End of sequence token for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-chat-template",
|
||||
action="store_true",
|
||||
help="Use the raw prompt without the tokenizer's chat template.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-default-chat-template",
|
||||
action="store_true",
|
||||
help="Use the default chat template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-kv-size",
|
||||
type=int,
|
||||
@@ -107,14 +97,12 @@ def main():
|
||||
|
||||
args.prompt = sys.stdin.read() if args.prompt == "-" else args.prompt
|
||||
|
||||
if args.use_default_chat_template:
|
||||
if tokenizer.chat_template is None:
|
||||
tokenizer.chat_template = tokenizer.default_chat_template
|
||||
|
||||
if not args.ignore_chat_template and tokenizer.chat_template is not None:
|
||||
if tokenizer.has_chat_template:
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=False, continue_final_message=True
|
||||
messages,
|
||||
add_generation_prompt=False,
|
||||
continue_final_message=True,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -153,7 +141,6 @@ def main():
|
||||
print("Saving...")
|
||||
metadata = {}
|
||||
metadata["model"] = args.model
|
||||
metadata["chat_template"] = json.dumps(tokenizer.chat_template)
|
||||
metadata["tokenizer_config"] = json.dumps(tokenizer_config)
|
||||
save_prompt_cache(args.prompt_cache_file, cache, metadata)
|
||||
|
||||
|
||||
+41
-20
@@ -7,13 +7,13 @@ import mlx.core as mx
|
||||
from .generate import stream_generate
|
||||
from .models.cache import make_prompt_cache
|
||||
from .sample_utils import make_sampler
|
||||
from .utils import load
|
||||
from .utils import load, sharded_load
|
||||
|
||||
DEFAULT_TEMP = 0.0
|
||||
DEFAULT_TOP_P = 1.0
|
||||
DEFAULT_XTC_PROBABILITY = 0.0
|
||||
DEFAULT_XTC_THRESHOLD = 0.0
|
||||
DEFAULT_SEED = None
|
||||
DEFAULT_SEED = 0
|
||||
DEFAULT_MAX_TOKENS = 256
|
||||
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
|
||||
|
||||
@@ -79,6 +79,11 @@ def setup_arg_parser():
|
||||
default=None,
|
||||
help="System prompt to be used for the chat template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -86,28 +91,41 @@ def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.seed is not None:
|
||||
mx.random.seed(args.seed)
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={
|
||||
"trust_remote_code": True if args.trust_remote_code else None
|
||||
},
|
||||
)
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
if group.size() > 1:
|
||||
if args.adapter_path:
|
||||
parser.error("Adapters not supported in distributed mode")
|
||||
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
|
||||
else:
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={
|
||||
"trust_remote_code": True if args.trust_remote_code else None
|
||||
},
|
||||
)
|
||||
|
||||
def print_help():
|
||||
print("The command list:")
|
||||
print("- 'q' to exit")
|
||||
print("- 'r' to reset the chat")
|
||||
print("- 'h' to display these commands")
|
||||
rprint("The command list:")
|
||||
rprint("- 'q' to exit")
|
||||
rprint("- 'r' to reset the chat")
|
||||
rprint("- 'h' to display these commands")
|
||||
|
||||
print(f"[INFO] Starting chat session with {args.model}.")
|
||||
rprint(f"[INFO] Starting chat session with {args.model}.")
|
||||
print_help()
|
||||
prompt_cache = make_prompt_cache(model, args.max_kv_size)
|
||||
while True:
|
||||
query = input(">> ")
|
||||
query = input(">> " if rank == 0 else "")
|
||||
if query == "q":
|
||||
break
|
||||
if query == "r":
|
||||
@@ -120,7 +138,10 @@ def main():
|
||||
if args.system_prompt is not None:
|
||||
messages.append({"role": "system", "content": args.system_prompt})
|
||||
messages.append({"role": "user", "content": query})
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for response in stream_generate(
|
||||
model,
|
||||
tokenizer,
|
||||
@@ -137,8 +158,8 @@ def main():
|
||||
),
|
||||
prompt_cache=prompt_cache,
|
||||
):
|
||||
print(response.text, flush=True, end="")
|
||||
print()
|
||||
rprint(response.text, flush=True, end="")
|
||||
rprint()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from inspect import isfunction
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from transformers.utils.chat_template_utils import get_json_schema
|
||||
|
||||
TOOLS_SYSTEM_TEMPLATE = """## Tools
|
||||
|
||||
You have access to a set of tools you can use to answer the user's question.
|
||||
You can invoke functions by writing a "<{dsml_token}function_calls>" block like the following as part of your reply to the user:
|
||||
<{dsml_token}function_calls>
|
||||
<{dsml_token}invoke name="$FUNCTION_NAME">
|
||||
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
<{dsml_token}invoke name="$FUNCTION_NAME2">
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
</{dsml_token}function_calls>
|
||||
|
||||
String and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The "string" attribute should be set to "true" for string type parameters and "false" for other types (numbers, booleans, arrays, objects).
|
||||
|
||||
If the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:
|
||||
|
||||
<{dsml_token}function_calls>
|
||||
...
|
||||
</{dsml_token}function_calls>
|
||||
|
||||
<function_results>
|
||||
...
|
||||
</function_results>
|
||||
|
||||
{thinking_start_token}...thinking about results{thinking_end_token}
|
||||
|
||||
Here are the functions available in JSONSchema format:
|
||||
<functions>
|
||||
{tool_schemas}
|
||||
</functions>
|
||||
"""
|
||||
|
||||
bos_token: str = "<|begin▁of▁sentence|>"
|
||||
eos_token: str = "<|end▁of▁sentence|>"
|
||||
thinking_start_token: str = "<think>"
|
||||
thinking_end_token: str = "</think>"
|
||||
dsml_token: str = "|DSML|"
|
||||
system_msg_template: str = "{content}"
|
||||
user_msg_template: str = "<|User|>{content}<|Assistant|>"
|
||||
assistant_msg_template: str = "{reasoning}{content}{tool_calls}<|end▁of▁sentence|>"
|
||||
thinking_template = "{reasoning_content}"
|
||||
|
||||
response_format_template: str = (
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
|
||||
)
|
||||
tool_call_template: str = (
|
||||
'<{dsml_token}invoke name="{name}">\n{arguments}\n</{dsml_token}invoke>'
|
||||
)
|
||||
tool_calls_template = (
|
||||
"<{dsml_token}function_calls>\n{tool_calls}\n</{dsml_token}function_calls>"
|
||||
)
|
||||
|
||||
tool_output_template: str = "\n<result>{content}</result>"
|
||||
|
||||
|
||||
def to_json(value: Any) -> str:
|
||||
try:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
except:
|
||||
return json.dumps(value, ensure_ascii=True)
|
||||
|
||||
|
||||
def tools_from_openai_format(tools):
|
||||
def normalize_tool(tool):
|
||||
if isfunction(tool):
|
||||
return get_json_schema(tool)
|
||||
return tool["function"]
|
||||
|
||||
return [normalize_tool(tool) for tool in tools]
|
||||
|
||||
|
||||
def tool_calls_from_openai_format(tool_calls):
|
||||
return [
|
||||
{
|
||||
"name": tool_call["function"]["name"],
|
||||
"arguments": tool_call["function"]["arguments"],
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
|
||||
p_dsml_template = """<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>"""
|
||||
P_dsml_strs = []
|
||||
|
||||
arguments = json.loads(tool_call["arguments"])
|
||||
|
||||
for k, v in arguments.items():
|
||||
p_dsml_str = p_dsml_template.format(
|
||||
dsml_token=dsml_token,
|
||||
key=k,
|
||||
is_str="true" if isinstance(v, str) else "false",
|
||||
value=v if isinstance(v, str) else to_json(v),
|
||||
)
|
||||
|
||||
P_dsml_strs.append(p_dsml_str)
|
||||
|
||||
return "\n".join(P_dsml_strs)
|
||||
|
||||
|
||||
def decode_dsml_to_arguments(
|
||||
tool_name: str, tool_args: Dict[str, Tuple[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
def _decode_value(key: str, value: str, string: str):
|
||||
if string == "true":
|
||||
value = to_json(value)
|
||||
return f"{to_json(key)}: {value}"
|
||||
|
||||
tool_args_json = (
|
||||
"{"
|
||||
+ ", ".join(
|
||||
[_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
return dict(name=tool_name, arguments=tool_args_json)
|
||||
|
||||
|
||||
def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
|
||||
tools_json = [to_json(t) for t in tools]
|
||||
|
||||
return TOOLS_SYSTEM_TEMPLATE.format(
|
||||
tool_schemas="\n".join(tools_json),
|
||||
dsml_token=dsml_token,
|
||||
thinking_start_token=thinking_start_token,
|
||||
thinking_end_token=thinking_end_token,
|
||||
)
|
||||
|
||||
|
||||
def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
|
||||
last_user_index = -1
|
||||
for idx in range(len(messages) - 1, -1, -1):
|
||||
if messages[idx].get("role") in ["user", "developer"]:
|
||||
last_user_index = idx
|
||||
break
|
||||
return last_user_index
|
||||
|
||||
|
||||
def render_message(
|
||||
index: int,
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
tools: Any = None,
|
||||
) -> str:
|
||||
assert 0 <= index < len(messages)
|
||||
assert thinking_mode in [
|
||||
"chat",
|
||||
"thinking",
|
||||
], f"Invalid thinking_mode `{thinking_mode}`"
|
||||
|
||||
prompt = ""
|
||||
msg = messages[index]
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
tools = tools or msg.get("tools")
|
||||
response_format = msg.get("response_format")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_content = msg.get("reasoning_content")
|
||||
|
||||
if tool_calls:
|
||||
tool_calls = tool_calls_from_openai_format(tool_calls)
|
||||
|
||||
if role == "system":
|
||||
prompt += system_msg_template.format(content=content or "")
|
||||
if tools:
|
||||
prompt += "\n\n" + render_tools(tools_from_openai_format(tools))
|
||||
|
||||
if response_format:
|
||||
prompt += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
elif role == "developer":
|
||||
assert content, f"Invalid message for role `{role}`: {msg}"
|
||||
content_developer = ""
|
||||
if tools:
|
||||
content_developer += "\n\n" + render_tools(tools_from_openai_format(tools))
|
||||
|
||||
if response_format:
|
||||
content_developer += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
content_developer += "\n\n# The user's message is: {}".format(content)
|
||||
|
||||
prompt += user_msg_template.format(content=content_developer)
|
||||
if index == last_user_idx and thinking_mode == "thinking":
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
elif role == "user":
|
||||
prompt += user_msg_template.format(content=content)
|
||||
|
||||
if index == last_user_idx and thinking_mode == "thinking":
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
elif role == "tool":
|
||||
prev_assistant_idx = index - 1
|
||||
assistant_msg = messages[prev_assistant_idx]
|
||||
while prev_assistant_idx >= 0 and assistant_msg.get("role") == "tool":
|
||||
prev_assistant_idx -= 1
|
||||
assistant_msg = messages[prev_assistant_idx]
|
||||
|
||||
assert (
|
||||
index == 0
|
||||
or prev_assistant_idx >= 0
|
||||
and assistant_msg.get("role") == "assistant"
|
||||
), f"Invalid messages at {index}:\n{assistant_msg}"
|
||||
|
||||
tool_call_order = index - prev_assistant_idx
|
||||
assistant_tool_calls = assistant_msg.get("tool_calls")
|
||||
assert (
|
||||
assistant_tool_calls and len(assistant_tool_calls) >= tool_call_order
|
||||
), "No tool calls but found tool output"
|
||||
|
||||
if tool_call_order == 1:
|
||||
prompt += "\n\n<function_results>"
|
||||
|
||||
prompt += tool_output_template.format(content=content)
|
||||
|
||||
if tool_call_order == len(assistant_tool_calls):
|
||||
prompt += "\n</function_results>"
|
||||
|
||||
if index >= last_user_idx and thinking_mode == "thinking":
|
||||
prompt += "\n\n" + thinking_start_token
|
||||
else:
|
||||
prompt += "\n\n" + thinking_end_token
|
||||
|
||||
elif role == "assistant":
|
||||
prev_assistant_idx = index
|
||||
thinking_part = ""
|
||||
|
||||
tool_calls_content = ""
|
||||
if tool_calls:
|
||||
tool_calls = [
|
||||
tool_call_template.format(
|
||||
dsml_token=dsml_token,
|
||||
name=tool_call.get("name"),
|
||||
arguments=encode_arguments_to_dsml(tool_call),
|
||||
)
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
tool_calls_content += "\n\n" + tool_calls_template.format(
|
||||
dsml_token=dsml_token, tool_calls="\n".join(tool_calls)
|
||||
)
|
||||
|
||||
summary_content = content or ""
|
||||
|
||||
if thinking_mode == "thinking" and index > last_user_idx:
|
||||
assert (
|
||||
reasoning_content or tool_calls
|
||||
), f"ThinkingMode: {thinking_mode}, invalid message without reasoning_content/tool_calls `{msg}` after last user message"
|
||||
thinking_part = (
|
||||
thinking_template.format(reasoning_content=reasoning_content or "")
|
||||
+ thinking_end_token
|
||||
)
|
||||
|
||||
prompt += assistant_msg_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tool_calls_content,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown role: {role}")
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def drop_thinking_messages(
|
||||
messages: List[Dict[str, Any]], last_user_idx: Optional[int] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
messages_wo_thinking: List[Dict[str, Any]] = []
|
||||
last_user_idx = (
|
||||
find_last_user_index(messages) if last_user_idx is None else last_user_idx
|
||||
)
|
||||
for idx, msg in enumerate(messages):
|
||||
role = msg.get("role")
|
||||
if role in ["user", "system", "tool"] or idx >= last_user_idx:
|
||||
messages_wo_thinking.append(msg)
|
||||
continue
|
||||
|
||||
elif role == "assistant":
|
||||
msg_wo_thinking = copy.copy(msg)
|
||||
msg_wo_thinking.pop("reasoning_content", None)
|
||||
messages_wo_thinking.append(msg_wo_thinking)
|
||||
|
||||
return messages_wo_thinking
|
||||
|
||||
|
||||
def encode_messages(
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str = "thinking",
|
||||
context: Optional[List[Dict[str, Any]]] = None,
|
||||
drop_thinking: bool = True,
|
||||
add_default_bos_token: bool = True,
|
||||
tools: Any = None,
|
||||
) -> str:
|
||||
context = context if context else []
|
||||
full_messages = context + messages
|
||||
prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
|
||||
|
||||
if thinking_mode == "thinking" and drop_thinking:
|
||||
full_messages = drop_thinking_messages(full_messages)
|
||||
|
||||
for idx in range(len(messages)):
|
||||
prompt += render_message(
|
||||
idx + len(context),
|
||||
full_messages,
|
||||
thinking_mode=thinking_mode,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def apply_chat_template(
|
||||
messages, continue_final_message=False, add_generation_prompt=False, **kwargs
|
||||
):
|
||||
out = encode_messages(messages, **kwargs)
|
||||
if continue_final_message and add_generation_prompt:
|
||||
raise ValueError(
|
||||
"Only one of continue_final_message or add_generation_prompt can be True"
|
||||
)
|
||||
if not add_generation_prompt and messages[-1]["role"] == "user":
|
||||
out = out.removesuffix("<|Assistant|><think>")
|
||||
if continue_final_message and messages[-1]["role"] == "assistant":
|
||||
out = out.removesuffix(eos_token)
|
||||
return out
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
subcommands = (
|
||||
"benchmark",
|
||||
"cache_prompt",
|
||||
"chat",
|
||||
"convert",
|
||||
"evaluate",
|
||||
"fuse",
|
||||
"generate",
|
||||
"lora",
|
||||
"manage",
|
||||
"perplexity",
|
||||
"awq",
|
||||
"dwq",
|
||||
"dynamic_quant",
|
||||
"gptq",
|
||||
"server",
|
||||
"upload",
|
||||
"share",
|
||||
)
|
||||
subpackages = {
|
||||
"awq": "quant",
|
||||
"dwq": "quant",
|
||||
"dynamic_quant": "quant",
|
||||
"gptq": "quant",
|
||||
}
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
subcommand = sys.argv.pop(1)
|
||||
if subcommand in subcommands:
|
||||
if subpackage := subpackages.get(subcommand):
|
||||
subcommand = f"{subpackage}.{subcommand}"
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
elif subcommand == "--version":
|
||||
from mlx_lm import __version__
|
||||
|
||||
print(__version__)
|
||||
elif subcommand in ("-h", "--help"):
|
||||
print(f"The supported subcommands are {subcommands}")
|
||||
print()
|
||||
print(
|
||||
"For help on an individual subcommand, pass --help "
|
||||
"to the subcommand. For example: mlx_lm.generate --help"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
+40
-19
@@ -10,8 +10,7 @@ from mlx.utils import tree_map_with_path
|
||||
|
||||
from .utils import (
|
||||
dequantize_model,
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
quantize_model,
|
||||
save,
|
||||
upload_to_hub,
|
||||
@@ -21,6 +20,7 @@ from .utils import (
|
||||
def mixed_quant_predicate_builder(
|
||||
recipe: str, model: nn.Module, group_size: int = 64
|
||||
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
|
||||
mode = "affine"
|
||||
high_bits = 6
|
||||
|
||||
if recipe == "mixed_2_6":
|
||||
@@ -63,14 +63,16 @@ def mixed_quant_predicate_builder(
|
||||
or index >= 7 * num_layers // 8
|
||||
or (index - num_layers // 8) % 3 == 2
|
||||
)
|
||||
if "v_proj" in path and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
if (
|
||||
"v_proj" in path or "v_a_proj" in path or "v_b_proj" in path
|
||||
) and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
if "down_proj" in path and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
if "lm_head" in path:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
|
||||
return {"group_size": group_size, "bits": low_bits}
|
||||
return {"group_size": group_size, "bits": low_bits, "mode": mode}
|
||||
|
||||
return mixed_quant_predicate
|
||||
|
||||
@@ -84,8 +86,8 @@ def convert(
|
||||
hf_path: str,
|
||||
mlx_path: str = "mlx_model",
|
||||
quantize: bool = False,
|
||||
q_group_size: int = 64,
|
||||
q_bits: int = 4,
|
||||
q_group_size: Optional[int] = None,
|
||||
q_bits: Optional[int] = None,
|
||||
q_mode: str = "affine",
|
||||
dtype: Optional[str] = None,
|
||||
upload_repo: str = None,
|
||||
@@ -107,18 +109,27 @@ def convert(
|
||||
)
|
||||
|
||||
print("[INFO] Loading")
|
||||
model_path, hf_path = get_model_path(hf_path, revision=revision)
|
||||
model, config, tokenizer = fetch_from_hub(
|
||||
model_path, lazy=True, trust_remote_code=trust_remote_code
|
||||
model, tokenizer, config = load(
|
||||
hf_path,
|
||||
revision=revision,
|
||||
return_config=True,
|
||||
tokenizer_config={"trust_remote_code": trust_remote_code},
|
||||
lazy=True,
|
||||
)
|
||||
|
||||
if isinstance(quant_predicate, str):
|
||||
if q_mode != "affine":
|
||||
raise ValueError(f"Quant predicates only support 'affine' quantization.")
|
||||
quant_predicate = mixed_quant_predicate_builder(
|
||||
quant_predicate, model, q_group_size
|
||||
quant_predicate,
|
||||
model,
|
||||
q_group_size,
|
||||
)
|
||||
|
||||
if dtype is None:
|
||||
dtype = config.get("torch_dtype", None)
|
||||
if dtype is None and (text_config := config.get("text_config", None)):
|
||||
dtype = text_config.get("dtype", None)
|
||||
if dtype in MODEL_CONVERSION_DTYPES:
|
||||
print("[INFO] Using dtype:", dtype)
|
||||
dtype = getattr(mx, dtype)
|
||||
@@ -154,11 +165,10 @@ def convert(
|
||||
|
||||
save(
|
||||
mlx_path,
|
||||
model_path,
|
||||
hf_path,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_path,
|
||||
)
|
||||
|
||||
if upload_repo is not None:
|
||||
@@ -176,7 +186,12 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
description="Convert Hugging Face model to MLX format"
|
||||
)
|
||||
|
||||
parser.add_argument("--hf-path", type=str, help="Path to the Hugging Face model.")
|
||||
parser.add_argument(
|
||||
"--hf-path",
|
||||
"--model",
|
||||
type=str,
|
||||
help="Path to the model. This can be a local path or a Hugging Face Hub model identifier.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mlx-path", type=str, default="mlx_model", help="Path to save the MLX model."
|
||||
)
|
||||
@@ -184,17 +199,23 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
"-q", "--quantize", help="Generate a quantized model.", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-group-size", help="Group size for quantization.", type=int, default=64
|
||||
"--q-group-size",
|
||||
help="Group size for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
|
||||
"--q-bits",
|
||||
help="Bits per weight for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-mode",
|
||||
help="The quantization mode.",
|
||||
type=str,
|
||||
default="affine",
|
||||
choices=["affine", "mxfp4"],
|
||||
choices=["affine", "mxfp4", "nvfp4", "mxfp8"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quant-predicate",
|
||||
|
||||
+83
-15
@@ -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
|
||||
@@ -20,12 +20,14 @@ import mlx.nn as nn
|
||||
import numpy as np
|
||||
from lm_eval.api.model import LM
|
||||
from lm_eval.api.registry import register_model
|
||||
from lm_eval.models import huggingface
|
||||
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 +38,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()
|
||||
@@ -62,15 +71,16 @@ def chat_template_fn(**extra_kwargs):
|
||||
@register_model("mlxlm")
|
||||
class MLXLM(LM):
|
||||
|
||||
tokenizer_name = huggingface.HFLM.tokenizer_name
|
||||
apply_chat_template = chat_template_fn()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path_or_hf_repo: str,
|
||||
max_tokens: int,
|
||||
max_tokens: Optional[int] = None,
|
||||
batch_size: int = 8,
|
||||
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}
|
||||
@@ -78,10 +88,11 @@ class MLXLM(LM):
|
||||
path_or_hf_repo, tokenizer_config=tokenizer_config
|
||||
)
|
||||
self._max_tokens = max_tokens
|
||||
self._batch_size = 8
|
||||
self._batch_size = batch_size
|
||||
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]
|
||||
@@ -134,6 +145,10 @@ class MLXLM(LM):
|
||||
for t in texts
|
||||
]
|
||||
|
||||
@property
|
||||
def tokenizer_name(self) -> str:
|
||||
return self.tokenizer.name_or_path.replace("/", "__")
|
||||
|
||||
def loglikelihood(self, requests) -> list[tuple[float, bool]]:
|
||||
"""Compute log-likelihood of generating a continuation from a context.
|
||||
Downstream tasks should attempt to use loglikelihood instead of other
|
||||
@@ -182,7 +197,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 :]
|
||||
@@ -304,11 +320,16 @@ class MLXLM(LM):
|
||||
continuation: str
|
||||
The generated continuation.
|
||||
"""
|
||||
group = mx.distributed.init()
|
||||
|
||||
# split data accross ranks
|
||||
total_requests = len(requests)
|
||||
requests = requests[group.rank() :: group.size()]
|
||||
|
||||
logging.info("Generating continuation for %d sequences." % len(requests))
|
||||
contexts, options = zip(*[req.args for req in requests])
|
||||
# The second element of the tuple contains:
|
||||
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
|
||||
completions = []
|
||||
|
||||
# Tokenize all contexts
|
||||
contexts = [
|
||||
@@ -319,7 +340,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,
|
||||
@@ -327,12 +351,46 @@ 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:
|
||||
with mx.stream(mx.cpu):
|
||||
pad_to = (total_requests + group.size() - 1) // group.size()
|
||||
pad = pad_to - len(completions)
|
||||
completions = [list(c.encode("utf-8")) for c in completions]
|
||||
max_len = mx.array(max(len(c) for c in completions))
|
||||
max_len = mx.distributed.all_max(max_len).item()
|
||||
lengths = mx.array([len(c) for c in completions] + [0] * pad)
|
||||
completions = mx.array(
|
||||
[c + [0] * (max_len - len(c)) for c in completions]
|
||||
+ [[0] * max_len] * pad,
|
||||
mx.uint8,
|
||||
)
|
||||
completions = (
|
||||
mx.distributed.all_gather(completions[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
lengths = (
|
||||
mx.distributed.all_gather(lengths[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
completions = completions[:total_requests]
|
||||
lengths = lengths[:total_requests]
|
||||
completions = [
|
||||
bytearray(c[:l]).decode() for c, l in zip(completions, lengths)
|
||||
]
|
||||
|
||||
return completions
|
||||
|
||||
|
||||
@@ -350,8 +408,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",
|
||||
@@ -393,7 +452,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)
|
||||
@@ -410,11 +471,18 @@ 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,
|
||||
batch_size=args.batch_size,
|
||||
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, max_tokens=2048
|
||||
)
|
||||
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])
|
||||
|
||||
@@ -15,7 +15,10 @@ prompt_cache = make_prompt_cache(model)
|
||||
# User turn
|
||||
prompt = "Hi my name is <Name>."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
@@ -29,7 +32,10 @@ response = generate(
|
||||
# User turn
|
||||
prompt = "What's my name?"
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
|
||||
@@ -14,7 +14,8 @@ conversation = [{"role": "user", "content": prompt}]
|
||||
|
||||
# Transform the prompt into the chat template
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
conversation=conversation, add_generation_prompt=True
|
||||
conversation=conversation,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Specify the maximum number of tokens
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The path to the local model directory or Hugging Face repo.
|
||||
model: "mlx-community/Llama-3.2-1B-Instruct"
|
||||
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
|
||||
|
||||
# Whether or not to train (boolean)
|
||||
train: true
|
||||
@@ -47,6 +47,9 @@ steps_per_report: 10
|
||||
# Number of training steps between validations.
|
||||
steps_per_eval: 200
|
||||
|
||||
# Number of micro-steps to accumulate before each optimizer update.
|
||||
grad_accumulation_steps: 1
|
||||
|
||||
# Load path to resume training with the given adapter weights.
|
||||
resume_adapter_file: null
|
||||
|
||||
@@ -90,4 +93,3 @@ lora_parameters:
|
||||
# valid_split: "train[-100:]"
|
||||
# prompt_feature: "text"
|
||||
# completion_feature: "summary"
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="not-needed",
|
||||
base_url="http://localhost:8080/v1",
|
||||
)
|
||||
|
||||
model = "mlx-community/Qwen3-4B-Thinking-2507-4bit"
|
||||
|
||||
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
|
||||
|
||||
# Non-streaming example
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model, messages=messages, max_tokens=2048
|
||||
)
|
||||
|
||||
reasoning = response.choices[0].message.reasoning
|
||||
content = response.choices[0].message.content
|
||||
|
||||
print("=== reasoning ===\n")
|
||||
print(f"\033[37m{reasoning}\033[0m")
|
||||
print("=== content ===\n")
|
||||
print(content)
|
||||
|
||||
# Streaming example
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
max_tokens=2048,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
if (reasoning := chunk.choices[0].delta.reasoning) is not None:
|
||||
print(f"\033[37m{reasoning}\033[0m", end="")
|
||||
if (content := chunk.choices[0].delta.content) is not None:
|
||||
print(f"{content}", end="")
|
||||
print()
|
||||
@@ -8,11 +8,13 @@ To run, first start the server:
|
||||
|
||||
Then run this script.
|
||||
"""
|
||||
import json
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
|
||||
|
||||
model = "mlx-community/qwen3-4b-4bit-DWQ"
|
||||
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
|
||||
|
||||
tools = [
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
|
||||
"""
|
||||
Run with:
|
||||
|
||||
```
|
||||
mlx.launch \
|
||||
--hostfile /path/to/hosts.json \
|
||||
/path/to/pipeline_generate.py \
|
||||
--prompt "hello world"
|
||||
```
|
||||
|
||||
Make sure you can run MLX over MPI on two hosts. For more information see the
|
||||
documentation:
|
||||
|
||||
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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="mlx-community/DeepSeek-R1-3bit",
|
||||
help="HF repo or path to local model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
"-p",
|
||||
default="Write a quicksort in C++.",
|
||||
help="Message to be processed by the model ('-' reads from stdin)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
"-m",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Maximum number of tokens to generate",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model, tokenizer = shard_and_load(args.model)
|
||||
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
|
||||
for response in stream_generate(
|
||||
model, tokenizer, prompt, max_tokens=args.max_tokens
|
||||
):
|
||||
rprint(response.text, end="", flush=True)
|
||||
|
||||
rprint()
|
||||
rprint("=" * 10)
|
||||
rprint(
|
||||
f"Prompt: {response.prompt_tokens} tokens, "
|
||||
f"{response.prompt_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(
|
||||
f"Generation: {response.generation_tokens} tokens, "
|
||||
f"{response.generation_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(f"Peak memory: {response.peak_memory:.3f} GB")
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
"""
|
||||
Run with:
|
||||
|
||||
```
|
||||
mlx.launch \
|
||||
--backend jaccl \
|
||||
--env MLX_METAL_FAST_SYNCH=1 \
|
||||
--hostfile /path/to/hosts.json \
|
||||
/path/to/sharded_generate.py \
|
||||
--prompt 'Hello world'
|
||||
```
|
||||
|
||||
For more information on running distributed programs with MLX see the documentation:
|
||||
|
||||
https://ml-explore.github.io/mlx/build/html/usage/distributed.html .
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import stream_generate
|
||||
from mlx_lm.utils import sharded_load
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM distributed inference example")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="mlx-community/Llama-3.3-70B-Instruct-4bit",
|
||||
help="HF repo or path to local model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
"-p",
|
||||
default="Write a quicksort in C++.",
|
||||
help="Message to be processed by the model ('-' reads from stdin)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
"-m",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Maximum number of tokens to generate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
|
||||
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for response in stream_generate(
|
||||
model, tokenizer, prompt, max_tokens=args.max_tokens
|
||||
):
|
||||
rprint(response.text, end="", flush=True)
|
||||
|
||||
rprint()
|
||||
rprint("=" * 10)
|
||||
rprint(
|
||||
f"Prompt: {response.prompt_tokens} tokens, "
|
||||
f"{response.prompt_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(
|
||||
f"Generation: {response.generation_tokens} tokens, "
|
||||
f"{response.generation_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(f"Peak memory: {response.peak_memory:.3f} GB")
|
||||
@@ -6,7 +6,7 @@ from mlx_lm import generate, load
|
||||
from mlx_lm.models.cache import make_prompt_cache
|
||||
|
||||
# Specify the checkpoint
|
||||
checkpoint = "mlx-community/Qwen2.5-32B-Instruct-4bit"
|
||||
checkpoint = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
|
||||
# Load the corresponding model and tokenizer
|
||||
model, tokenizer = load(path_or_hf_repo=checkpoint)
|
||||
@@ -31,7 +31,9 @@ prompt = "Multiply 12234585 and 48838483920."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True, tools=list(tools.values())
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
tools=list(tools.values()),
|
||||
)
|
||||
|
||||
prompt_cache = make_prompt_cache(model)
|
||||
@@ -47,12 +49,11 @@ response = generate(
|
||||
)
|
||||
|
||||
# Parse the tool call:
|
||||
# (Note, the tool call format is model specific)
|
||||
tool_open = "<tool_call>"
|
||||
tool_close = "</tool_call>"
|
||||
start_tool = response.find(tool_open) + len(tool_open)
|
||||
end_tool = response.find(tool_close)
|
||||
tool_call = json.loads(response[start_tool:end_tool].strip())
|
||||
# - The tool call format is model specific.
|
||||
# - The tokenizer's tool parser expects tool call text to be already extracted.
|
||||
start_tool = response.find(tokenizer.tool_call_start) + len(tokenizer.tool_call_start)
|
||||
end_tool = response.find(tokenizer.tool_call_end)
|
||||
tool_call = tokenizer.tool_parser(response[start_tool:end_tool].strip())
|
||||
tool_result = tools[tool_call["name"]](**tool_call["arguments"])
|
||||
|
||||
# Put the tool result in the prompt
|
||||
|
||||
+14
-21
@@ -4,10 +4,9 @@ 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 (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
dequantize_model,
|
||||
load,
|
||||
save,
|
||||
upload_to_hub,
|
||||
)
|
||||
@@ -40,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(
|
||||
@@ -62,14 +61,12 @@ def main() -> None:
|
||||
print("Loading pretrained model")
|
||||
args = parse_arguments()
|
||||
|
||||
model_path, hf_path = get_model_path(args.model)
|
||||
model, config, tokenizer = fetch_from_hub(model_path)
|
||||
|
||||
model.freeze()
|
||||
model = load_adapters(model, args.adapter_path)
|
||||
model, tokenizer, config = load(
|
||||
args.model, adapter_path=args.adapter_path, return_config=True
|
||||
)
|
||||
|
||||
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")
|
||||
]
|
||||
@@ -77,19 +74,19 @@ def main() -> None:
|
||||
if fused_linears:
|
||||
model.update_modules(tree_unflatten(fused_linears))
|
||||
|
||||
if args.de_quantize:
|
||||
print("De-quantizing model")
|
||||
model = dequantize(model)
|
||||
if args.dequantize:
|
||||
print("Dequantizing model")
|
||||
model = dequantize_model(model)
|
||||
config.pop("quantization", None)
|
||||
config.pop("quantization_config", None)
|
||||
|
||||
save_path = Path(args.save_path)
|
||||
save(
|
||||
save_path,
|
||||
model_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_path,
|
||||
donate_model=False,
|
||||
)
|
||||
|
||||
@@ -100,13 +97,9 @@ def main() -> None:
|
||||
f"Model type {model_type} not supported for GGUF conversion."
|
||||
)
|
||||
weights = dict(tree_flatten(model.parameters()))
|
||||
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
|
||||
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
|
||||
|
||||
if args.upload_repo is not None:
|
||||
if hf_path is None:
|
||||
raise ValueError(
|
||||
"Must provide original Hugging Face repo to upload local model."
|
||||
)
|
||||
upload_to_hub(args.save_path, args.upload_repo)
|
||||
|
||||
|
||||
|
||||
+1120
-270
File diff suppressed because it is too large
Load Diff
+16
-20
@@ -21,7 +21,7 @@ from .tuner.utils import (
|
||||
load_adapters,
|
||||
print_trainable_parameters,
|
||||
)
|
||||
from .utils import load, save_config
|
||||
from .utils import _parse_size, load, save_config
|
||||
|
||||
yaml_loader = yaml.SafeLoader
|
||||
yaml_loader.add_implicit_resolver(
|
||||
@@ -40,7 +40,7 @@ yaml_loader.add_implicit_resolver(
|
||||
)
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
"model": "mlx_model",
|
||||
"model": "Qwen/Qwen3-0.6b",
|
||||
"train": False,
|
||||
"fine_tune_type": "lora",
|
||||
"optimizer": "adam",
|
||||
@@ -51,7 +51,7 @@ CONFIG_DEFAULTS = {
|
||||
"sgd": {},
|
||||
"adafactor": {},
|
||||
},
|
||||
"data": "data/",
|
||||
"data": "mlx-community/WikiSQL",
|
||||
"seed": 0,
|
||||
"num_layers": 16,
|
||||
"batch_size": 4,
|
||||
@@ -68,10 +68,11 @@ CONFIG_DEFAULTS = {
|
||||
"max_seq_length": 2048,
|
||||
"config": None,
|
||||
"grad_checkpoint": False,
|
||||
"grad_accumulation_steps": 1,
|
||||
"clear_cache_threshold": 0,
|
||||
"lr_schedule": None,
|
||||
"lora_parameters": {"rank": 8, "dropout": 0.0, "scale": 20.0},
|
||||
"mask_prompt": False,
|
||||
"wandb": None, # will be deprecated in a future release
|
||||
"report_to": None,
|
||||
"project_name": None,
|
||||
}
|
||||
@@ -142,6 +143,11 @@ def build_parser():
|
||||
type=int,
|
||||
help="Number of training steps between validations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--grad-accumulation-steps",
|
||||
type=int,
|
||||
help="Number of steps to accumulate before each optimizer update.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume-adapter-file",
|
||||
type=str,
|
||||
@@ -185,14 +191,11 @@ def build_parser():
|
||||
help="Use gradient checkpointing to reduce memory use.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument( # will be deprecated in a future release
|
||||
"--wandb",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"The 'wandb' argument is deprecated and will be removed in a future release. "
|
||||
"Use 'report_to: wandb' and 'project_name' in the configuration instead."
|
||||
),
|
||||
parser.add_argument(
|
||||
"--clear-cache-threshold",
|
||||
type=_parse_size,
|
||||
default=0,
|
||||
help="Clear the allocator cache between steps if it grows too large.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-to",
|
||||
@@ -265,6 +268,7 @@ def train_model(
|
||||
adapter_file=adapter_file,
|
||||
max_seq_length=args.max_seq_length,
|
||||
grad_checkpoint=args.grad_checkpoint,
|
||||
grad_accumulation_steps=args.grad_accumulation_steps,
|
||||
)
|
||||
|
||||
# Initialize the selected optimizer
|
||||
@@ -314,14 +318,6 @@ def evaluate_model(args, model: nn.Module, test_set):
|
||||
|
||||
def run(args, training_callback: TrainingCallback = None):
|
||||
np.random.seed(args.seed)
|
||||
if args.wandb is not None:
|
||||
warnings.warn(
|
||||
"The 'wandb' argument is deprecated and will be removed in a future release. "
|
||||
"Use 'report_to: wandb' and 'project_name' in the configuration instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
args.report_to = "wandb"
|
||||
args.project_name = args.wandb
|
||||
training_callback = get_reporting_callbacks(
|
||||
args.report_to,
|
||||
project_name=args.project_name,
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, List, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -114,7 +115,7 @@ class KlearMLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class KlearSparseMoeBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright © 2023-2026 Apple Inc.
|
||||
|
||||
from functools import partial
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(gate, x):
|
||||
return nn.silu(gate) * x
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def xielu(x, alpha_p, alpha_n, beta, eps):
|
||||
alpha_p = nn.softplus(alpha_p)
|
||||
alpha_n = beta + nn.softplus(alpha_n)
|
||||
return mx.where(
|
||||
x > 0,
|
||||
alpha_p * mx.square(x) + beta * x,
|
||||
(mx.expm1(mx.minimum(x, eps)) - x) * alpha_n + beta * x,
|
||||
)
|
||||
|
||||
|
||||
class XieLU(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
alpha_p_init=0.8,
|
||||
alpha_n_init=0.8,
|
||||
beta=0.5,
|
||||
eps=-1e-6,
|
||||
):
|
||||
super().__init__()
|
||||
alpha_p_tensor = mx.array(alpha_p_init)
|
||||
alpha_n_tensor = mx.array(alpha_n_init - beta)
|
||||
self.alpha_p = mx.log(mx.exp(alpha_p_tensor) - 1)
|
||||
self.alpha_n = mx.log(mx.exp(alpha_n_tensor) - 1)
|
||||
|
||||
self.beta = mx.array(beta)
|
||||
self.eps = mx.array(eps)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return xielu(x, self.alpha_p, self.alpha_n, self.beta, self.eps)
|
||||
@@ -9,6 +9,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ConcatenateKVCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -50,7 +51,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 +80,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
|
||||
@@ -262,11 +263,6 @@ class KVReuseAttention(nn.Module):
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _swiglu(g, x):
|
||||
return nn.silu(g) * x
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -281,7 +277,7 @@ class MLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
g = self.gate_proj(x)
|
||||
x = self.up_proj(x)
|
||||
return self.down_proj(_swiglu(g, x))
|
||||
return self.down_proj(swiglu(g, x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
# 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 .activations import swiglu
|
||||
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(swiglu(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
|
||||
+10
-34
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import XieLU
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -32,38 +33,6 @@ class ModelArgs(BaseModelArgs):
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def xielu(x, alpha_p, alpha_n, beta, eps):
|
||||
alpha_p = nn.softplus(alpha_p)
|
||||
alpha_n = beta + nn.softplus(alpha_n)
|
||||
return mx.where(
|
||||
x > 0,
|
||||
alpha_p * mx.square(x) + beta * x,
|
||||
(mx.expm1(mx.minimum(x, eps)) - x) * alpha_n + beta * x,
|
||||
)
|
||||
|
||||
|
||||
class XieLU(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
alpha_p_init=0.8,
|
||||
alpha_n_init=0.8,
|
||||
beta=0.5,
|
||||
eps=-1e-6,
|
||||
):
|
||||
super().__init__()
|
||||
alpha_p_tensor = mx.array(alpha_p_init)
|
||||
alpha_n_tensor = mx.array(alpha_n_init - beta)
|
||||
self.alpha_p = mx.log(mx.exp(alpha_p_tensor) - 1)
|
||||
self.alpha_n = mx.log(mx.exp(alpha_n_tensor) - 1)
|
||||
|
||||
self.beta = mx.array(beta)
|
||||
self.eps = mx.array(eps)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return xielu(x, self.alpha_p, self.alpha_n, self.beta, self.eps)
|
||||
|
||||
|
||||
class ApertusMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -198,7 +167,8 @@ class Model(nn.Module):
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = ApertusModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -206,12 +176,18 @@ class Model(nn.Module):
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
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):
|
||||
for k, v in weights.items():
|
||||
if k.endswith("alpha_p") or k.endswith("alpha_n"):
|
||||
weights[k] = v.squeeze()
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,8 +6,9 @@ from typing import Any, List, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache, MambaCache, RotatingKVCache
|
||||
from .cache import ArraysCache, CacheList, KVCache, RotatingKVCache
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -140,7 +141,7 @@ class MLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
@@ -222,7 +223,7 @@ class Model(nn.Module):
|
||||
caches = []
|
||||
for i, layer in enumerate(self.model.layers):
|
||||
is_swa = i in self.config.sliding_window_layers
|
||||
conv_cache = MambaCache()
|
||||
conv_cache = ArraysCache(size=2)
|
||||
if is_swa:
|
||||
kv_cache = RotatingKVCache(max_size=self.config.sliding_window)
|
||||
else:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -37,6 +39,7 @@ class ModelArgs(BaseModelArgs):
|
||||
use_qk_norm: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
rotary_dim: Optional[int] = None
|
||||
moe_router_enable_expert_bias: bool = False
|
||||
moe_router_enable_routed_scaling: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
@@ -47,6 +50,13 @@ class ModelArgs(BaseModelArgs):
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def aggregate_expert_outputs(expert_outputs, scores):
|
||||
return (
|
||||
(expert_outputs * scores[..., None]).sum(axis=-2).astype(expert_outputs.dtype)
|
||||
)
|
||||
|
||||
|
||||
class BailingMoeMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
@@ -67,7 +77,7 @@ class BailingMoeMLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class BailingMoeAttention(nn.Module):
|
||||
@@ -94,8 +104,10 @@ class BailingMoeAttention(nn.Module):
|
||||
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)
|
||||
|
||||
if (rope_dim := args.rotary_dim) is None:
|
||||
rope_dim = int(self.head_dim * args.partial_rotary_factor)
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
rope_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
@@ -146,6 +158,7 @@ class BailingMoeAttention(nn.Module):
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
@@ -171,15 +184,15 @@ def group_expert_select(
|
||||
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.stop_gradient(group_idx), mx.array(0.0, scores.dtype), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
inds = mx.argpartition(scores, kth=-k, 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)
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
@@ -245,7 +258,7 @@ class BailingMoeSparseMoeBlock(nn.Module):
|
||||
def __call__(self, x):
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = (out * topk_weight[..., None]).sum(axis=-2)
|
||||
out = aggregate_expert_outputs(out, topk_weight)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
# 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 .activations import swiglu
|
||||
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(swiglu(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
|
||||
+1054
-49
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -109,7 +110,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Optional, Tuple
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
|
||||
@@ -106,7 +107,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -7,6 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import numpy as np
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -107,7 +108,7 @@ class MLP(nn.Module):
|
||||
self.w2 = nn.Linear(ffn_dim, d_model, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
current_hidden_states = nn.silu(self.w1(x)) * self.v1(x)
|
||||
current_hidden_states = swiglu(self.w1(x), self.v1(x))
|
||||
current_hidden_states = self.w2(current_hidden_states)
|
||||
return current_hidden_states
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -120,7 +121,7 @@ class DeepseekMLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
|
||||
@@ -6,8 +6,11 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -258,7 +261,7 @@ class DeepseekV2MLP(nn.Module):
|
||||
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))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -314,13 +317,21 @@ class DeepseekV2MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -355,7 +366,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 +375,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,22 +388,25 @@ 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:
|
||||
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]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -450,6 +440,62 @@ class Model(nn.Module):
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.kv_b_proj = shard_linear(
|
||||
layer.self_attn.kv_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV2MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
return self.model.pipeline_layers
|
||||
|
||||
+183
-159
@@ -7,8 +7,13 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .mla import MultiLinear
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -45,85 +50,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__()
|
||||
@@ -160,11 +86,11 @@ class DeepseekV3Attention(nn.Module):
|
||||
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.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
@@ -175,35 +101,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,
|
||||
@@ -223,29 +133,38 @@ class DeepseekV3Attention(nn.Module):
|
||||
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)
|
||||
kv_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
offset = cache.offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
q_pe = self.rope(q_pe, cache.offset)
|
||||
k_pe = self.rope(k_pe, cache.offset)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys, values = cache.update_and_fetch(
|
||||
mx.concatenate([k_nope, k_pe], axis=-1), values
|
||||
)
|
||||
else:
|
||||
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)
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
queries = mx.concatenate([q_nope, q_pe], axis=-1)
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
@@ -266,7 +185,7 @@ class DeepseekV3MLP(nn.Module):
|
||||
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))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -349,13 +268,21 @@ class DeepseekV3MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(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)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -389,7 +316,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 +325,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,23 +338,25 @@ class DeepseekV3Model(nn.Module):
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0], 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])
|
||||
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]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -471,7 +379,8 @@ class Model(nn.Module):
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
@@ -485,7 +394,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:
|
||||
@@ -509,6 +433,42 @@ class Model(nn.Module):
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.model.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
# Remove multi-token prediction layer and any unused precomputed rotary freqs
|
||||
return {
|
||||
@@ -517,9 +477,73 @@ class Model(nn.Module):
|
||||
if not k.startswith("model.layers.61") and "rotary_emb.inv_freq" not in k
|
||||
}
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV3MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@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,654 @@
|
||||
# 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 mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .mla import MultiLinear
|
||||
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=True,
|
||||
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)
|
||||
k = self.wk(x)
|
||||
k = self.k_norm(k)
|
||||
k = mx.reshape(k, (b, 1, s, self.head_dim))
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
|
||||
q = self.rope(q, offset=offset)
|
||||
k = self.rope(k, offset=offset)
|
||||
|
||||
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, keepdims=True)
|
||||
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.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
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_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
offset = cache[0].offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache[0].update_and_fetch(kv_latent, k_pe)
|
||||
else:
|
||||
cache = [None] * 2
|
||||
|
||||
topk_indices = self.indexer(x, qr, mask, cache=cache[1])
|
||||
if topk_indices is not None:
|
||||
if L == 1:
|
||||
idx = topk_indices[:, :, 0, :, None]
|
||||
kv_latent = mx.take_along_axis(
|
||||
kv_latent,
|
||||
mx.broadcast_to(idx, idx.shape[:-1] + (kv_latent.shape[-1],)),
|
||||
axis=2,
|
||||
)
|
||||
k_pe = mx.take_along_axis(
|
||||
k_pe,
|
||||
mx.broadcast_to(idx, idx.shape[:-1] + (k_pe.shape[-1],)),
|
||||
axis=2,
|
||||
)
|
||||
if mask is not None:
|
||||
mask = mx.take_along_axis(mask, topk_indices, axis=-1)
|
||||
else:
|
||||
shape = list(topk_indices.shape)
|
||||
shape[-1] = kv_latent.shape[2]
|
||||
sparse_mask = mx.zeros(shape, dtype=mx.bool_)
|
||||
sparse_mask = mx.put_along_axis(
|
||||
sparse_mask, topk_indices, mx.array(True), axis=-1
|
||||
)
|
||||
if mask is not None:
|
||||
sparse_mask = sparse_mask & mask
|
||||
mask = sparse_mask
|
||||
# Ensure the indexer cache is evaluated even if the topk_indices are unused
|
||||
# to keep the graph from getting too large
|
||||
if cache is not None and cache[0] is not None:
|
||||
cache[0].keys = mx.depends(cache[0].keys, (cache[1].keys, cache[1].values))
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
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(swiglu(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
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(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)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
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][0].keys = mx.depends(cache[-1][0].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
if pipeline_size > 1:
|
||||
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):
|
||||
# Remove multi-token prediction layers
|
||||
mpt_layer = self.args.num_hidden_layers
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
parts = k.split(".")
|
||||
if len(parts) >= 3 and parts[1] == "layers" and int(parts[2]) >= mpt_layer:
|
||||
continue
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
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)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.model.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV32MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@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,6 +7,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -180,7 +181,7 @@ class Dots1MLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Dots1MoE(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -87,7 +88,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -98,7 +99,7 @@ class Ernie4_5_MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Ernie4_5_MoeMLP(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -91,7 +92,7 @@ class MLP(nn.Module):
|
||||
self.c_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.c_proj(nn.silu(self.c_fc_0(x)) * self.c_fc_1(x))
|
||||
return self.c_proj(swiglu(self.c_fc_0(x), self.c_fc_1(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -102,7 +103,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
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
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
head_dim: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
num_shared_experts: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
sliding_window: int
|
||||
layer_types: List[str]
|
||||
is_moe_layer: List[bool]
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
routed_scaling_factor: float = 2.5
|
||||
norm_topk_prob: bool = True
|
||||
scoring_func: str = "sigmoid"
|
||||
topk_method: str = "noaux_tc"
|
||||
rope_theta: float = 1000000.0
|
||||
rope_scaling: Optional[dict] = None
|
||||
rope_parameters: Optional[dict] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
|
||||
|
||||
@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 + 1e-20)
|
||||
|
||||
scores = scores * routed_scaling_factor
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.n_routed_experts = args.num_experts
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, args.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert args.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 MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(args)
|
||||
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args,
|
||||
intermediate_size=args.moe_intermediate_size * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts is not None and args.num_shared_experts > 0
|
||||
else None
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
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.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.is_sliding_window = args.layer_types[layer_idx] == "sliding_attention"
|
||||
self.apply_rope_all_layers = "sliding_attention" not in args.layer_types
|
||||
self.use_rope = self.is_sliding_window or self.apply_rope_all_layers
|
||||
|
||||
if self.use_rope:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
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
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_norm(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)
|
||||
|
||||
if cache is not None:
|
||||
if self.use_rope:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
elif self.use_rope:
|
||||
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 DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
|
||||
self.self_attn = Attention(args, layer_idx)
|
||||
self.mlp = MoE(args) if args.is_moe_layer[layer_idx] else MLP(args)
|
||||
self.is_sliding_window = self.self_attn.is_sliding_window
|
||||
|
||||
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 = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class ExaoneMoEModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [DecoderLayer(args, idx) for idx in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.swa_idx = None
|
||||
self.ga_idx = None
|
||||
for i, layer in enumerate(self.layers):
|
||||
if layer.is_sliding_window and self.swa_idx is None:
|
||||
self.swa_idx = i
|
||||
if not layer.is_sliding_window and self.ga_idx is None:
|
||||
self.ga_idx = i
|
||||
|
||||
self.window_size = args.sliding_window
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
global_mask = create_attention_mask(
|
||||
h, cache[self.ga_idx] if self.ga_idx is not None else cache[0]
|
||||
)
|
||||
swa_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.swa_idx] if self.swa_idx is not None else cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.is_sliding_window else global_mask
|
||||
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 = ExaoneMoEModel(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,
|
||||
):
|
||||
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):
|
||||
new_weights = {k: v for k, v in weights.items() if not k.startswith("mtp.")}
|
||||
weights = new_weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if not self.args.is_moe_layer[l]:
|
||||
continue
|
||||
|
||||
prefix = f"model.layers.{l}"
|
||||
|
||||
bias_key = f"{prefix}.mlp.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{prefix}.mlp.gate.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
first_key = f"{prefix}.mlp.experts.0.{m}.{k}"
|
||||
last_key = (
|
||||
f"{prefix}.mlp.experts.{self.args.num_experts - 1}.{m}.{k}"
|
||||
)
|
||||
if first_key in weights and last_key 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 self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
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
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.layers:
|
||||
if layer.is_sliding_window:
|
||||
caches.append(
|
||||
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
|
||||
)
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
if isinstance(layer.mlp, MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
if layer.mlp.shared_experts is not None:
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
@@ -0,0 +1,504 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, CacheList, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool = False
|
||||
attention_in_multiplier: float = 1.0
|
||||
attention_out_multiplier: float = 0.9375
|
||||
embedding_multiplier: float = 5.656854249492381
|
||||
head_dim: int = 64
|
||||
hidden_size: int = 1024
|
||||
initializer_range: float = 0.02
|
||||
intermediate_size: int = 2048
|
||||
key_multiplier: float = 0.390625
|
||||
lm_head_multiplier: float = 0.0390625
|
||||
mamba_chunk_size: int = 128
|
||||
mamba_conv_bias: bool = True
|
||||
mamba_d_conv: int = 4
|
||||
mamba_d_head: int = 64
|
||||
mamba_d_ssm: int = 1536
|
||||
mamba_d_state: int = 128
|
||||
mamba_expand: int = 2
|
||||
mamba_n_groups: int = 1
|
||||
mamba_n_heads: int = 24
|
||||
mamba_norm_before_gate: bool = False
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_rms_norm: bool = False
|
||||
mamba_use_mlp: bool = True
|
||||
max_position_embeddings: int = 131072
|
||||
mlp_bias: bool = False
|
||||
mlp_expansion_factor: int = 8
|
||||
mlp_multipliers: List[float] = field(
|
||||
default_factory=lambda: [0.8838834764831844, 0.5859375]
|
||||
)
|
||||
model_type: str = "falcon_h1"
|
||||
num_attention_heads: int = 8
|
||||
num_hidden_layers: int = 36
|
||||
num_key_value_heads: int = 2
|
||||
projectors_bias: bool = False
|
||||
rms_norm_eps: float = 1e-05
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[float] = None
|
||||
rope_theta: float = 100000000000.0
|
||||
ssm_in_multiplier: float = 1.25
|
||||
ssm_multipliers: List[float] = field(
|
||||
default_factory=lambda: [
|
||||
0.3535533905932738,
|
||||
0.25,
|
||||
0.3535533905932738,
|
||||
0.5,
|
||||
0.3535533905932738,
|
||||
]
|
||||
)
|
||||
ssm_out_multiplier: float = 0.23570226039551587
|
||||
vocab_size: int = 32784
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class FalconH1RMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size, eps=1e-6, n_groups=1, norm_before_gate=True):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((hidden_size,))
|
||||
self.variance_epsilon = eps
|
||||
self.n_groups = n_groups
|
||||
self.norm_before_gate = norm_before_gate
|
||||
|
||||
def __call__(self, hidden_states, gate=None):
|
||||
if not self.norm_before_gate and gate is not None:
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
|
||||
hidden_states = mx.fast.rms_norm(
|
||||
hidden_states, self.weight, self.variance_epsilon
|
||||
)
|
||||
|
||||
if self.norm_before_gate and gate is not None:
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
||||
def compute_mup_vector(args):
|
||||
intermediate_size = args.mamba_d_ssm
|
||||
groups_time_state_size = args.mamba_n_groups * args.mamba_d_state
|
||||
num_heads = args.mamba_n_heads
|
||||
sizes = [
|
||||
intermediate_size,
|
||||
intermediate_size,
|
||||
groups_time_state_size,
|
||||
groups_time_state_size,
|
||||
num_heads,
|
||||
]
|
||||
return mx.concatenate(
|
||||
[
|
||||
mx.broadcast_to(mx.array(m), (s,))
|
||||
for s, m in zip(sizes, args.ssm_multipliers)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class FalconH1Attention(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.head_dim, bias=args.attention_bias
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.head_dim, self.hidden_size, bias=args.attention_bias
|
||||
)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
args.rope_traditional,
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(self, x, mask=None, cache=None):
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.num_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_kv_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, mask=mask, scale=self.scale, cache=cache
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class FalconH1Mixer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.num_heads = args.mamba_n_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_d_ssm
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
|
||||
self.layer_norm_epsilon = args.rms_norm_eps
|
||||
self.groups_time_state_size = args.mamba_n_groups * self.ssm_state_size
|
||||
|
||||
self.n_groups = args.mamba_n_groups
|
||||
self.head_dim = args.mamba_d_head
|
||||
self.chunk_size = args.mamba_chunk_size
|
||||
|
||||
self.time_step_limit = (0.0, float("inf"))
|
||||
self.time_step_min = 0.001
|
||||
self.time_step_max = 0.1
|
||||
|
||||
self.conv_dim = self.intermediate_size + 2 * self.n_groups * self.ssm_state_size
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.conv_dim,
|
||||
out_channels=self.conv_dim,
|
||||
bias=self.use_conv_bias,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.conv_dim,
|
||||
)
|
||||
|
||||
projection_size = self.intermediate_size + self.conv_dim + self.num_heads
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
projection_size,
|
||||
bias=args.mamba_proj_bias,
|
||||
)
|
||||
|
||||
self.dt_bias = mx.ones(self.num_heads)
|
||||
|
||||
A = mx.arange(1, self.num_heads + 1)
|
||||
self.A_log = mx.log(A)
|
||||
|
||||
self.mamba_rms_norm = args.mamba_rms_norm
|
||||
if self.mamba_rms_norm:
|
||||
self.norm = FalconH1RMSNormGated(
|
||||
self.intermediate_size,
|
||||
eps=self.layer_norm_epsilon,
|
||||
n_groups=self.n_groups,
|
||||
norm_before_gate=args.mamba_norm_before_gate,
|
||||
)
|
||||
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.projectors_bias
|
||||
)
|
||||
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
t = padded_input.shape[1]
|
||||
ends = mx.clip(cache.lengths, 0, t - n_keep)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(padded_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = padded_input[:, -n_keep:, :]
|
||||
else:
|
||||
padded_input = mx.pad(
|
||||
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
|
||||
)
|
||||
|
||||
conv_output = self.conv1d(padded_input)
|
||||
return nn.silu(conv_output)
|
||||
|
||||
def _ssm(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
hidden_states = hidden_states.reshape(
|
||||
batch_size, seq_len, self.num_heads, self.head_dim
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
lengths,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(self, input_states, cache=None, mask: Optional[mx.array] = None):
|
||||
projected_states = self.in_proj(input_states)
|
||||
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected_states,
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
|
||||
if cache:
|
||||
cache.advance(y.shape[1])
|
||||
|
||||
if self.mamba_rms_norm:
|
||||
y = self.norm(y, gate)
|
||||
else:
|
||||
y = swiglu(gate, y)
|
||||
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class FalconH1MLP(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = args.intermediate_size
|
||||
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x):
|
||||
y = swiglu(self.gate_proj(x), self.up_proj(x))
|
||||
y = self.down_proj(y)
|
||||
return y
|
||||
|
||||
|
||||
class FalconH1DecoderLayer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.feed_forward = FalconH1MLP(args)
|
||||
|
||||
head_dim = args.head_dim
|
||||
self.channels_attn = (
|
||||
args.num_attention_heads * head_dim
|
||||
+ 2 * args.num_key_value_heads * head_dim
|
||||
)
|
||||
|
||||
self.mamba = FalconH1Mixer(args=args)
|
||||
|
||||
self.self_attn = FalconH1Attention(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)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
h: mx.array,
|
||||
cache,
|
||||
attn_mask: Optional[mx.array],
|
||||
mamba_mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
|
||||
residual = h
|
||||
h = self.input_layernorm(h)
|
||||
|
||||
mamba_h = self.mamba(input_states=h, cache=cache[0], mask=mamba_mask)
|
||||
|
||||
attn_h = self.self_attn(
|
||||
h,
|
||||
mask=attn_mask,
|
||||
cache=cache[1],
|
||||
)
|
||||
|
||||
h = residual + mamba_h + attn_h
|
||||
|
||||
residual = h
|
||||
h = self.pre_ff_layernorm(h)
|
||||
h = self.feed_forward(h)
|
||||
return residual + h
|
||||
|
||||
|
||||
class FalconH1Model(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(self.vocab_size, self.hidden_size)
|
||||
|
||||
self._mup_vector = compute_mup_vector(args)
|
||||
self.layers = [
|
||||
FalconH1DecoderLayer(args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.final_layernorm = nn.RMSNorm(self.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
h = h
|
||||
|
||||
if cache is None:
|
||||
cache = [(None, None) * len(self.layers)]
|
||||
|
||||
mamba_mask = create_ssm_mask(h, cache[0][0])
|
||||
attn_mask = create_attention_mask(h, cache[0][1])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(
|
||||
h,
|
||||
cache=c,
|
||||
attn_mask=attn_mask,
|
||||
mamba_mask=mamba_mask,
|
||||
)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = FalconH1Model(args=args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
hidden_states = self.model(inputs, cache=cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(hidden_states)
|
||||
return out * (self.args.lm_head_multiplier / self.args.embedding_multiplier)
|
||||
else:
|
||||
return self.lm_head(hidden_states)
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Check if needs sanitization
|
||||
c1d = weights["model.layers.0.mamba.conv1d.weight"]
|
||||
if c1d.shape[-1] <= c1d.shape[1]:
|
||||
return weights
|
||||
|
||||
sanitized_weights = {}
|
||||
args = self.args
|
||||
|
||||
for name, param in weights.items():
|
||||
# Fold-in multipliers
|
||||
if name.endswith("embed_tokens.weight"):
|
||||
param *= args.embedding_multiplier
|
||||
elif name.endswith("lm_head.weight"):
|
||||
param *= args.lm_head_multiplier
|
||||
elif name.endswith("q_proj.weight") or name.endswith("k_proj.weight"):
|
||||
param *= args.attention_in_multiplier
|
||||
elif name.endswith("key_proj.weight"):
|
||||
param *= args.attention_in_multiplier * args.key_multiplier
|
||||
elif name.endswith("o_proj.weight"):
|
||||
param *= args.attention_out_multiplier
|
||||
elif name.endswith("out_proj.weight"):
|
||||
param *= args.ssm_out_multiplier
|
||||
elif name.endswith("gate_proj.weight"):
|
||||
param *= args.mlp_multipliers[0]
|
||||
elif name.endswith("down_proj.weight"):
|
||||
param *= args.mlp_multipliers[1]
|
||||
elif name.endswith("in_proj.weight"):
|
||||
param *= (
|
||||
args.ssm_in_multiplier
|
||||
* self.model._mup_vector.astype(param.dtype)[:, None]
|
||||
)
|
||||
elif "conv1d.weight" in name:
|
||||
param = param.transpose(0, 2, 1)
|
||||
sanitized_weights[name] = param
|
||||
return sanitized_weights
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
CacheList(ArraysCache(size=2), KVCache())
|
||||
for _ in range(self.args.num_hidden_layers)
|
||||
]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
+119
-55
@@ -7,15 +7,27 @@ 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))
|
||||
|
||||
|
||||
def _make_gated_delta_kernel():
|
||||
def _make_gated_delta_kernel(has_mask=False, vectorized=False):
|
||||
if not mx.metal.is_available():
|
||||
return None
|
||||
source = """
|
||||
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;
|
||||
auto hv_idx = n % Hv;
|
||||
@@ -38,60 +50,80 @@ def _make_gated_delta_kernel():
|
||||
auto o_state = state_out + (n * Dv + dv_idx) * Dk;
|
||||
|
||||
float state[n_per_t];
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = static_cast<float>(i_state[s_idx]);
|
||||
}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
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) {
|
||||
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];
|
||||
kv_mem += state[i] * k_[s_idx];
|
||||
}
|
||||
kv_mem = simd_sum(kv_mem);
|
||||
for (int t = 0; t < T; ++t) {{
|
||||
if ({mask_source}) {{
|
||||
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_access};
|
||||
kv_mem += state[i] * k_[s_idx];
|
||||
}}
|
||||
kv_mem = simd_sum(kv_mem);
|
||||
|
||||
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
|
||||
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
|
||||
|
||||
float out = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] + k_[s_idx] * delta;
|
||||
out += state[i] * q_[s_idx];
|
||||
}
|
||||
out = simd_sum(out);
|
||||
if (thread_index_in_simdgroup == 0) {
|
||||
y[dv_idx] = static_cast<InT>(out);
|
||||
}
|
||||
float out = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] + k_[s_idx] * delta;
|
||||
out += state[i] * q_[s_idx];
|
||||
}}
|
||||
out = simd_sum(out);
|
||||
if (thread_index_in_simdgroup == 0) {{
|
||||
y[dv_idx] = static_cast<InT>(out);
|
||||
}}
|
||||
}} else {{
|
||||
y[dv_idx] = static_cast<InT>(0);
|
||||
}}
|
||||
// Increment data pointers to next time step
|
||||
q_ += Hk * Dk;
|
||||
k_ += Hk * Dk;
|
||||
v_ += Hv * Dv;
|
||||
y += Hv * Dv;
|
||||
g_ += Hv;
|
||||
{g_advance}
|
||||
beta_ += Hv;
|
||||
}
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
}}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
o_state[s_idx] = static_cast<InT>(state[i]);
|
||||
}
|
||||
o_state[s_idx] = static_cast<StT>(state[i]);
|
||||
}}
|
||||
"""
|
||||
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",
|
||||
input_names=["q", "k", "v", "g", "beta", "state_in", "T"],
|
||||
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 = _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
|
||||
def _gated_delta_step_ops(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
@@ -99,6 +131,7 @@ def _gated_delta_step_ops(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""
|
||||
Ops-based reference implementation for a single recurrent step.
|
||||
@@ -106,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]
|
||||
@@ -114,13 +148,24 @@ def _gated_delta_step_ops(
|
||||
"""
|
||||
|
||||
# Decay
|
||||
state = state * g[..., None, None]
|
||||
old_state = state
|
||||
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]
|
||||
return y, state
|
||||
|
||||
if mask is not None:
|
||||
mask = mx.expand_dims(mask, axis=(1, 2, 3))
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y.astype(q.dtype), state
|
||||
|
||||
|
||||
def gated_delta_kernel(
|
||||
@@ -130,14 +175,30 @@ def gated_delta_kernel(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
B, T, Hk, Dk = k.shape
|
||||
Hv, Dv = v.shape[2:]
|
||||
input_type = q.dtype
|
||||
return _gated_delta_kernel(
|
||||
inputs=[q, k, v, g, beta, state, T],
|
||||
state_type = state.dtype
|
||||
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=[
|
||||
("InT", input_type),
|
||||
("StT", state_type),
|
||||
("Dk", Dk),
|
||||
("Dv", Dv),
|
||||
("Hk", Hk),
|
||||
@@ -146,7 +207,7 @@ def gated_delta_kernel(
|
||||
grid=(32, Dv, B * Hv),
|
||||
threadgroup=(32, 4, 1),
|
||||
output_shapes=[(B, T, Hv, Dv), state.shape],
|
||||
output_dtypes=[input_type, input_type],
|
||||
output_dtypes=[input_type, state_type],
|
||||
)
|
||||
|
||||
|
||||
@@ -157,23 +218,26 @@ def gated_delta_ops(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> 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:]
|
||||
if state is None:
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=mx.float32)
|
||||
|
||||
if (repeat_factor := Hv // Hk) > 1:
|
||||
q = mx.repeat(q, repeat_factor, -2)
|
||||
@@ -188,6 +252,7 @@ def gated_delta_ops(
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
None if mask is None else mask[:, t],
|
||||
)
|
||||
ys.append(y)
|
||||
y = mx.stack(ys, axis=1)
|
||||
@@ -203,17 +268,16 @@ def gated_delta_update(
|
||||
A_log: mx.array,
|
||||
dt_bias: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
use_kernel: bool = True,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
|
||||
beta = mx.sigmoid(b)
|
||||
g = compute_g(A_log, a, dt_bias)
|
||||
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=mx.float32)
|
||||
|
||||
if mx.default_device() != mx.gpu or not mx.metal.is_available():
|
||||
return gated_delta_ops(q, k, v, g, beta, state)
|
||||
else:
|
||||
return gated_delta_kernel(q, k, v, g, beta, state)
|
||||
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)
|
||||
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,15 +54,20 @@ 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
|
||||
),
|
||||
)
|
||||
if self.is_sliding:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_local_base_freq,
|
||||
traditional=False,
|
||||
)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -87,8 +94,6 @@ class Attention(nn.Module):
|
||||
keys = self.rope(keys)
|
||||
|
||||
# Sliding window
|
||||
if isinstance(mask, mx.array) and mask.shape[-1] != keys.shape[-2]:
|
||||
mask = mask[..., -keys.shape[-2] :]
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
@@ -189,12 +194,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
|
||||
@@ -246,7 +253,5 @@ class Model(nn.Module):
|
||||
):
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(
|
||||
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
|
||||
)
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import gemma4_text
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "gemma4"
|
||||
text_config: dict = None
|
||||
vocab_size: int = 262144
|
||||
|
||||
def __post_init__(self):
|
||||
if self.text_config is None:
|
||||
self.text_config = {}
|
||||
self.text_config["vocab_size"] = self.vocab_size
|
||||
self.text_config["num_attention_heads"] = self.text_config.get(
|
||||
"num_attention_heads", 8
|
||||
)
|
||||
self.text_config["num_key_value_heads"] = self.text_config.get(
|
||||
"num_key_value_heads", 1
|
||||
)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = gemma4_text.Model(
|
||||
gemma4_text.ModelArgs.from_dict(args.text_config)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs,
|
||||
cache=cache,
|
||||
input_embeddings=input_embeddings,
|
||||
per_layer_inputs=per_layer_inputs,
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
starts_w_model = k.startswith("model.")
|
||||
|
||||
k = k.removeprefix("model.")
|
||||
if k.startswith(
|
||||
(
|
||||
"vision_tower",
|
||||
"multi_modal_projector",
|
||||
"audio_tower",
|
||||
"embed_audio",
|
||||
"embed_vision",
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
if not starts_w_model:
|
||||
new_weights[k] = v
|
||||
continue
|
||||
|
||||
if k.startswith("language_model"):
|
||||
k = k.replace("language_model.", "language_model.model.")
|
||||
|
||||
new_weights[k] = v
|
||||
|
||||
return self.language_model.sanitize(new_weights)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
return self.language_model.quant_predicate
|
||||
|
||||
def make_cache(self):
|
||||
return self.language_model.make_cache()
|
||||
@@ -0,0 +1,688 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, 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, _BaseCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "gemma4_text"
|
||||
hidden_size: int = 1536
|
||||
num_hidden_layers: int = 35
|
||||
intermediate_size: int = 6144
|
||||
num_attention_heads: int = 8
|
||||
head_dim: int = 256
|
||||
global_head_dim: int = 512
|
||||
global_partial_rotary_factor: float = 0.25
|
||||
rms_norm_eps: float = 1e-6
|
||||
vocab_size: int = 262144
|
||||
vocab_size_per_layer_input: int = 262144
|
||||
num_key_value_heads: int = 1
|
||||
num_global_key_value_heads: Optional[int] = None
|
||||
num_kv_shared_layers: int = 20
|
||||
pad_token_id: int = 0
|
||||
hidden_size_per_layer_input: int = 256
|
||||
rope_traditional: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
rope_parameters: Optional[Dict] = None
|
||||
sliding_window: int = 512
|
||||
sliding_window_pattern: int = 5
|
||||
max_position_embeddings: int = 131072
|
||||
attention_k_eq_v: bool = False
|
||||
final_logit_softcapping: float = 30.0
|
||||
use_double_wide_mlp: bool = True
|
||||
enable_moe_block: bool = False
|
||||
num_experts: Optional[int] = None
|
||||
top_k_experts: Optional[int] = None
|
||||
moe_intermediate_size: Optional[int] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is None:
|
||||
self.rope_parameters = {
|
||||
"full_attention": {
|
||||
"partial_rotary_factor": 0.25,
|
||||
"rope_theta": 1000000.0,
|
||||
"rope_type": "proportional",
|
||||
},
|
||||
"sliding_attention": {
|
||||
"partial_rotary_factor": 1.0,
|
||||
"rope_theta": 10000.0,
|
||||
"rope_type": "default",
|
||||
},
|
||||
}
|
||||
if self.layer_types is None:
|
||||
pattern = ["sliding_attention"] * (self.sliding_window_pattern - 1) + [
|
||||
"full_attention"
|
||||
]
|
||||
self.layer_types = (pattern * (self.num_hidden_layers // len(pattern) + 1))[
|
||||
: self.num_hidden_layers
|
||||
]
|
||||
|
||||
|
||||
class RMSNormNoScale(nn.Module):
|
||||
"""RMSNorm without learnable scale."""
|
||||
|
||||
def __init__(self, dim: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return mx.fast.rms_norm(x, None, self.eps)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def logit_softcap(softcap, x):
|
||||
return mx.tanh(x / softcap) * softcap
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _complete_square(x2, y2, xy):
|
||||
return x2 + mx.expand_dims(y2, -1) - 2 * xy
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def geglu(gate, x):
|
||||
return nn.gelu_approx(gate) * x
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int = 0):
|
||||
super().__init__()
|
||||
first_kv_shared_layer_idx = (
|
||||
config.num_hidden_layers - config.num_kv_shared_layers
|
||||
)
|
||||
is_kv_shared_layer = layer_idx >= first_kv_shared_layer_idx > 0
|
||||
use_double_wide = config.use_double_wide_mlp and is_kv_shared_layer
|
||||
intermediate_size = config.intermediate_size * (2 if use_double_wide else 1)
|
||||
|
||||
self.gate_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(intermediate_size, config.hidden_size, bias=False)
|
||||
self.up_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(geglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Router(nn.Module):
|
||||
"""Expert router: norm -> scale -> project -> top-k -> renormalize."""
|
||||
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.eps = config.rms_norm_eps
|
||||
self.proj = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
||||
self.scale = mx.ones((config.hidden_size,))
|
||||
self.per_expert_scale = mx.ones((config.num_experts,))
|
||||
self._root_size = config.hidden_size**-0.5
|
||||
|
||||
def __call__(self, x: mx.array):
|
||||
x = mx.fast.rms_norm(x, self.scale * self._root_size, self.eps)
|
||||
|
||||
expert_scores = self.proj(x)
|
||||
|
||||
top_k_indices = mx.argpartition(
|
||||
expert_scores, kth=-self.config.top_k_experts, axis=-1
|
||||
)
|
||||
top_k_indices = top_k_indices[..., -self.config.top_k_experts :]
|
||||
|
||||
top_k_weights = mx.take_along_axis(expert_scores, top_k_indices, axis=-1)
|
||||
top_k_weights = mx.softmax(top_k_weights, axis=-1)
|
||||
top_k_weights = top_k_weights * self.per_expert_scale[top_k_indices]
|
||||
|
||||
return top_k_indices, top_k_weights
|
||||
|
||||
|
||||
class GeGLU(nn.Module):
|
||||
"""GELU-gated linear unit activation for SwitchGLU."""
|
||||
|
||||
def __call__(self, x, gate):
|
||||
return geglu(gate, x)
|
||||
|
||||
|
||||
class Experts(nn.Module):
|
||||
"""Sparse MoE using SwitchGLU with gather_mm."""
|
||||
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.switch_glu = SwitchGLU(
|
||||
input_dims=config.hidden_size,
|
||||
hidden_dims=config.moe_intermediate_size,
|
||||
num_experts=config.num_experts,
|
||||
activation=GeGLU(),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, top_k_indices: mx.array, top_k_weights: mx.array
|
||||
) -> mx.array:
|
||||
w = mx.expand_dims(top_k_weights, -1)
|
||||
y = self.switch_glu(x, top_k_indices)
|
||||
|
||||
return (w * y).sum(-2)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.layer_idx = layer_idx
|
||||
self.layer_type = config.layer_types[layer_idx]
|
||||
self.is_sliding = self.layer_type == "sliding_attention"
|
||||
self.has_kv = layer_idx < config.num_hidden_layers - config.num_kv_shared_layers
|
||||
|
||||
self.head_dim = (
|
||||
config.global_head_dim
|
||||
if self.layer_type == "full_attention"
|
||||
and hasattr(config, "global_head_dim")
|
||||
and config.global_head_dim
|
||||
else config.head_dim
|
||||
)
|
||||
|
||||
dim = config.hidden_size
|
||||
self.n_heads = config.num_attention_heads
|
||||
|
||||
# K-eq-V for full attention layers (26B/31B models)
|
||||
self.use_k_eq_v = config.attention_k_eq_v and not self.is_sliding
|
||||
if self.use_k_eq_v and config.num_global_key_value_heads is not None:
|
||||
self.n_kv_heads = config.num_global_key_value_heads
|
||||
else:
|
||||
self.n_kv_heads = config.num_key_value_heads
|
||||
|
||||
self.scale = 1.0
|
||||
|
||||
self.q_proj = nn.Linear(dim, self.n_heads * self.head_dim, bias=False)
|
||||
if self.has_kv:
|
||||
self.k_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
|
||||
if not self.use_k_eq_v:
|
||||
self.v_proj = nn.Linear(
|
||||
dim, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(self.n_heads * self.head_dim, dim, bias=False)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
if self.has_kv:
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.v_norm = RMSNormNoScale(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
# RoPE (with partial rotation support)
|
||||
layer_key = "sliding_attention" if self.is_sliding else "full_attention"
|
||||
rope_params = config.rope_parameters.get(layer_key, {})
|
||||
rope_theta = rope_params.get("rope_theta", 10000.0)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.head_dim,
|
||||
traditional=config.rope_traditional,
|
||||
base=rope_theta,
|
||||
scaling_config=rope_params,
|
||||
max_position_embeddings=config.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
shared_kv: Optional[tuple] = None,
|
||||
offset: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.q_proj(x).reshape(B, L, self.n_heads, self.head_dim)
|
||||
queries = self.q_norm(queries)
|
||||
|
||||
if shared_kv is not None:
|
||||
keys, values = shared_kv
|
||||
elif not self.has_kv:
|
||||
raise ValueError(
|
||||
f"Layer {self.layer_idx} is a KV-shared layer but received no shared_kv"
|
||||
)
|
||||
else:
|
||||
keys = self.k_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim)
|
||||
values = keys
|
||||
if not self.use_k_eq_v:
|
||||
values = self.v_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim)
|
||||
|
||||
offset = mx.array(cache.offset) if cache is not None else 0
|
||||
|
||||
keys = self.k_norm(keys)
|
||||
keys = keys.transpose(0, 2, 1, 3)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
values = self.v_norm(values)
|
||||
values = values.transpose(0, 2, 1, 3)
|
||||
|
||||
queries = queries.transpose(0, 2, 1, 3)
|
||||
queries = self.rope(queries, offset=offset)
|
||||
|
||||
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)
|
||||
|
||||
return self.o_proj(output), (keys, values), offset
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.layer_idx = layer_idx
|
||||
self.layer_type = config.layer_types[layer_idx]
|
||||
self.self_attn = Attention(config, layer_idx)
|
||||
self.mlp = MLP(config, layer_idx)
|
||||
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
|
||||
)
|
||||
self.pre_feedforward_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
# MoE (26B model)
|
||||
self.enable_moe = config.enable_moe_block
|
||||
if self.enable_moe:
|
||||
self.router = Router(config)
|
||||
self.experts = Experts(config)
|
||||
self.post_feedforward_layernorm_1 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm_2 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.pre_feedforward_layernorm_2 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
# Per-layer input gating (2B/4B models)
|
||||
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
|
||||
if self.hidden_size_per_layer_input:
|
||||
self.per_layer_input_gate = nn.Linear(
|
||||
config.hidden_size, self.hidden_size_per_layer_input, bias=False
|
||||
)
|
||||
self.per_layer_projection = nn.Linear(
|
||||
self.hidden_size_per_layer_input, config.hidden_size, bias=False
|
||||
)
|
||||
self.post_per_layer_input_norm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
else:
|
||||
self.per_layer_input_gate = None
|
||||
self.per_layer_projection = None
|
||||
self.post_per_layer_input_norm = None
|
||||
|
||||
# Layer scalar
|
||||
self.layer_scalar = mx.ones((1,))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
per_layer_input: Optional[mx.array] = None,
|
||||
shared_kv: Optional[tuple] = None,
|
||||
offset: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
residual = x
|
||||
|
||||
h = self.input_layernorm(x)
|
||||
h, shared_kv, offset = self.self_attn(
|
||||
h, mask, cache, shared_kv=shared_kv, offset=offset
|
||||
)
|
||||
h = self.post_attention_layernorm(h)
|
||||
h = residual + h
|
||||
|
||||
residual = h
|
||||
|
||||
if self.enable_moe:
|
||||
h1 = self.pre_feedforward_layernorm(h)
|
||||
h1 = self.mlp(h1)
|
||||
h1 = self.post_feedforward_layernorm_1(h1)
|
||||
|
||||
top_k_indices, top_k_weights = self.router(h)
|
||||
h2 = self.pre_feedforward_layernorm_2(h)
|
||||
h2 = self.experts(h2, top_k_indices, top_k_weights)
|
||||
h2 = self.post_feedforward_layernorm_2(h2)
|
||||
|
||||
h = h1 + h2
|
||||
else:
|
||||
h = self.pre_feedforward_layernorm(h)
|
||||
h = self.mlp(h)
|
||||
|
||||
h = self.post_feedforward_layernorm(h)
|
||||
h = residual + h
|
||||
|
||||
# Per-layer input gating
|
||||
if (
|
||||
self.per_layer_input_gate is not None
|
||||
and self.per_layer_projection is not None
|
||||
and self.post_per_layer_input_norm is not None
|
||||
and per_layer_input is not None
|
||||
):
|
||||
residual = h
|
||||
gate = self.per_layer_input_gate(h)
|
||||
gate = nn.gelu_approx(gate)
|
||||
gate = mx.multiply(gate, per_layer_input)
|
||||
gate = self.per_layer_projection(gate)
|
||||
gate = self.post_per_layer_input_norm(gate)
|
||||
h = residual + gate
|
||||
|
||||
if self.layer_scalar is not None:
|
||||
h = h * self.layer_scalar
|
||||
|
||||
return h, shared_kv, offset
|
||||
|
||||
|
||||
class Gemma4TextModel(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.vocab_size = config.vocab_size
|
||||
self.window_size = config.sliding_window
|
||||
self.sliding_window_pattern = config.sliding_window_pattern
|
||||
self.num_hidden_layers = config.num_hidden_layers
|
||||
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.embed_scale = config.hidden_size**0.5
|
||||
self.layers = [
|
||||
DecoderLayer(config, layer_idx=i) for i in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
# Per-layer input embeddings (2B/4B models)
|
||||
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
|
||||
if self.hidden_size_per_layer_input:
|
||||
self.embed_tokens_per_layer = nn.Embedding(
|
||||
config.vocab_size_per_layer_input,
|
||||
config.num_hidden_layers * config.hidden_size_per_layer_input,
|
||||
)
|
||||
self.embed_tokens_per_layer_scale = config.hidden_size_per_layer_input**0.5
|
||||
self.per_layer_input_scale = 2.0**-0.5
|
||||
self.per_layer_projection_scale = config.hidden_size**-0.5
|
||||
self.per_layer_model_projection = nn.Linear(
|
||||
config.hidden_size,
|
||||
config.num_hidden_layers * config.hidden_size_per_layer_input,
|
||||
bias=False,
|
||||
)
|
||||
self.per_layer_projection_norm = nn.RMSNorm(
|
||||
config.hidden_size_per_layer_input, eps=config.rms_norm_eps
|
||||
)
|
||||
else:
|
||||
self.embed_tokens_per_layer = None
|
||||
self.per_layer_input_scale = None
|
||||
self.per_layer_projection_scale = None
|
||||
self.per_layer_model_projection = None
|
||||
self.per_layer_projection_norm = None
|
||||
|
||||
# Arrange for shared KVs
|
||||
self.previous_kvs = list(range(len(self.layers)))
|
||||
if config.num_kv_shared_layers > 0:
|
||||
N = len(self.layers)
|
||||
M = N - config.num_kv_shared_layers
|
||||
kvs_by_type = {}
|
||||
for i in range(M):
|
||||
kvs_by_type[self.layers[i].layer_type] = i
|
||||
for j in range(M, N):
|
||||
self.previous_kvs[j] = kvs_by_type[self.layers[j].layer_type]
|
||||
|
||||
def _get_per_layer_inputs(
|
||||
self,
|
||||
input_ids: Optional[mx.array],
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
if input_ids is None:
|
||||
if input_embeddings is None:
|
||||
raise RuntimeError(
|
||||
"input_embeddings must be provided when input_ids are omitted."
|
||||
)
|
||||
|
||||
# Split the sequence dimension if this still holds too much
|
||||
# memory. 260k vocab means the distance tensor would be ~1GB
|
||||
# per 2k tokens in bf16.
|
||||
#
|
||||
# If the embedding is quantized we have to dequantize it anyway to
|
||||
# perform the match test.
|
||||
norms_embedding = self.embed_tokens.weight.square().sum(-1)
|
||||
norms_input = input_embeddings.square().sum(-1)
|
||||
distance = _complete_square(
|
||||
norms_embedding,
|
||||
norms_input,
|
||||
self.embed_tokens.as_linear(input_embeddings),
|
||||
)
|
||||
|
||||
# Checks can be added if needed but they necessarily break the GPU
|
||||
# pipelining and force an eval.
|
||||
#
|
||||
# match_counts = (distance < eps).sum(-1)
|
||||
#
|
||||
input_ids = mx.argmin(distance, -1)
|
||||
|
||||
result = self.embed_tokens_per_layer(input_ids)
|
||||
result = result * self.embed_tokens_per_layer_scale
|
||||
return mx.unflatten(
|
||||
result,
|
||||
-1,
|
||||
(self.config.num_hidden_layers, self.hidden_size_per_layer_input),
|
||||
)
|
||||
|
||||
def _project_per_layer_inputs(
|
||||
self,
|
||||
input_embeddings: mx.array,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
per_layer_projection = self.per_layer_model_projection(input_embeddings)
|
||||
per_layer_projection = per_layer_projection * self.per_layer_projection_scale
|
||||
per_layer_projection = mx.unflatten(
|
||||
per_layer_projection,
|
||||
-1,
|
||||
(self.config.num_hidden_layers, self.hidden_size_per_layer_input),
|
||||
)
|
||||
per_layer_projection = self.per_layer_projection_norm(per_layer_projection)
|
||||
|
||||
if per_layer_inputs is None:
|
||||
return per_layer_projection
|
||||
|
||||
return (per_layer_projection + per_layer_inputs) * self.per_layer_input_scale
|
||||
|
||||
def _make_masks(self, h, cache):
|
||||
mask = {}
|
||||
masks = []
|
||||
for l, c in zip(self.layers, cache):
|
||||
if l.layer_type not in mask:
|
||||
if l.layer_type == "full_attention":
|
||||
mask["full_attention"] = create_attention_mask(h, c)
|
||||
elif l.layer_type == "sliding_attention":
|
||||
mask["sliding_attention"] = create_attention_mask(
|
||||
h, c, window_size=self.window_size
|
||||
)
|
||||
masks.append(mask[l.layer_type])
|
||||
return masks
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
# Make the initial hidden state
|
||||
if input_embeddings is None:
|
||||
input_embeddings = self.embed_tokens(inputs)
|
||||
h = input_embeddings
|
||||
h = h * self.embed_scale
|
||||
|
||||
# Get the extra inputs per layer if we have per layer embeddings
|
||||
if self.hidden_size_per_layer_input:
|
||||
if per_layer_inputs is None:
|
||||
per_layer_inputs = self._get_per_layer_inputs(inputs, input_embeddings)
|
||||
per_layer_inputs = self._project_per_layer_inputs(h, per_layer_inputs)
|
||||
if per_layer_inputs is not None:
|
||||
per_layer_inputs = [
|
||||
per_layer_inputs[:, :, i, :] for i, _ in enumerate(self.layers)
|
||||
]
|
||||
else:
|
||||
per_layer_inputs = [None] * len(self.layers)
|
||||
|
||||
# Make the kv cache list, be sure to append None for all the shared kv
|
||||
# layers
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
else:
|
||||
cache = cache + [None] * (len(self.layers) - len(cache))
|
||||
|
||||
# Apply each layer. We save all intermediate kvs and offset and grab
|
||||
# the previous one for the shared kv layers.
|
||||
masks = self._make_masks(h, cache)
|
||||
intermediates = [(None, None)] * len(self.layers)
|
||||
for idx, (layer, c, mask, prev_idx, per_layer_input) in enumerate(
|
||||
zip(
|
||||
self.layers,
|
||||
cache,
|
||||
masks,
|
||||
self.previous_kvs,
|
||||
per_layer_inputs,
|
||||
)
|
||||
):
|
||||
kvs, offset = intermediates[prev_idx]
|
||||
|
||||
h, kvs, offset = layer(
|
||||
h,
|
||||
mask,
|
||||
c,
|
||||
per_layer_input=per_layer_input,
|
||||
shared_kv=kvs,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
intermediates[idx] = (kvs, offset)
|
||||
|
||||
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 = Gemma4TextModel(args)
|
||||
self.final_logit_softcapping = args.final_logit_softcapping
|
||||
self.tie_word_embeddings = args.tie_word_embeddings
|
||||
if not self.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,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(
|
||||
inputs,
|
||||
cache=cache,
|
||||
input_embeddings=input_embeddings,
|
||||
per_layer_inputs=per_layer_inputs,
|
||||
)
|
||||
if self.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
if self.final_logit_softcapping is not None:
|
||||
out = logit_softcap(self.final_logit_softcapping, out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
sanitized = {}
|
||||
first_kv_shared = self.args.num_hidden_layers - self.args.num_kv_shared_layers
|
||||
for k, v in weights.items():
|
||||
if any(
|
||||
s in k
|
||||
for s in (
|
||||
"self_attn.rotary_emb",
|
||||
"input_max",
|
||||
"input_min",
|
||||
"output_max",
|
||||
"output_min",
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# KV-shared layers reuse K/V from earlier layers — drop their projections
|
||||
if any(
|
||||
s in k
|
||||
for s in (".self_attn.k_proj", ".self_attn.v_proj", ".self_attn.k_norm")
|
||||
):
|
||||
try:
|
||||
layer_idx = int(k.split("layers.")[1].split(".")[0])
|
||||
if layer_idx >= first_kv_shared:
|
||||
continue
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
|
||||
if k.endswith(".experts.gate_up_proj"):
|
||||
base = k.removesuffix(".gate_up_proj")
|
||||
gate, up = map(mx.contiguous, mx.split(v, 2, axis=-2))
|
||||
sanitized[f"{base}.switch_glu.gate_proj.weight"] = gate
|
||||
sanitized[f"{base}.switch_glu.up_proj.weight"] = up
|
||||
continue
|
||||
|
||||
if k.endswith(".experts.down_proj"):
|
||||
base = k.removesuffix(".down_proj")
|
||||
sanitized[f"{base}.switch_glu.down_proj.weight"] = v
|
||||
continue
|
||||
|
||||
sanitized[k] = v
|
||||
|
||||
return sanitized
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router.proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def head_dim(self):
|
||||
return self.args.head_dim
|
||||
|
||||
@property
|
||||
def n_kv_heads(self):
|
||||
return self.args.num_key_value_heads
|
||||
|
||||
def make_cache(self):
|
||||
first_kv_shared = self.args.num_hidden_layers - self.args.num_kv_shared_layers
|
||||
caches = []
|
||||
for i in range(first_kv_shared):
|
||||
if self.args.layer_types[i] == "full_attention":
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(
|
||||
RotatingKVCache(
|
||||
max_size=self.args.sliding_window,
|
||||
keep=0,
|
||||
)
|
||||
)
|
||||
return caches
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -102,7 +103,7 @@ class GLMMLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
x = self.gate_up_proj(x)
|
||||
gate, x = mx.split(x, 2, axis=-1)
|
||||
return self.down_proj(nn.silu(gate) * x)
|
||||
return self.down_proj(swiglu(gate, x))
|
||||
|
||||
|
||||
class GLMBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class Glm4MLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
x = self.gate_up_proj(x)
|
||||
gate, up_states = mx.split(x, 2, axis=-1)
|
||||
return self.down_proj(nn.silu(gate) * up_states)
|
||||
return self.down_proj(swiglu(gate, up_states))
|
||||
|
||||
|
||||
class Glm4Attention(nn.Module):
|
||||
|
||||
+89
-11
@@ -7,8 +7,11 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -121,7 +124,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -204,13 +207,21 @@ class MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(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)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -243,7 +254,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
|
||||
@@ -251,10 +262,6 @@ class LanguageModel(nn.Module):
|
||||
self.layers = [
|
||||
DecoderLayer(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)
|
||||
|
||||
def __call__(
|
||||
@@ -264,13 +271,29 @@ 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
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -313,9 +336,64 @@ class Model(nn.Module):
|
||||
if not k.startswith(f"model.layers.{mpt_layer}")
|
||||
}
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
# Copyright © 2026 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 mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .mla import MultiLinear
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "glm4_moe_lite"
|
||||
vocab_size: int = 154880
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 10240
|
||||
moe_intermediate_size: int = 1536
|
||||
num_hidden_layers: int = 47
|
||||
num_attention_heads: int = 20
|
||||
num_key_value_heads: int = 20
|
||||
n_shared_experts: Optional[int] = 1
|
||||
n_routed_experts: Optional[int] = 64
|
||||
routed_scaling_factor: float = 1.8
|
||||
kv_lora_rank: int = 512
|
||||
q_lora_rank: int = 768
|
||||
qk_rope_head_dim: int = 64
|
||||
qk_nope_head_dim: int = 192
|
||||
v_head_dim: int = 256
|
||||
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 = 4
|
||||
moe_layer_freq: int = 1
|
||||
first_k_dense_replace: int = 1
|
||||
max_position_embeddings: int = 202752
|
||||
rms_norm_eps: float = 1e-5
|
||||
rope_theta: float = 1_000_000.0
|
||||
rope_scaling: Optional[Dict] = None
|
||||
attention_bias: bool = False
|
||||
attention_dropout: float = 0.0
|
||||
partial_rotary_factor: float = 1.0
|
||||
tie_word_embeddings: bool = False
|
||||
num_nextn_predict_layers: int = 1
|
||||
quantization: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class Glm4MoeLiteAttention(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
|
||||
rope_params = config.rope_scaling
|
||||
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
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
else:
|
||||
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=config.rms_norm_eps)
|
||||
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=config.rms_norm_eps)
|
||||
head_dim = self.qk_nope_head_dim + self.v_head_dim
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if rope_params is not None:
|
||||
mscale_all_dim = rope_params.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = rope_params["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
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=rope_params,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q = self.q_proj(x)
|
||||
else:
|
||||
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
|
||||
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_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class Glm4MoeLiteMLP(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(swiglu(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 + 1e-20)
|
||||
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 Glm4MoeLiteMoE(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 = Glm4MoeLiteMLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(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)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Glm4MoeLiteDecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = Glm4MoeLiteAttention(config)
|
||||
use_moe = (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
and layer_idx % config.moe_layer_freq == 0
|
||||
)
|
||||
self.mlp = Glm4MoeLiteMoE(config) if use_moe else Glm4MoeLiteMLP(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 Glm4MoeLiteModel(PipelineMixin, 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 = [
|
||||
Glm4MoeLiteDecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
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] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0], 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 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
|
||||
if pipeline_size > 1:
|
||||
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 = Glm4MoeLiteModel(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 is_mpt_layer(key):
|
||||
subkeys = key.split(".")
|
||||
if len(subkeys) < 3:
|
||||
return False
|
||||
if (
|
||||
subkeys[1] == "layers"
|
||||
and int(subkeys[2]) >= self.args.num_hidden_layers
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if is_mpt_layer(k):
|
||||
continue
|
||||
else:
|
||||
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)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
rank = group.rank()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, Glm4MoeLiteMLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
if getattr(layer.mlp, "shared_experts", None) is not None:
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .deepseek_v32 import Model as DSV32Model
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
index_head_dim: int
|
||||
index_n_heads: int
|
||||
index_topk: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
n_shared_experts: Optional[int]
|
||||
n_routed_experts: Optional[int]
|
||||
routed_scaling_factor: float
|
||||
kv_lora_rank: int
|
||||
q_lora_rank: int
|
||||
qk_rope_head_dim: int
|
||||
v_head_dim: int
|
||||
qk_nope_head_dim: int
|
||||
topk_method: str
|
||||
scoring_func: str
|
||||
norm_topk_prob: bool
|
||||
n_group: int
|
||||
topk_group: int
|
||||
num_experts_per_tok: int
|
||||
moe_layer_freq: int
|
||||
first_k_dense_replace: int
|
||||
max_position_embeddings: int
|
||||
rms_norm_eps: float
|
||||
rope_parameters: Dict
|
||||
attention_bias: bool
|
||||
rope_scaling: Dict = None
|
||||
rope_theta: Optional[float] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.rope_scaling = self.rope_parameters
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
|
||||
|
||||
class Model(DSV32Model):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__(config)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2023 - 2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
@@ -138,7 +138,9 @@ class GPT2Model(nn.Module):
|
||||
if cache[0] is not None:
|
||||
offset = cache[0].offset
|
||||
|
||||
position_ids = mx.arange(offset, offset + L)
|
||||
offset = mx.array(offset)
|
||||
position_ids = mx.arange(L) + offset[..., None]
|
||||
|
||||
hidden_states += self.wpe(position_ids)
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[0])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
@@ -142,8 +143,12 @@ class MLPBlock(nn.Module):
|
||||
bias=True,
|
||||
)
|
||||
self.router = nn.Linear(config.hidden_size, config.num_local_experts, bias=True)
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
g = self.router(x)
|
||||
experts, indices = mlx_topk(g, k=self.num_experts_per_tok, axis=-1)
|
||||
expert_weights = mx.softmax(experts, axis=-1, precise=True)
|
||||
@@ -152,7 +157,13 @@ class MLPBlock(nn.Module):
|
||||
x = self.experts(x, indices)
|
||||
|
||||
x = x * mx.expand_dims(expert_weights, axis=-1)
|
||||
return x.sum(axis=-2)
|
||||
|
||||
y = x.sum(axis=-2)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -268,6 +279,47 @@ class Model(nn.Module):
|
||||
|
||||
return new_weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
R = group.rank()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, sharding="sharded-to-all", group=group
|
||||
)
|
||||
|
||||
layer.self_attn.num_attention_heads //= N
|
||||
layer.self_attn.num_key_value_heads //= N
|
||||
layer.self_attn.num_key_value_groups = (
|
||||
layer.self_attn.num_attention_heads
|
||||
// layer.self_attn.num_key_value_heads
|
||||
)
|
||||
|
||||
layer.self_attn.sinks = layer.self_attn.sinks[
|
||||
layer.self_attn.num_attention_heads
|
||||
* R : layer.self_attn.num_attention_heads
|
||||
* (R + 1)
|
||||
]
|
||||
|
||||
shard_inplace(layer.mlp.experts.gate_proj, "all-to-sharded", group=group)
|
||||
shard_inplace(layer.mlp.experts.down_proj, "sharded-to-all", group=group)
|
||||
layer.mlp.experts.down_proj.bias /= N
|
||||
shard_inplace(
|
||||
layer.mlp.experts.up_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
|
||||
layer.mlp.sharding_group = group
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -104,7 +105,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,13 +6,14 @@ from typing import Any, List, Optional, Tuple
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .ssm import ssm_update
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -20,6 +21,7 @@ from .switch_layers import SwitchGLU
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
# Required fields (no defaults)
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
@@ -29,34 +31,42 @@ class ModelArgs(BaseModelArgs):
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
|
||||
# Scalar multipliers
|
||||
embedding_multiplier: float
|
||||
attention_multiplier: float
|
||||
logits_scaling: float
|
||||
residual_multiplier: float
|
||||
|
||||
# MoE parameters
|
||||
num_local_experts: int
|
||||
num_experts_per_tok: int
|
||||
shared_intermediate_size: int
|
||||
|
||||
# Mamba parameters
|
||||
mamba_n_heads: int
|
||||
mamba_d_head: int
|
||||
mamba_proj_bias: bool
|
||||
mamba_d_state: int
|
||||
mamba_d_conv: int
|
||||
mamba_n_groups: int
|
||||
mamba_conv_bias: bool
|
||||
|
||||
layer_types: List[str]
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
|
||||
# Optional fields (with defaults)
|
||||
# MoE parameters (optional for dense mode)
|
||||
num_local_experts: Optional[int] = None
|
||||
num_experts_per_tok: Optional[int] = None
|
||||
shared_intermediate_size: Optional[int] = None
|
||||
|
||||
# Mamba parameters (optional for non-hybrid mode)
|
||||
mamba_n_heads: Optional[int] = None
|
||||
mamba_d_head: Optional[int] = None
|
||||
mamba_proj_bias: Optional[bool] = None
|
||||
mamba_d_state: Optional[int] = None
|
||||
mamba_d_conv: Optional[int] = None
|
||||
mamba_n_groups: Optional[int] = None
|
||||
mamba_conv_bias: Optional[bool] = None
|
||||
|
||||
# Dense MLP parameters (for non-MoE mode)
|
||||
mlp_bias: bool = False
|
||||
|
||||
# Other optional parameters
|
||||
position_embedding_type: str = "rope"
|
||||
tie_word_embeddings: bool = True
|
||||
time_step_limit: Tuple[float, float] = (0.001, 100.0)
|
||||
|
||||
# Mode flags - inferred from num_local_experts
|
||||
@property
|
||||
def use_moe(self) -> bool:
|
||||
return bool(self.num_local_experts)
|
||||
|
||||
|
||||
class GraniteMoeHybridRMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
@@ -66,7 +76,7 @@ class GraniteMoeHybridRMSNormGated(nn.Module):
|
||||
|
||||
def __call__(self, hidden_states: mx.array, gate: mx.array = None) -> mx.array:
|
||||
if gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
@@ -110,21 +120,36 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
self.intermediate_size, self.hidden_size, bias=args.mamba_proj_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if cache is None or cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
if cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
t = padded_input.shape[1]
|
||||
ends = mx.clip(cache.lengths, 0, t - n_keep)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(padded_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = padded_input[:, -n_keep:, :]
|
||||
else:
|
||||
padded_input = mx.pad(
|
||||
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
|
||||
)
|
||||
|
||||
conv_output = self.conv1d(padded_input)
|
||||
return nn.silu(conv_output)
|
||||
@@ -135,8 +160,8 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
@@ -145,27 +170,34 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
self.D.astype(hidden_states.dtype),
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache] = None,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[ArraysCache] = None,
|
||||
) -> mx.array:
|
||||
|
||||
projected = self.in_proj(hidden_states)
|
||||
@@ -175,11 +207,7 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
@@ -188,10 +216,9 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
y = self._ssm(hidden_states_ssm, B, C, dt, cache, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
@@ -311,7 +338,22 @@ class GraniteMoeHybridSharedMLP(nn.Module):
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gate, up = mx.split(self.input_linear(x), 2, axis=-1)
|
||||
return self.output_linear(nn.silu(gate) * up)
|
||||
return self.output_linear(swiglu(gate, up))
|
||||
|
||||
|
||||
class GraniteMoeHybridMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
mlp_bias = args.mlp_bias
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class GraniteMoeHybridLayer(nn.Module):
|
||||
@@ -319,6 +361,7 @@ class GraniteMoeHybridLayer(nn.Module):
|
||||
super().__init__()
|
||||
self.layer_type = layer_type
|
||||
self.residual_multiplier = args.residual_multiplier
|
||||
self.use_moe = args.use_moe
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
@@ -329,8 +372,14 @@ class GraniteMoeHybridLayer(nn.Module):
|
||||
else:
|
||||
raise ValueError(f"Unknown layer type: {layer_type}")
|
||||
|
||||
self.shared_mlp = GraniteMoeHybridSharedMLP(args)
|
||||
self.block_sparse_moe = GraniteMoeHybridMoE(args)
|
||||
# MoE or dense MLP after attention/mamba
|
||||
if self.use_moe:
|
||||
self.shared_mlp = GraniteMoeHybridSharedMLP(args)
|
||||
self.block_sparse_moe = GraniteMoeHybridMoE(args)
|
||||
else:
|
||||
# Dense MLP mode
|
||||
self.mlp = GraniteMoeHybridMLP(args)
|
||||
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
@@ -352,13 +401,16 @@ class GraniteMoeHybridLayer(nn.Module):
|
||||
|
||||
hidden_states = residual + hidden_states * self.residual_multiplier
|
||||
|
||||
# Second block: MoE + shared_mlp
|
||||
# Second block: MoE + shared_mlp OR dense MLP
|
||||
residual = hidden_states
|
||||
normed = self.post_attention_layernorm(hidden_states)
|
||||
|
||||
moe_out = self.block_sparse_moe(normed)
|
||||
shared_out = self.shared_mlp(normed)
|
||||
mlp_out = moe_out + shared_out
|
||||
if self.use_moe:
|
||||
moe_out = self.block_sparse_moe(normed)
|
||||
shared_out = self.shared_mlp(normed)
|
||||
mlp_out = moe_out + shared_out
|
||||
else:
|
||||
mlp_out = self.mlp(normed)
|
||||
|
||||
hidden_states = residual + mlp_out * self.residual_multiplier
|
||||
|
||||
@@ -375,9 +427,16 @@ class GraniteMoeHybridModel(nn.Module):
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.embedding_multiplier = args.embedding_multiplier
|
||||
self.fa_idx = args.layer_types.index("attention")
|
||||
self.ssm_idx = args.layer_types.index("mamba")
|
||||
self.layer_types = args.layer_types
|
||||
|
||||
# Handle hybrid vs non-hybrid mode
|
||||
self.fa_idx = (
|
||||
args.layer_types.index("attention")
|
||||
if "attention" in args.layer_types
|
||||
else None
|
||||
)
|
||||
self.ssm_idx = (
|
||||
args.layer_types.index("mamba") if "mamba" in args.layer_types else None
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -389,11 +448,16 @@ class GraniteMoeHybridModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
mamba_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
# Create masks based on what layer types exist
|
||||
attn_mask = None
|
||||
mamba_mask = None
|
||||
|
||||
cache_counter = 0
|
||||
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
|
||||
if self.fa_idx is not None:
|
||||
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
if self.ssm_idx is not None:
|
||||
mamba_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.layer_type == "attention" else mamba_mask
|
||||
hidden_states = layer(hidden_states, mask=mask, cache=c)
|
||||
|
||||
@@ -432,7 +496,7 @@ class Model(nn.Module):
|
||||
caches = []
|
||||
for layer in self.layers:
|
||||
if layer.layer_type == "mamba":
|
||||
caches.append(MambaCache())
|
||||
caches.append(ArraysCache(size=2))
|
||||
elif layer.layer_type == "attention":
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
@@ -443,8 +507,11 @@ class Model(nn.Module):
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
# Handle MoE weight transformation to SwitchGLU format
|
||||
if "model.layers.0.block_sparse_moe.input_linear.weight" in weights:
|
||||
# Handle MoE weight transformation to SwitchGLU format (only for MoE models)
|
||||
if (
|
||||
self.args.use_moe
|
||||
and "model.layers.0.block_sparse_moe.input_linear.weight" in weights
|
||||
):
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.block_sparse_moe"
|
||||
|
||||
@@ -461,12 +528,31 @@ class Model(nn.Module):
|
||||
f"{prefix}.output_linear.weight"
|
||||
)
|
||||
|
||||
# Handle dense MLP weight transformation (for dense models)
|
||||
elif (
|
||||
not self.args.use_moe
|
||||
and "model.layers.0.shared_mlp.input_linear.weight" in weights
|
||||
):
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.shared_mlp"
|
||||
|
||||
# Transform shared_mlp weights to standard mlp weights
|
||||
input_weight = weights.pop(f"{prefix}.input_linear.weight")
|
||||
# Split into gate and up projections (each half)
|
||||
gate_proj, up_proj = mx.split(input_weight, 2, axis=0)
|
||||
weights[f"model.layers.{l}.mlp.gate_proj.weight"] = gate_proj
|
||||
weights[f"model.layers.{l}.mlp.up_proj.weight"] = up_proj
|
||||
|
||||
weights[f"model.layers.{l}.mlp.down_proj.weight"] = weights.pop(
|
||||
f"{prefix}.output_linear.weight"
|
||||
)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router.layer"):
|
||||
if self.args.use_moe and path.endswith("router.layer"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -92,7 +93,7 @@ class HeliumMLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class HeliumDecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Tuple, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -148,7 +149,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -144,7 +145,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -156,7 +157,7 @@ class MLP(nn.Module):
|
||||
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
||||
return self.w2(swiglu(self.w1(x), self.w3(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -154,7 +155,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _compute_gate(query: mx.array, weight: mx.array, bias: mx.array) -> mx.array:
|
||||
gate_logits = query @ weight[:, None, :].swapaxes(-1, -2)
|
||||
gate_logits = gate_logits + bias[..., None, None]
|
||||
return mx.sigmoid(gate_logits)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _silu_mul(gate: mx.array, up: mx.array) -> mx.array:
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _mix_attention(
|
||||
gate: mx.array, attn_global: mx.array, attn_local: mx.array
|
||||
) -> mx.array:
|
||||
return gate * attn_global + (1 - gate) * attn_local
|
||||
|
||||
|
||||
@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: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int = 131072
|
||||
attention_bias: bool = False
|
||||
mlp_bias: bool = False
|
||||
rope_theta: float = 500000.0
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
loop_num: int = 2
|
||||
loop_window_size: int = 64
|
||||
|
||||
|
||||
class LoopGateProjection(nn.Module):
|
||||
def __init__(self, num_heads: int, head_dim: int):
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = head_dim
|
||||
self.weight = mx.zeros((num_heads, head_dim))
|
||||
self.bias = mx.zeros((num_heads,))
|
||||
|
||||
def __call__(self, query: mx.array) -> mx.array:
|
||||
return _compute_gate(query, self.weight, self.bias)
|
||||
|
||||
|
||||
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
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=args.attention_bias)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
head_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def get_qkv(
|
||||
self, x: mx.array, offset: int = 0
|
||||
) -> Tuple[mx.array, mx.array, mx.array]:
|
||||
B, L, _ = x.shape
|
||||
queries = self.q_proj(x).reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = self.k_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = self.v_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
return queries, keys, values
|
||||
|
||||
def attention(
|
||||
self,
|
||||
queries: mx.array,
|
||||
keys: mx.array,
|
||||
values: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
return scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
|
||||
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=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(_silu_mul(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class IQuestLoopCoderModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
assert args.loop_num == 2, f"Only loop_num=2 is supported, got {args.loop_num}"
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gate_projections = [
|
||||
LoopGateProjection(args.num_attention_heads, args.head_dim)
|
||||
for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.loop_num = args.loop_num
|
||||
self.loop_window_size = args.loop_window_size
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
):
|
||||
B, L = inputs.shape[:2]
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * (2 * len(self.layers))
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
window_mask = create_attention_mask(
|
||||
h, cache[len(self.layers)], window_size=self.loop_window_size
|
||||
)
|
||||
|
||||
loop1_kv = []
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q1, k1, v1 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
|
||||
if c is not None:
|
||||
k1, v1 = c.update_and_fetch(k1, v1)
|
||||
loop1_kv.append((k1, v1))
|
||||
|
||||
out = layer.self_attn.attention(q1, k1, v1, mask, cache=c)
|
||||
r = layer.self_attn.o_proj(out.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
for layer, gate_proj, c, (k1, v1) in zip(
|
||||
self.layers, self.gate_projections, cache[len(self.layers) :], loop1_kv
|
||||
):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q2, k2, v2 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
gate = gate_proj(q2)
|
||||
attn_global = layer.self_attn.attention(q2, k1, v1, mask, cache=c)
|
||||
|
||||
if c is not None:
|
||||
k2, v2 = c.update_and_fetch(k2, v2)
|
||||
attn_local = layer.self_attn.attention(
|
||||
q2,
|
||||
k2,
|
||||
v2,
|
||||
window_mask,
|
||||
cache=c,
|
||||
)
|
||||
|
||||
mixed = _mix_attention(gate, attn_global, attn_local)
|
||||
r = layer.self_attn.o_proj(mixed.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
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 = IQuestLoopCoderModel(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 shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
|
||||
for i, layer in enumerate(self.model.layers):
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
gate_proj = self.model.gate_projections[i]
|
||||
heads_per_rank = gate_proj.num_heads // N
|
||||
start = rank * heads_per_rank
|
||||
end = start + heads_per_rank
|
||||
gate_proj.weight = gate_proj.weight[start:end, :]
|
||||
gate_proj.bias = gate_proj.bias[start:end]
|
||||
gate_proj.num_heads = heads_per_rank
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [KVCache() for _ in self.layers] + [
|
||||
RotatingKVCache(max_size=self.args.loop_window_size) for _ in self.layers
|
||||
]
|
||||
@@ -0,0 +1,385 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attn_layer_offset: int
|
||||
attn_layer_period: int
|
||||
expert_layer_offset: int
|
||||
expert_layer_period: int
|
||||
mamba_d_conv: int
|
||||
mamba_d_state: int
|
||||
mamba_expand: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
vocab_size: int
|
||||
mamba_dt_rank: Union[str, int] = "auto"
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_conv_bias: bool = True
|
||||
layers_block_type: Optional[List[str]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if self.mamba_dt_rank == "auto":
|
||||
self.mamba_dt_rank = math.ceil(self.hidden_size / 16)
|
||||
if self.layers_block_type is None:
|
||||
self.layers_block_type = [
|
||||
(
|
||||
"attention"
|
||||
if i % self.attn_layer_period == self.attn_layer_offset
|
||||
else "mamba"
|
||||
)
|
||||
for i in range(self.num_hidden_layers)
|
||||
]
|
||||
|
||||
|
||||
class JambaMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class JambaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.hidden_size // args.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
args.num_attention_heads * self.head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
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)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def fma(a, b, c):
|
||||
return a * b + c
|
||||
|
||||
|
||||
class JambaMambaMixer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_expand * args.hidden_size
|
||||
self.time_step_rank = args.mamba_dt_rank
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
self.use_bias = args.mamba_proj_bias
|
||||
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size, self.intermediate_size * 2, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.intermediate_size,
|
||||
out_channels=self.intermediate_size,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.intermediate_size,
|
||||
bias=self.use_conv_bias,
|
||||
padding=0,
|
||||
)
|
||||
self.x_proj = nn.Linear(
|
||||
self.intermediate_size,
|
||||
self.time_step_rank + self.ssm_state_size * 2,
|
||||
bias=False,
|
||||
)
|
||||
self.dt_proj = nn.Linear(self.time_step_rank, self.intermediate_size, bias=True)
|
||||
|
||||
A = mx.repeat(
|
||||
mx.arange(1.0, self.ssm_state_size + 1.0).reshape([1, self.ssm_state_size]),
|
||||
repeats=self.intermediate_size,
|
||||
axis=0,
|
||||
)
|
||||
self.A_log = mx.log(A)
|
||||
self.D = mx.ones([self.intermediate_size])
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.dt_layernorm = nn.RMSNorm(self.time_step_rank, eps=args.rms_norm_eps)
|
||||
self.b_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
self.c_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
|
||||
def ssm_step(self, x, A, state=None):
|
||||
T = x.shape[1]
|
||||
D = self.D
|
||||
deltaBC = self.x_proj(x)
|
||||
delta, B, C = mx.split(
|
||||
deltaBC,
|
||||
[self.time_step_rank, self.time_step_rank + self.ssm_state_size],
|
||||
axis=-1,
|
||||
)
|
||||
delta, B, C = self.dt_layernorm(delta), self.b_layernorm(B), self.c_layernorm(C)
|
||||
delta = nn.softplus(self.dt_proj(delta))
|
||||
new_state = mx.expand_dims(delta * x, -1) * mx.expand_dims(B, -2)
|
||||
dtA = mx.exp(mx.expand_dims(delta, -1) * A)
|
||||
|
||||
# TODO, speed up prefill with chunked scan
|
||||
for t in range(T):
|
||||
if state is not None:
|
||||
new_state[:, t] = fma(state, dtA[:, t], new_state[:, t])
|
||||
state = new_state[:, t]
|
||||
y = (new_state @ mx.expand_dims(C, -1)).squeeze(-1)
|
||||
y = y + D * x
|
||||
return y, new_state[:, -1]
|
||||
|
||||
def _process_sequence(self, x, conv_state, ssm_state):
|
||||
xz = self.in_proj(x)
|
||||
x, z = xz.split(indices_or_sections=2, axis=-1)
|
||||
K = self.conv_kernel_size
|
||||
if conv_state is not None:
|
||||
x_full = mx.concatenate([conv_state, x], axis=1)
|
||||
else:
|
||||
x_full = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
|
||||
conv_out = self.conv1d(x_full)
|
||||
conv_state = x_full[:, -(K - 1) :, :]
|
||||
x = nn.silu(conv_out)
|
||||
A = -mx.exp(self.A_log)
|
||||
y, ssm_state = self.ssm_step(x, A, ssm_state)
|
||||
z = self.out_proj(swiglu(z, y))
|
||||
return z, (conv_state, ssm_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
if cache is None:
|
||||
conv_state, ssm_state = None, None
|
||||
else:
|
||||
conv_state, ssm_state = cache[0], cache[1]
|
||||
|
||||
output, (conv_state, ssm_state) = self._process_sequence(
|
||||
x, conv_state, ssm_state
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = conv_state
|
||||
cache[1] = ssm_state
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class JambaSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
|
||||
self.router = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, args.num_experts
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.stop_gradient(mx.argpartition(-gates, kth=k - 1, axis=-1)[..., :k])
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
scores = mx.softmax(scores, axis=-1, precise=True)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
return y
|
||||
|
||||
|
||||
class JambaDecoderLayer(nn.Module):
|
||||
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)
|
||||
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)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
if self.is_attn:
|
||||
h = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
h = self.mamba(self.input_layernorm(x), cache)
|
||||
r = x + h
|
||||
out = r + self.feed_forward(self.pre_ff_layernorm(r))
|
||||
return out
|
||||
|
||||
|
||||
class JambaModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
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")
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attn else ssm_mask
|
||||
h = layer(h, mask=mask, cache=c)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.model_type = args.model_type
|
||||
self.args = args
|
||||
self.model = JambaModel(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.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.model.layers:
|
||||
if layer.is_attn:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(ArraysCache(size=2))
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
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)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
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
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .deepseek_v3 import DeepseekV3Model
|
||||
from .deepseek_v3 import Model as DeepseekV3LM
|
||||
from .deepseek_v3 import ModelArgs as TextConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
text_config: Union[TextConfig, dict]
|
||||
model_type: str = "kimi_k25"
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.text_config, dict):
|
||||
self.text_config = TextConfig.from_dict(self.text_config)
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, config: TextConfig):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model = DeepseekV3Model(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)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.language_model = LanguageModel(config.text_config)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
return self.language_model(inputs, cache)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights.pop("vision_model", None)
|
||||
weights.pop("multi_modal_projector", None)
|
||||
weights.pop("mm_projector", None)
|
||||
lm_weights = dict(tree_flatten(weights["language_model"]))
|
||||
lm_weights = DeepseekV3LM.sanitize(self.language_model, lm_weights)
|
||||
weights["language_model"] = tree_unflatten(list(lm_weights.items()))
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
DeepseekV3LM.shard(self.language_model, group)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self.language_model.model
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,611 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .gated_delta import gated_delta_update
|
||||
from .mla import MultiLinear
|
||||
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(swiglu(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.kv_lora_rank = args.kv_lora_rank
|
||||
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.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, args.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
args.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, hidden, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
q = self.q_proj(x).reshape(B, L, self.num_heads, self.q_head_dim)
|
||||
q = q.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_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
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,
|
||||
state: Optional[mx.array],
|
||||
mask: Optional[mx.array],
|
||||
lengths: Optional[mx.array],
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if mask is not None:
|
||||
x = mx.where(mask[..., None], x, 0)
|
||||
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(x.shape[0], self.kernel_size - 1, x.shape[-1]), dtype=x.dtype
|
||||
)
|
||||
conv_input = mx.concatenate([state, x], axis=1)
|
||||
out = nn.silu(self.conv(conv_input))
|
||||
n_keep = self.kernel_size - 1
|
||||
if lengths is not None:
|
||||
ends = mx.clip(lengths, 0, x.shape[1])
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
new_state = mx.take_along_axis(conv_input, positions, axis=1)
|
||||
else:
|
||||
new_state = mx.contiguous(conv_input[:, -n_keep:, :])
|
||||
|
||||
return out, new_state
|
||||
|
||||
|
||||
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:
|
||||
q_state, k_state, v_state, ssm_state = cache
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
q_state = None
|
||||
k_state = None
|
||||
v_state = None
|
||||
ssm_state = None
|
||||
lengths = None
|
||||
|
||||
if q_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
|
||||
|
||||
q_conv, q_state = self.q_conv(self.q_proj(x), q_state, mask, lengths)
|
||||
k_conv, k_state = self.k_conv(self.k_proj(x), k_state, mask, lengths)
|
||||
v_conv, v_state = self.v_conv(self.v_proj(x), v_state, mask, lengths)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = q_state
|
||||
cache[1] = k_state
|
||||
cache[2] = 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)
|
||||
|
||||
inv_scale = self.scale
|
||||
q = (inv_scale**2) * mx.fast.rms_norm(q, None, 1e-6)
|
||||
k = inv_scale * mx.fast.rms_norm(k, None, 1e-6)
|
||||
|
||||
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[3] = ssm_state
|
||||
cache.advance(T)
|
||||
|
||||
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], return_array=True)
|
||||
|
||||
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(ArraysCache(size=4))
|
||||
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,))
|
||||
|
||||
attn_prefix = f"{prefix}.self_attn"
|
||||
kv_b_key = f"{attn_prefix}.kv_b_proj.weight"
|
||||
if kv_b_key in weights:
|
||||
qk_nope = self.args.qk_nope_head_dim or self.args.head_dim
|
||||
v_head = self.args.v_head_dim or self.args.head_dim
|
||||
head_dim = qk_nope + v_head
|
||||
num_heads = self.args.num_attention_heads
|
||||
|
||||
quantized = f"{attn_prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(kv_b_key)
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{attn_prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{attn_prefix}.kv_b_proj.biases")
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(v[:, :qk_nope, :].swapaxes(-1, -2))
|
||||
wv = mx.contiguous(v[:, qk_nope:, :])
|
||||
|
||||
if quantized:
|
||||
wk, wk_s, wk_b = mx.quantize(wk, bits=bits, group_size=group_size)
|
||||
wv, wv_s, wv_b = mx.quantize(wv, bits=bits, group_size=group_size)
|
||||
weights[f"{attn_prefix}.embed_q.scales"] = wk_s
|
||||
weights[f"{attn_prefix}.embed_q.biases"] = wk_b
|
||||
weights[f"{attn_prefix}.unembed_out.scales"] = wv_s
|
||||
weights[f"{attn_prefix}.unembed_out.biases"] = wv_b
|
||||
|
||||
weights[f"{attn_prefix}.embed_q.weight"] = wk
|
||||
weights[f"{attn_prefix}.unembed_out.weight"] = wv
|
||||
|
||||
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
|
||||
@@ -18,11 +18,6 @@ class ModelArgs(BaseModelArgs):
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
self.text_config["full_attn_idxs"] = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.text_config["layer_types"])
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
+38
-13
@@ -5,6 +5,7 @@ from typing import Any, List, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
@@ -31,8 +32,22 @@ class ModelArgs(BaseModelArgs):
|
||||
block_multiple_of: int
|
||||
block_ffn_dim_multiplier: float
|
||||
block_auto_adjust_ff_dim: bool
|
||||
full_attn_idxs: List[int]
|
||||
rope_theta: float
|
||||
rope_theta: float = 1000000.0
|
||||
rope_parameters: Optional[dict] = None
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
@@ -122,23 +137,33 @@ class ShortConv(nn.Module):
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
seqlen = x.shape[1]
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
state = None
|
||||
if cache is not None:
|
||||
state = cache[0]
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
|
||||
)
|
||||
|
||||
Bx = mx.concatenate([state, Bx], axis=-2)
|
||||
if cache is not None:
|
||||
cache[0] = Bx[:, -(self.L_cache - 1) :]
|
||||
if cache[0] is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size),
|
||||
dtype=Bx.dtype,
|
||||
)
|
||||
else:
|
||||
state = cache[0]
|
||||
Bx = mx.concatenate([state, Bx], axis=1)
|
||||
n_keep = self.L_cache - 1
|
||||
t = x.shape[1]
|
||||
if cache.lengths is not None:
|
||||
ends = mx.clip(cache.lengths, 0, t)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(Bx, positions, axis=1)
|
||||
else:
|
||||
cache[0] = Bx[:, -n_keep:, :]
|
||||
cache.advance(t)
|
||||
else:
|
||||
Bx = mx.pad(Bx, [(0, 0), (self.L_cache - 1, 0), (0, 0)])
|
||||
|
||||
conv_out = self.conv(Bx)
|
||||
|
||||
y = C * conv_out
|
||||
@@ -166,7 +191,7 @@ class MLP(nn.Module):
|
||||
self.w2 = nn.Linear(ff_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
||||
return self.w2(swiglu(self.w1(x), self.w3(x)))
|
||||
|
||||
|
||||
class Lfm2DecoderLayer(nn.Module):
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
use_expert_bias: bool
|
||||
num_dense_layers: int
|
||||
norm_eps: float
|
||||
conv_bias: bool
|
||||
conv_L_cache: int
|
||||
rope_theta: float = 1000000.0
|
||||
rope_parameters: Optional[dict] = None
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
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.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
self.k_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
|
||||
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.out_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
queries = self.q_layernorm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_layernorm(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)
|
||||
|
||||
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, mask=mask, scale=self.scale
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
class ShortConv(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
layer_idx: int,
|
||||
):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.layer_idx = layer_idx
|
||||
self.L_cache = args.conv_L_cache
|
||||
self.bias = args.conv_bias
|
||||
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=args.hidden_size,
|
||||
out_channels=args.hidden_size,
|
||||
kernel_size=self.L_cache,
|
||||
groups=args.hidden_size,
|
||||
bias=self.bias,
|
||||
)
|
||||
self.in_proj = nn.Linear(args.hidden_size, 3 * args.hidden_size, bias=self.bias)
|
||||
self.out_proj = nn.Linear(args.hidden_size, args.hidden_size, bias=self.bias)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size),
|
||||
dtype=Bx.dtype,
|
||||
)
|
||||
else:
|
||||
state = cache[0]
|
||||
Bx = mx.concatenate([state, Bx], axis=1)
|
||||
n_keep = self.L_cache - 1
|
||||
t = x.shape[1]
|
||||
if cache.lengths is not None:
|
||||
ends = mx.clip(cache.lengths, 0, t)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(Bx, positions, axis=1)
|
||||
else:
|
||||
cache[0] = Bx[:, -n_keep:, :]
|
||||
cache.advance(t)
|
||||
else:
|
||||
Bx = mx.pad(Bx, [(0, 0), (self.L_cache - 1, 0), (0, 0)])
|
||||
|
||||
conv_out = self.conv(Bx)
|
||||
|
||||
y = C * conv_out
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.hidden_size = config.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) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Lfm2MoeSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
intermediate_size = args.moe_intermediate_size
|
||||
|
||||
self.num_experts = num_experts = args.num_experts
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.use_expert_bias = args.use_expert_bias
|
||||
|
||||
self.gate = nn.Linear(dim, num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(dim, intermediate_size, num_experts)
|
||||
if self.use_expert_bias:
|
||||
self.expert_bias = mx.zeros((self.num_experts,))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
):
|
||||
gates = self.gate(x).astype(mx.float32)
|
||||
gates = mx.softmax(gates, axis=-1)
|
||||
|
||||
if self.use_expert_bias:
|
||||
gates += self.expert_bias
|
||||
|
||||
k = self.top_k
|
||||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||||
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
if self.norm_topk_prob:
|
||||
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 Lfm2DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_attention_layer = layer_idx in args.full_attn_idxs
|
||||
|
||||
if self.is_attention_layer:
|
||||
self.self_attn = Attention(args)
|
||||
else:
|
||||
self.conv = ShortConv(args, layer_idx)
|
||||
self.feed_forward = (
|
||||
MLP(
|
||||
config=args,
|
||||
intermediate_size=args.intermediate_size,
|
||||
)
|
||||
if layer_idx < args.num_dense_layers
|
||||
else Lfm2MoeSparseMoeBlock(args)
|
||||
)
|
||||
|
||||
self.operator_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
self.ffn_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
|
||||
if self.is_attention_layer:
|
||||
r = self.self_attn(self.operator_norm(x), mask=mask, cache=cache)
|
||||
else:
|
||||
r = self.conv(
|
||||
self.operator_norm(x),
|
||||
mask=mask,
|
||||
cache=cache,
|
||||
)
|
||||
h = x + r
|
||||
out = h + self.feed_forward(self.ffn_norm(h))
|
||||
return out
|
||||
|
||||
|
||||
class Lfm2Model(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.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Lfm2DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
|
||||
self.embedding_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
self.fa_idx = args.full_attn_idxs[0]
|
||||
self.conv_idx = 0
|
||||
for i in range(args.num_hidden_layers):
|
||||
if i in args.full_attn_idxs:
|
||||
self.conv_idx += 1
|
||||
else:
|
||||
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)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
conv_mask = create_ssm_mask(h, cache[self.conv_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attention_layer else conv_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.embedding_norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Lfm2Model(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
sanitized_weights = {}
|
||||
for name, param in weights.items():
|
||||
if "conv.weight" in name:
|
||||
if param.shape[-1] > param.shape[1]:
|
||||
param = param.transpose(0, 2, 1)
|
||||
replacements = {
|
||||
"w1.weight": "gate_proj.weight",
|
||||
"w2.weight": "down_proj.weight",
|
||||
"w3.weight": "up_proj.weight",
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
if old in name:
|
||||
name = name.replace(old, new)
|
||||
sanitized_weights[name] = param
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Only sanitize MoE layer weights
|
||||
for n in ["gate_proj", "down_proj", "up_proj"]:
|
||||
if f"{prefix}.feed_forward.experts.0.{n}.weight" in sanitized_weights:
|
||||
to_join = [
|
||||
sanitized_weights.pop(
|
||||
f"{prefix}.feed_forward.experts.{e}.{n}.weight"
|
||||
)
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
sanitized_weights[
|
||||
f"{prefix}.feed_forward.switch_mlp.{n}.weight"
|
||||
] = mx.stack(to_join)
|
||||
return sanitized_weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
KVCache() if l.is_attention_layer else ArraysCache(size=1)
|
||||
for l in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("feed_forward.gate"):
|
||||
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
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -87,7 +88,7 @@ class Lille130mMLP(nn.Module):
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
h = self.norm(x)
|
||||
return self.down_proj(nn.silu(self.gate_proj(h)) * self.up_proj(h))
|
||||
return self.down_proj(swiglu(self.gate_proj(h), self.up_proj(h)))
|
||||
|
||||
|
||||
class Lille130Block(nn.Module):
|
||||
|
||||
+71
-7
@@ -1,12 +1,15 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@@ -28,11 +31,16 @@ class ModelArgs(BaseModelArgs):
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: 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
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
@@ -110,14 +118,15 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
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)
|
||||
@@ -145,12 +154,21 @@ class LlamaModel(nn.Module):
|
||||
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
|
||||
assert self.vocab_size > 0
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_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,
|
||||
@@ -166,10 +184,15 @@ class LlamaModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
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
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
for layer, cache in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, mask, cache=cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -205,6 +228,47 @@ class Model(nn.Module):
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@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
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ChunkedKVCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -145,7 +146,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoE(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -95,7 +96,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(intermediate_size, 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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
+174
-62
@@ -4,9 +4,13 @@ from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .mla import MultiLinear
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -38,6 +42,7 @@ class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool
|
||||
norm_topk_prob: bool = False
|
||||
router_bias: bool = False
|
||||
rope_scaling: Optional[Dict] = None
|
||||
|
||||
|
||||
class LongcatFlashMLA(nn.Module):
|
||||
@@ -76,10 +81,11 @@ class LongcatFlashMLA(nn.Module):
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
self.kv_lora_rank,
|
||||
self.num_attention_heads * (self.qk_nope_head_dim + args.v_head_dim),
|
||||
bias=False,
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_attention_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_attention_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
@@ -93,8 +99,20 @@ class LongcatFlashMLA(nn.Module):
|
||||
if args.mla_scale_kv_lora:
|
||||
self.mla_scale_kv_lora = (args.hidden_size / self.kv_lora_rank) ** 0.5
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
dims=self.qk_rope_head_dim, base=args.rope_theta, traditional=True
|
||||
if args.rope_scaling is not None:
|
||||
mscale_all_dim = args.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = args.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.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=True,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
@@ -106,56 +124,59 @@ class LongcatFlashMLA(nn.Module):
|
||||
B, L, _ = x.shape
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q_states = self.q_proj(x)
|
||||
q = self.q_proj(x)
|
||||
else:
|
||||
q_states = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
|
||||
q_states = q_states.reshape(B, L, -1, self.qk_head_dim).transpose(0, 2, 1, 3)
|
||||
|
||||
if self.mla_scale_q_lora is not None:
|
||||
q_states = q_states * self.mla_scale_q_lora
|
||||
|
||||
q_pass, q_rot = mx.split(q_states, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
k_pass, k_rot = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pass = self.kv_a_layernorm(k_pass)
|
||||
|
||||
if self.mla_scale_kv_lora is not None:
|
||||
k_pass = k_pass * self.mla_scale_kv_lora
|
||||
|
||||
key_shape = (B, L, -1, self.qk_nope_head_dim + self.v_head_dim)
|
||||
k_pass = self.kv_b_proj(k_pass).reshape(*key_shape).transpose(0, 2, 1, 3)
|
||||
k_pass, value_states = mx.split(k_pass, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
k_rot = k_rot.reshape(B, 1, L, self.qk_rope_head_dim)
|
||||
|
||||
if cache is not None:
|
||||
q_rot = self.rope(q_rot, cache.offset)
|
||||
k_rot = self.rope(k_rot, cache.offset)
|
||||
else:
|
||||
q_rot = self.rope(q_rot)
|
||||
k_rot = self.rope(k_rot)
|
||||
|
||||
k_rot = mx.broadcast_to(k_rot, (*k_pass.shape[:-1], k_rot.shape[-1]))
|
||||
|
||||
query_states = mx.concatenate([q_pass, q_rot], axis=-1)
|
||||
key_states = mx.concatenate([k_pass, k_rot], axis=-1)
|
||||
|
||||
if cache is not None:
|
||||
key_states, value_states = cache.update_and_fetch(key_states, value_states)
|
||||
|
||||
attn_output = scaled_dot_product_attention(
|
||||
query_states,
|
||||
key_states,
|
||||
value_states,
|
||||
cache=cache,
|
||||
scale=self.scale,
|
||||
mask=mask,
|
||||
q = q.reshape(B, L, self.num_attention_heads, self.qk_head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(attn_output)
|
||||
if self.mla_scale_q_lora is not None:
|
||||
q = q * self.mla_scale_q_lora
|
||||
|
||||
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_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
if self.mla_scale_kv_lora is not None:
|
||||
kv_latent = kv_latent * self.mla_scale_kv_lora
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class LongcatFlashMLP(nn.Module):
|
||||
@@ -168,7 +189,7 @@ class LongcatFlashMLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class LongcatFlashTopkRouter(nn.Module):
|
||||
@@ -223,8 +244,11 @@ class LongcatFlashMoE(nn.Module):
|
||||
)
|
||||
|
||||
self.router = LongcatFlashTopkRouter(args)
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, hidden_states):
|
||||
if self.sharding_group is not None:
|
||||
hidden_states = sum_gradients(self.sharding_group)(hidden_states)
|
||||
|
||||
topk_indices, topk_weights = self.router(hidden_states)
|
||||
|
||||
@@ -235,15 +259,21 @@ class LongcatFlashMoE(nn.Module):
|
||||
|
||||
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
|
||||
|
||||
weighted_outputs = regular_outputs * topk_weights[..., None]
|
||||
|
||||
# Add identity expert contribution if needed
|
||||
assert self.zero_expert_type == "identity"
|
||||
identity_weights = mx.where(mask, topk_weights, 0.0)
|
||||
identity_outputs = hidden_states[..., None, :] * identity_weights[..., None]
|
||||
weighted_outputs = weighted_outputs + identity_outputs
|
||||
|
||||
weighted_outputs = regular_outputs * regular_weights[..., None]
|
||||
final_output = mx.sum(weighted_outputs, axis=-2)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
final_output = mx.distributed.all_sum(
|
||||
final_output, group=self.sharding_group
|
||||
)
|
||||
|
||||
# Add identity expert contribution after all_sum to avoid summing it N times
|
||||
assert self.zero_expert_type == "identity"
|
||||
identity_weights_sum = mx.sum(
|
||||
mx.where(mask, topk_weights, 0.0), axis=-1, keepdims=True
|
||||
)
|
||||
final_output = final_output + hidden_states * identity_weights_sum
|
||||
|
||||
return final_output
|
||||
|
||||
|
||||
@@ -314,7 +344,7 @@ class LongcatFlashModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [(None, None)] * self.num_layers
|
||||
|
||||
mask = create_attention_mask(h, cache[0][0])
|
||||
mask = create_attention_mask(h, cache[0][0], return_array=True)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
@@ -370,6 +400,47 @@ class Model(nn.Module):
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
|
||||
for l in range(self.args.num_layers):
|
||||
for i in range(2):
|
||||
prefix = f"model.layers.{l}.self_attn.{i}"
|
||||
kv_b_key = f"{prefix}.kv_b_proj.weight"
|
||||
if kv_b_key in weights:
|
||||
num_heads = self.args.num_attention_heads
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(kv_b_key)
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
|
||||
if quantized:
|
||||
wk, wk_s, wk_b = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_s, wv_b = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_s
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_b
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_s
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_b
|
||||
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if k.startswith("model.mtp"):
|
||||
@@ -379,3 +450,44 @@ class Model(nn.Module):
|
||||
|
||||
def make_cache(self):
|
||||
return [CacheList(KVCache(), KVCache()) for _ in self.model.layers]
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
|
||||
for layer in self.model.layers:
|
||||
for attn in layer.self_attn:
|
||||
if attn.q_lora_rank is None:
|
||||
attn.q_proj = shard_linear(
|
||||
attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
attn.q_b_proj = shard_linear(
|
||||
attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
attn.o_proj = shard_linear(attn.o_proj, "sharded-to-all", group=group)
|
||||
attn.num_attention_heads //= N
|
||||
num_heads = attn.num_attention_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
attn.embed_q.apply(shard_heads)
|
||||
attn.unembed_out.apply(shard_heads)
|
||||
|
||||
for mlp in layer.mlps:
|
||||
mlp.gate_proj = shard_linear(
|
||||
mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
mlp.up_proj = shard_linear(mlp.up_proj, "all-to-sharded", group=group)
|
||||
mlp.down_proj = shard_linear(
|
||||
mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group)
|
||||
shard_inplace(layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group)
|
||||
shard_inplace(layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# Copyright © 2026 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
|
||||
from .cache import ArraysCache, CacheList, KVCache
|
||||
from .longcat_flash import LongcatFlashDecoderLayer
|
||||
from .longcat_flash import Model as LongcatFlashLM
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
ffn_hidden_size: int
|
||||
moe_topk: int
|
||||
expert_ffn_hidden_size: int
|
||||
n_routed_experts: int
|
||||
zero_expert_num: int
|
||||
num_layers: int
|
||||
vocab_size: int
|
||||
max_position_embeddings: int
|
||||
num_attention_heads: int
|
||||
kv_lora_rank: int
|
||||
q_lora_rank: int
|
||||
qk_rope_head_dim: int
|
||||
qk_nope_head_dim: int
|
||||
v_head_dim: int
|
||||
routed_scaling_factor: float
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
mla_scale_q_lora: bool
|
||||
mla_scale_kv_lora: bool
|
||||
attention_bias: bool = False
|
||||
zero_expert_type: str = "identity"
|
||||
ngram_vocab_size_ratio: int = 78
|
||||
emb_neighbor_num: int = 4
|
||||
emb_split_num: int = 4
|
||||
norm_topk_prob: bool = False
|
||||
router_bias: bool = False
|
||||
rope_scaling: Optional[Dict] = None
|
||||
|
||||
|
||||
class NgramEmbedding(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = args.vocab_size
|
||||
self.hidden_size = args.hidden_size
|
||||
self.m = args.ngram_vocab_size_ratio * args.vocab_size
|
||||
self.k = args.emb_split_num
|
||||
self.n = args.emb_neighbor_num
|
||||
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
num_embedders = self.k * (self.n - 1)
|
||||
emb_dim = args.hidden_size // num_embedders
|
||||
|
||||
self.embedders = []
|
||||
self.post_projs = []
|
||||
for i in range(num_embedders):
|
||||
emb_vocab_size = int(self.m + i * 2 + 1)
|
||||
self.embedders.append(nn.Embedding(emb_vocab_size, emb_dim))
|
||||
self.post_projs.append(nn.Linear(emb_dim, args.hidden_size, bias=False))
|
||||
self._compute_vocab_mods()
|
||||
|
||||
def _compute_vocab_mods(self):
|
||||
vocab_mods = {}
|
||||
for i in range(2, self.n + 1):
|
||||
for j in range(self.k):
|
||||
index = (i - 2) * self.k + j
|
||||
emb_vocab_dim = int(self.m + index * 2 + 1)
|
||||
mods = []
|
||||
power_mod = 1
|
||||
for _ in range(i - 1):
|
||||
power_mod = (power_mod * self.vocab_size) % emb_vocab_dim
|
||||
mods.append(power_mod)
|
||||
vocab_mods[(i, j)] = mods
|
||||
self._vocab_mods = vocab_mods
|
||||
|
||||
def _shift_right(self, x: mx.array, n: int) -> mx.array:
|
||||
if n <= 0:
|
||||
return x
|
||||
batch_size, seq_len = x.shape
|
||||
if seq_len <= n:
|
||||
return mx.zeros_like(x)
|
||||
return mx.concatenate(
|
||||
[mx.zeros((batch_size, n), dtype=x.dtype), x[..., :-n]], axis=-1
|
||||
)
|
||||
|
||||
def _get_ngram_ids(
|
||||
self,
|
||||
input_ids: mx.array,
|
||||
shifted_ids: Dict[int, mx.array],
|
||||
vocab_mods: List[int],
|
||||
ngram: int,
|
||||
) -> mx.array:
|
||||
ngram_ids = input_ids
|
||||
for k in range(2, ngram + 1):
|
||||
ngram_ids = ngram_ids + shifted_ids[k] * vocab_mods[k - 2]
|
||||
return ngram_ids
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
input_ids: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
seq_len = input_ids.shape[-1]
|
||||
|
||||
input_ids = input_ids.astype(mx.int64)
|
||||
if cache is not None:
|
||||
context = cache[0]
|
||||
if context is None:
|
||||
context = input_ids
|
||||
else:
|
||||
context = mx.concatenate([context, input_ids], axis=-1)
|
||||
cache[0] = context[..., max(0, context.shape[-1] - self.n + 1) :]
|
||||
else:
|
||||
context = input_ids
|
||||
|
||||
x = self.word_embeddings(input_ids)
|
||||
vocab_mods = self._vocab_mods
|
||||
|
||||
shifted_ids = {}
|
||||
for i in range(2, self.n + 1):
|
||||
shifted_ids[i] = self._shift_right(context, i - 1)
|
||||
|
||||
for i in range(2, self.n + 1):
|
||||
for j in range(self.k):
|
||||
index = (i - 2) * self.k + j
|
||||
emb_vocab_dim = int(self.m + index * 2 + 1)
|
||||
ngram_ids = self._get_ngram_ids(
|
||||
context, shifted_ids, vocab_mods[(i, j)], ngram=i
|
||||
)
|
||||
new_ids = (ngram_ids % emb_vocab_dim)[..., -seq_len:]
|
||||
x_ngram = self.embedders[index](new_ids)
|
||||
x_proj = self.post_projs[index](x_ngram)
|
||||
x = x + x_proj
|
||||
|
||||
return x / (1 + self.k * (self.n - 1))
|
||||
|
||||
|
||||
class LongcatFlashNgramModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_layers = args.num_layers
|
||||
self.ngram_embeddings = NgramEmbedding(args)
|
||||
self.layers = [LongcatFlashDecoderLayer(args) for _ in range(args.num_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
input_ids: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
if cache is None:
|
||||
cache = [None] + [(None, None)] * self.num_layers
|
||||
|
||||
h = self.ngram_embeddings(input_ids, cache=cache[0])
|
||||
|
||||
mask = create_attention_mask(h, cache[1][0], return_array=True)
|
||||
|
||||
for layer, c in zip(self.layers, cache[1:]):
|
||||
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 = LongcatFlashNgramModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
return LongcatFlashLM.quant_predicate.fget(self)
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
return LongcatFlashLM.cast_predicate.fget(self)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = LongcatFlashLM.sanitize(self, weights)
|
||||
if "model.embed_tokens.weight" in weights:
|
||||
weights["model.ngram_embeddings.word_embeddings.weight"] = weights.pop(
|
||||
"model.embed_tokens.weight"
|
||||
)
|
||||
return weights
|
||||
|
||||
def make_cache(self):
|
||||
return [ArraysCache(size=1)] + [
|
||||
CacheList(KVCache(), KVCache()) for _ in self.model.layers
|
||||
]
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
LongcatFlashLM.shard(self, group)
|
||||
@@ -6,8 +6,9 @@ from dataclasses import dataclass
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs
|
||||
from .cache import MambaCache
|
||||
from .cache import ArraysCache
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -139,7 +140,7 @@ class MambaBlock(nn.Module):
|
||||
y_t, current_state = self.ssm_step(x[:, t], A, current_state)
|
||||
y.append(y_t)
|
||||
y = mx.stack(y, axis=1)
|
||||
z = self.out_proj(nn.silu(z) * y)
|
||||
z = self.out_proj(swiglu(z, y))
|
||||
return z, (new_conv_cache, current_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
@@ -152,7 +153,7 @@ class MambaBlock(nn.Module):
|
||||
x, conv_cache, state_cache
|
||||
)
|
||||
|
||||
if isinstance(cache, MambaCache):
|
||||
if isinstance(cache, ArraysCache):
|
||||
cache[0] = new_conv_cache
|
||||
cache[1] = new_state_cache
|
||||
|
||||
@@ -207,7 +208,7 @@ class Model(nn.Module):
|
||||
return logits
|
||||
|
||||
def make_cache(self):
|
||||
return [MambaCache() for _ in range(len(self.layers))]
|
||||
return [ArraysCache(size=2) for _ in range(len(self.layers))]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
|
||||
+39
-20
@@ -7,8 +7,9 @@ from typing import Optional, Tuple, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_ssm_mask
|
||||
from .cache import MambaCache
|
||||
from .cache import ArraysCache
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@@ -48,7 +49,7 @@ class MambaRMSNormGated(nn.Module):
|
||||
|
||||
def __call__(self, hidden_states: mx.array, gate: mx.array = None) -> mx.array:
|
||||
if gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
@@ -93,9 +94,15 @@ class Mamba2Block(nn.Module):
|
||||
self.intermediate_size, self.hidden_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
@@ -105,7 +112,14 @@ class Mamba2Block(nn.Module):
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :, :]
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
t = padded_input.shape[1]
|
||||
ends = mx.clip(cache.lengths, 0, t - n_keep)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(padded_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = padded_input[:, -n_keep:, :]
|
||||
else:
|
||||
padded_input = mx.pad(
|
||||
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
|
||||
@@ -120,8 +134,8 @@ class Mamba2Block(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
hidden_states = hidden_states.reshape(
|
||||
@@ -129,6 +143,11 @@ class Mamba2Block(nn.Module):
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
@@ -140,14 +159,17 @@ class Mamba2Block(nn.Module):
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
lengths,
|
||||
)
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
if cache:
|
||||
cache[1] = state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[MambaCache] = None,
|
||||
cache: Optional[ArraysCache] = None,
|
||||
) -> mx.array:
|
||||
projected = self.in_proj(hidden_states)
|
||||
gate, conv_input, dt = mx.split(
|
||||
@@ -155,9 +177,7 @@ class Mamba2Block(nn.Module):
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
@@ -166,10 +186,9 @@ class Mamba2Block(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states, B, C, dt, state, mask=mask)
|
||||
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
@@ -181,7 +200,7 @@ class ResidualBlock(nn.Module):
|
||||
self.norm = nn.RMSNorm(args.hidden_size)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, mask: Optional[mx.array], cache: Optional[MambaCache] = None
|
||||
self, x: mx.array, mask: Optional[mx.array], cache: Optional[ArraysCache] = None
|
||||
) -> mx.array:
|
||||
output = self.mixer(self.norm(x), mask, cache)
|
||||
return output + x
|
||||
@@ -196,7 +215,7 @@ class Mamba2(nn.Module):
|
||||
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.layer_norm_epsilon)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, cache: Optional[list[MambaCache]] = None
|
||||
self, x: mx.array, cache: Optional[list[ArraysCache]] = None
|
||||
) -> mx.array:
|
||||
hidden = self.embeddings(x)
|
||||
|
||||
@@ -221,7 +240,7 @@ class Model(nn.Module):
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self, inputs: mx.array, cache: Optional[list[MambaCache]] = None
|
||||
self, inputs: mx.array, cache: Optional[list[ArraysCache]] = None
|
||||
) -> mx.array:
|
||||
hidden = self.backbone(inputs, cache)
|
||||
|
||||
@@ -231,8 +250,8 @@ class Model(nn.Module):
|
||||
logits = self.lm_head(hidden)
|
||||
return logits
|
||||
|
||||
def make_cache(self, batch_size: int = 1) -> list[MambaCache]:
|
||||
return [MambaCache() for _ in range(self.args.num_hidden_layers)]
|
||||
def make_cache(self, batch_size: int = 1) -> list[ArraysCache]:
|
||||
return [ArraysCache(size=2) for _ in range(self.args.num_hidden_layers)]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -90,7 +91,7 @@ class MLP(nn.Module):
|
||||
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))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
num_experts_per_tok: int
|
||||
hybrid_layer_pattern: List[int]
|
||||
moe_layer_freq: List[int]
|
||||
add_swa_attention_sink_bias: bool
|
||||
add_full_attention_sink_bias: bool
|
||||
sliding_window_size: int
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
n_shared_experts: Optional[int]
|
||||
n_routed_experts: Optional[int]
|
||||
routed_scaling_factor: Optional[float]
|
||||
topk_method: str
|
||||
scoring_func: str
|
||||
norm_topk_prob: bool
|
||||
n_group: int
|
||||
topk_group: int
|
||||
max_position_embeddings: int
|
||||
layernorm_epsilon: float
|
||||
rope_theta: float
|
||||
swa_rope_theta: float
|
||||
swa_num_attention_heads: int
|
||||
swa_num_key_value_heads: int
|
||||
head_dim: int
|
||||
v_head_dim: int
|
||||
swa_head_dim: int
|
||||
swa_v_head_dim: int
|
||||
partial_rotary_factor: int
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, is_sliding_window: bool):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.is_sliding_window = is_sliding_window
|
||||
if self.is_sliding_window:
|
||||
self.n_heads = n_heads = args.swa_num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.swa_num_key_value_heads
|
||||
self.has_sinks = args.add_swa_attention_sink_bias
|
||||
head_dim = args.swa_head_dim
|
||||
v_head_dim = args.swa_v_head_dim
|
||||
rope_theta = args.swa_rope_theta
|
||||
else:
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
self.has_sinks = args.add_full_attention_sink_bias
|
||||
head_dim = args.head_dim
|
||||
v_head_dim = args.v_head_dim
|
||||
rope_theta = args.rope_theta
|
||||
|
||||
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 * v_head_dim, bias=False)
|
||||
self.o_proj = nn.Linear(n_heads * v_head_dim, dim, bias=False)
|
||||
if self.has_sinks:
|
||||
self.attention_sink_bias = mx.ones((self.n_heads,))
|
||||
else:
|
||||
self.attention_sink_bias = None
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
int(args.partial_rotary_factor * head_dim),
|
||||
traditional=False,
|
||||
base=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)
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
sinks=self.attention_sink_bias,
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(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(swiglu(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 + 1e-20)
|
||||
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
|
||||
if config.routed_scaling_factor is not None
|
||||
else 1.0
|
||||
)
|
||||
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 MoE(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 = MLP(
|
||||
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 DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, is_moe, is_sliding_window):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(config, is_sliding_window)
|
||||
self.mlp = MoE(config) if is_moe else MLP(config)
|
||||
self.is_sliding_window = is_sliding_window
|
||||
self.input_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.layernorm_epsilon
|
||||
)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.layernorm_epsilon
|
||||
)
|
||||
|
||||
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 LanguageModel(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 = [
|
||||
DecoderLayer(
|
||||
config,
|
||||
is_moe=config.moe_layer_freq[idx] == 1,
|
||||
is_sliding_window=config.hybrid_layer_pattern[idx] == 1,
|
||||
)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
|
||||
self.swa_idx = config.hybrid_layer_pattern.index(1)
|
||||
self.ga_idx = config.hybrid_layer_pattern.index(0)
|
||||
self.sliding_window_size = config.sliding_window_size
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
full_mask = create_attention_mask(x, cache[self.ga_idx])
|
||||
swa_mask = create_attention_mask(
|
||||
x, cache[self.swa_idx], window_size=self.sliding_window_size
|
||||
)
|
||||
|
||||
for l, c in zip(self.layers, cache):
|
||||
mask = swa_mask if l.is_sliding_window else full_mask
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
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 = LanguageModel(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 = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = bs * scale_inv.shape[0] - m
|
||||
pad_side = bs * scale_inv.shape[1] - n
|
||||
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 fp8
|
||||
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
|
||||
return {k: v for k, v in weights.items() if not k.startswith("model.mtp")}
|
||||
|
||||
@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
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for l in self.layers:
|
||||
if l.is_sliding_window:
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window_size))
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -38,7 +39,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import SuScaledRoPE
|
||||
|
||||
@@ -156,7 +157,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
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
|
||||
|
||||
|
||||
@lru_cache
|
||||
def sharded_rms_norm(group):
|
||||
@mx.compile
|
||||
def _cast_square_sum(x):
|
||||
return x.astype(mx.float32).square().sum(-1, keepdims=True)
|
||||
|
||||
@mx.compile
|
||||
def _normalize(x, norm2, w, eps):
|
||||
norm2 = mx.distributed.all_sum(norm2, group=group)
|
||||
norm = mx.rsqrt(norm2 / (x.shape[-1] * group.size()) + eps)
|
||||
return (x.astype(mx.float32) * norm * w).astype(x.dtype)
|
||||
|
||||
# Split the compile so that x upcasting doesn't break the compile and we
|
||||
# have 2 kernels generated 1 for f(x) = square(upcast(x)) and another
|
||||
# g(x) = downcast(upcast(x) * norm * w)
|
||||
def _inner_sharded_rms_norm(x, w, eps):
|
||||
return _normalize(x, _cast_square_sum(x), w, eps)
|
||||
|
||||
return _inner_sharded_rms_norm
|
||||
|
||||
|
||||
class ShardedRMSNorm(nn.Module):
|
||||
def __init__(
|
||||
self, dims: int, eps: float = 1e-5, group: Optional[mx.distributed.Group] = None
|
||||
):
|
||||
super().__init__()
|
||||
group = group or mx.distributed.init()
|
||||
self.weight = mx.ones((dims // group.size(),))
|
||||
self.group = group
|
||||
self.eps = eps
|
||||
|
||||
def _extra_repr(self):
|
||||
return f"{self.weight.shape[0] * self.group.size()}, eps={self.eps}"
|
||||
|
||||
def __call__(self, x):
|
||||
return sharded_rms_norm(self.group)(x, self["weight"], self.eps)
|
||||
|
||||
@classmethod
|
||||
def from_rms_norm(
|
||||
cls, norm_module, *, group: Optional[mx.distributed.Group] = None
|
||||
):
|
||||
sn = cls(norm_module.weight.shape[0], norm_module.eps, group=group)
|
||||
sn.weight = mx.contiguous(
|
||||
mx.split(norm_module.weight, group.size(), axis=-1)[group.rank()]
|
||||
)
|
||||
|
||||
return sn
|
||||
|
||||
|
||||
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,))
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
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)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
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 = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
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
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
if layer.self_attn.use_qk_norm:
|
||||
layer.self_attn.q_norm = ShardedRMSNorm.from_rms_norm(
|
||||
layer.self_attn.q_norm, group=group
|
||||
)
|
||||
layer.self_attn.k_norm = ShardedRMSNorm.from_rms_norm(
|
||||
layer.self_attn.k_norm, group=group
|
||||
)
|
||||
|
||||
layer.self_attn.num_attention_heads //= N
|
||||
layer.self_attn.num_key_value_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
shard_inplace(
|
||||
layer.block_sparse_moe.switch_mlp.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.block_sparse_moe.switch_mlp.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.block_sparse_moe.switch_mlp.up_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
layer.block_sparse_moe.sharding_group = group
|
||||
|
||||
@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,333 @@
|
||||
# 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 mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .pipeline import PipelineMixin
|
||||
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(size, offset, beta: float, max_position_embeddings: int):
|
||||
if isinstance(offset, mx.array) and offset.ndim > 0:
|
||||
offset = offset[:, None]
|
||||
|
||||
scaling = 1 + beta * mx.log(
|
||||
1 + mx.floor((mx.arange(size) + offset) / max_position_embeddings)
|
||||
)
|
||||
if scaling.ndim == 2:
|
||||
return scaling[:, None, :, None]
|
||||
else:
|
||||
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(swiglu(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(PipelineMixin, 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 pipeline(self, group):
|
||||
super().pipeline(group)
|
||||
self.fa_idx = None
|
||||
self.swa_idx = None
|
||||
for e, l in enumerate(self.pipeline_layers):
|
||||
if self.swa_idx is None and l.use_sliding:
|
||||
self.swa_idx = e
|
||||
elif self.fa_idx is None and not l.use_sliding:
|
||||
self.fa_idx = e
|
||||
if self.fa_idx is not None and self.swa_idx is not None:
|
||||
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)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
offset = 0
|
||||
else:
|
||||
offset = cache[0].offset
|
||||
|
||||
swa_mask = fa_mask = None
|
||||
if self.fa_idx is not None:
|
||||
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(
|
||||
inputs.shape[1],
|
||||
offset,
|
||||
self.args.rope_parameters["llama_4_scaling_beta"],
|
||||
self.args.rope_parameters["original_max_position_embeddings"],
|
||||
).astype(h.dtype)
|
||||
|
||||
# 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):
|
||||
mask = swa_mask if l.use_sliding else fa_mask
|
||||
h = l(h, attn_scale, 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
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
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
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
@@ -25,6 +25,7 @@ class ModelArgs(BaseModelArgs):
|
||||
rope_theta: float = 1e6
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
@@ -162,8 +163,12 @@ class MixtralModel(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
@@ -179,20 +184,27 @@ class MixtralModel(nn.Module):
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = MixtralModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.args = 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)
|
||||
return self.lm_head(out)
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.args.tie_word_embeddings:
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
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):
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
import math
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
|
||||
class MultiLinear(nn.Module):
|
||||
def __init__(self, input_dims: int, output_dims: int, num_heads: int) -> None:
|
||||
super().__init__()
|
||||
scale = math.sqrt(1.0 / input_dims)
|
||||
self.weight = mx.random.uniform(
|
||||
low=-scale,
|
||||
high=scale,
|
||||
shape=(num_heads, output_dims, input_dims),
|
||||
)
|
||||
|
||||
def __call__(self, x, transpose=True):
|
||||
if transpose:
|
||||
return x @ self.weight.swapaxes(-1, -2)
|
||||
else:
|
||||
return x @ self.weight
|
||||
|
||||
def to_quantized(
|
||||
self,
|
||||
group_size: int,
|
||||
bits: int,
|
||||
mode: str = "affine",
|
||||
):
|
||||
num_heads, output_dims, input_dims = self.weight.shape
|
||||
ql = QuantizedMultiLinear(
|
||||
input_dims, output_dims, num_heads, group_size, bits, mode
|
||||
)
|
||||
ql.weight, ql.scales, *biases = mx.quantize(
|
||||
self.weight,
|
||||
group_size,
|
||||
bits,
|
||||
mode=mode,
|
||||
)
|
||||
ql.biases = biases[0] if biases else None
|
||||
return ql
|
||||
|
||||
|
||||
class QuantizedMultiLinear(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dims: int,
|
||||
output_dims: int,
|
||||
num_heads: int,
|
||||
group_size: int,
|
||||
bits: int,
|
||||
mode: str,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.group_size = group_size
|
||||
self.bits = bits
|
||||
self.mode = mode
|
||||
|
||||
# Initialize the quantized weight
|
||||
scale = math.sqrt(1 / input_dims)
|
||||
weight = mx.random.uniform(
|
||||
low=-scale,
|
||||
high=scale,
|
||||
shape=(num_heads, output_dims, input_dims),
|
||||
)
|
||||
self.weight, self.scales, *biases = mx.quantize(
|
||||
weight, group_size, bits, mode=mode
|
||||
)
|
||||
self.biases = biases[0] if biases else None
|
||||
|
||||
self.freeze()
|
||||
|
||||
def __call__(self, x, transpose=True):
|
||||
return mx.quantized_matmul(
|
||||
x,
|
||||
self["weight"],
|
||||
scales=self["scales"],
|
||||
biases=self.get("biases"),
|
||||
transpose=transpose,
|
||||
group_size=self.group_size,
|
||||
bits=self.bits,
|
||||
mode=self.mode,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user