Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d042c6124 | |||
| 0fbff353db | |||
| 0ad37e2bbf | |||
| 454bf9a22b | |||
| 133b5d3bd7 | |||
| abc52a0a48 | |||
| 6b42901468 | |||
| f353e0178b | |||
| f940cf3a95 | |||
| 34cbb8b51a | |||
| 4bc21cc17b | |||
| 9fd3e419ec | |||
| 743f4f7710 | |||
| 088e7ad7ca | |||
| 1d01257d2e | |||
| 2959af09fb | |||
| 8f1f88e5af | |||
| 606ff3ef06 | |||
| cd367819c7 | |||
| ba2cf3c0ee | |||
| 6c1a459314 | |||
| 3833c205c1 | |||
| 3356b0a017 | |||
| f6c94659d8 | |||
| d3bf847e6f | |||
| df6434185c | |||
| 974e17b43a | |||
| a82790a141 | |||
| 2aa31f95a7 | |||
| 4decc4d381 | |||
| 0d8272483b | |||
| 663b822de5 | |||
| f36977385f | |||
| 1e8fca4e0b | |||
| 61669b270f | |||
| f2b0262824 | |||
| 367d6d7686 | |||
| 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 | |||
| c991106dbf | |||
| a7f534c3f5 | |||
| 04d6d926f3 | |||
| 38dc092e1f | |||
| de47734510 | |||
| 9e89bb5c25 | |||
| b2564b5226 | |||
| 502554646d | |||
| 40dd25d7b7 | |||
| 55bb9471b8 | |||
| 9a11a81add | |||
| 4aaac2072d | |||
| 469461f463 | |||
| 714157be6d | |||
| 06a9fdc5ad | |||
| 420418ca4e | |||
| d6c45998f0 | |||
| 42828df17a | |||
| 80c217ff96 | |||
| cf8e59ef76 | |||
| 7b84eb6bd1 | |||
| 547b6b7e26 | |||
| 319c453828 | |||
| 64b3c51a58 | |||
| 1537efd29a | |||
| 4a085c7618 | |||
| 87961a743a | |||
| d79f3cd612 | |||
| 103877ea3e | |||
| 64574e19b8 | |||
| 1b08ef199b | |||
| 4ce11c6e50 | |||
| 0f268680c8 | |||
| 3ae6583393 | |||
| dccde59664 | |||
| 30c30a2a7e | |||
| 6dd9d48bdc | |||
| f5741cae33 | |||
| 3b9074f48a | |||
| e797abf49b | |||
| 60320dc234 | |||
| 1cd6045176 | |||
| d17184063a | |||
| 24fefe3d05 | |||
| da1309f5a7 | |||
| bdcac4b635 | |||
| 04a113fbdc | |||
| cd9884dab3 | |||
| 249b0a11d6 | |||
| 6fd60d3edb | |||
| 1b7b67bfcf | |||
| e7f241094c | |||
| a2acdd6ddd | |||
| c0d630a9b4 | |||
| ebc2eea042 | |||
| d9a3ece154 | |||
| b513585c2f | |||
| 5f71d8bd84 | |||
| 877cc38e6c | |||
| 5ff59f0389 | |||
| 6d74487ed6 | |||
| 90e33f5443 | |||
| f1a6359c92 | |||
| 6c876ca5d1 | |||
| cfa74add1e | |||
| e22bdaafe3 | |||
| d5bdab1a22 | |||
| 667a7116c3 | |||
| cbfba0a973 | |||
| fc800f1a0b | |||
| 06efe8db99 | |||
| 4d6d705140 | |||
| 7c987941f2 | |||
| e4470506ab | |||
| 72bc789b2b | |||
| e9b1649662 | |||
| b60cec88df | |||
| b26c608811 | |||
| 489e63376b | |||
| d23c79bf90 |
@@ -1,100 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
apple: ml-explore/pr-approval@0.1.0
|
||||
|
||||
jobs:
|
||||
linux_build_and_test:
|
||||
docker:
|
||||
- image: cimg/python:3.9
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run style checks
|
||||
command: |
|
||||
pip install pre-commit
|
||||
pre-commit run --all
|
||||
if ! git diff --quiet; then echo 'Style checks failed, please install pre-commit and run pre-commit run --all and push the change'; exit 1; fi
|
||||
|
||||
mlx_lm_build_and_test:
|
||||
macos:
|
||||
xcode: "15.2.0"
|
||||
resource_class: m2pro.medium
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
brew install python@3.9
|
||||
python3.9 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install sentencepiece
|
||||
pip install unittest-xml-reporting
|
||||
pip install -e ".[test]"
|
||||
- run:
|
||||
name: Run Python tests
|
||||
command: |
|
||||
source env/bin/activate
|
||||
python -m xmlrunner discover -v tests -o test-results/
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
build_release:
|
||||
macos:
|
||||
xcode: "15.2.0"
|
||||
resource_class: m2pro.medium
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
brew install python@3.9
|
||||
python3.9 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install build
|
||||
pip install twine
|
||||
- run:
|
||||
name: Build and upload
|
||||
command: |
|
||||
source env/bin/activate
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
- store_artifacts:
|
||||
path: dist/
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
when:
|
||||
matches:
|
||||
pattern: "^(?!pull/)[-\\w]+$"
|
||||
value: << pipeline.git.branch >>
|
||||
jobs:
|
||||
- mlx_lm_build_and_test
|
||||
- linux_build_and_test
|
||||
|
||||
build_pypi_release:
|
||||
jobs:
|
||||
- build_release:
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
prb:
|
||||
when:
|
||||
matches:
|
||||
pattern: "^pull/\\d+(/head)?$"
|
||||
value: << pipeline.git.branch >>
|
||||
jobs:
|
||||
- hold:
|
||||
type: approval
|
||||
- apple/authenticate:
|
||||
context: pr-approval
|
||||
- mlx_lm_build_and_test:
|
||||
requires: [ hold ]
|
||||
- linux_build_and_test:
|
||||
requires: [ hold ]
|
||||
@@ -0,0 +1,16 @@
|
||||
name: 'Setup macOS Environment'
|
||||
description: 'Install dependencies for macOS'
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: 'Python version to use'
|
||||
required: false
|
||||
default: '3.10'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: "latest"
|
||||
python-version: ${{ inputs.python-version }}
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/head/main' }}
|
||||
|
||||
jobs:
|
||||
check_lint:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
|
||||
mac_build_and_test:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: [self-hosted, macos]
|
||||
needs: check_lint
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-macos
|
||||
- name: Install test dependencies
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
pip install unittest-xml-reporting
|
||||
pip install -e ".[test]"
|
||||
- name: Run tests
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
python -m xmlrunner discover -v tests -o test-results/
|
||||
@@ -0,0 +1,41 @@
|
||||
name: PyPI Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build_release:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mlx-lm
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Build package
|
||||
shell: sh
|
||||
run: |
|
||||
pip install build
|
||||
python -m build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
overwrite: true
|
||||
name: mlx-lm
|
||||
path: dist/*
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://upload.pypi.org/legacy/
|
||||
+19
-2
@@ -8,5 +8,22 @@ with a short description of your contribution(s) below. For example:
|
||||
MLX LM was developed with contributions from the following individuals:
|
||||
|
||||
- Shunta Saito: Added support for PLaMo models.
|
||||
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's`Mamba v1`, Z.ai & THUKEG's `GLM4`, Rednote `dots.llm1`, Baisu's `Ernie4.5 MoE`, and Allenai's `OLMoE`; Added support for the following training algorithms: `full-fine-tuning`; 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`.
|
||||
- Gökdeniz Gülmez: Added support for the following architectures:
|
||||
OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1` and
|
||||
`Mamba v2`, Z.ai & THUKEG's `GLM`, `GLM4`, Rednote `dots.llm1`, Baidu's `Ernie4.5 MoE`,
|
||||
inclusionAI's `Bailing MoE e.g. Ling-family`, `Bailing MoE Linear e.g. Ling-Linear-family`,
|
||||
Klear team - Kuaishou Technology's `Klear`, AI21 Lab's `Jamba` IBM's `Granite MoE`,
|
||||
Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's `Apertus`, Nikity's `Lille130m`,
|
||||
Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE` and `Olmo 3`;
|
||||
Helped add support for the following model architectures:
|
||||
Alibaba Qwen's `Qwen3 & Qwen3MoE)`; Added support for the following training algorithms:
|
||||
`Full Weight Fine-Tuning`, and the `Muon` optimizer;
|
||||
Added support for the following other features:
|
||||
`Multiple Optimizers to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
|
||||
- Prince Canuma: Helped add support for the following model architectures:
|
||||
HuggingFace's `Starcoder2`, Cohere's `Cohere (1 and 2)`, Alibaba Qwen's `Qwen (2, 3 and MoE)`,
|
||||
Microsoft's `Phi (3 and 3.5 MoE)`, `BitNet1.58`, Meta's `Llama (3 and 4)`, 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`.
|
||||
@@ -52,6 +52,12 @@ options for a command, e.g.:
|
||||
mlx_lm.generate -h
|
||||
```
|
||||
|
||||
The default model for generation and chat is
|
||||
`mlx-community/Llama-3.2-3B-Instruct-4bit`. You can specify any MLX-compatible
|
||||
model with the `--model` flag. Thousands are available in the
|
||||
[MLX Community](https://huggingface.co/mlx-community) Hugging Face
|
||||
organization.
|
||||
|
||||
### Python API
|
||||
|
||||
You can use `mlx-lm` as a module:
|
||||
@@ -79,7 +85,9 @@ To see a description of all the arguments you can do:
|
||||
|
||||
Check out the [generation
|
||||
example](https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/examples/generate_response.py)
|
||||
to see how to use the API in more detail.
|
||||
to see how to use the API in more detail. Check out the [batch generation
|
||||
example](https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/examples/batch_generate_response.py)
|
||||
to see how to efficiently generate continuations for a batch of prompts.
|
||||
|
||||
The `mlx-lm` package also comes with functionality to quantize and optionally
|
||||
upload models to the Hugging Face Hub.
|
||||
@@ -228,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,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
-3
@@ -84,8 +84,14 @@ You can resume fine-tuning with an existing adapter with
|
||||
|
||||
#### Logging
|
||||
|
||||
You can log training metrics to Weights & Biases by passing a project name with
|
||||
the `--wandb` flag. Make sure to install wandb with `pip install wandb`.
|
||||
You can log training metrics to Weights & Biases using `--report-to wandb`, or
|
||||
to SwanLab using `--report-to swanlab`. Make sure to install the required
|
||||
packages beforehand: `pip install wandb` or `pip install swanlab`. You can
|
||||
enable both tracking tools simultaneously by separating them with a comma, for
|
||||
example: `--report-to wandb,swanlab`.
|
||||
|
||||
To specify a project name for the logging tracker, use `--project-name <YOUR
|
||||
PROJECT NAME>`.
|
||||
|
||||
#### Prompt Masking
|
||||
|
||||
@@ -365,7 +371,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
|
||||
|
||||
+10
-1
@@ -7,5 +7,14 @@ from ._version import __version__
|
||||
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
|
||||
|
||||
from .convert import convert
|
||||
from .generate import generate, stream_generate
|
||||
from .generate import batch_generate, generate, stream_generate
|
||||
from .utils import load
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"convert",
|
||||
"batch_generate",
|
||||
"generate",
|
||||
"stream_generate",
|
||||
"load",
|
||||
]
|
||||
|
||||
+10
-3
@@ -9,6 +9,7 @@ if __name__ == "__main__":
|
||||
"quant.dwq",
|
||||
"quant.dynamic_quant",
|
||||
"quant.gptq",
|
||||
"benchmark",
|
||||
"cache_prompt",
|
||||
"chat",
|
||||
"convert",
|
||||
@@ -16,6 +17,7 @@ if __name__ == "__main__":
|
||||
"fuse",
|
||||
"generate",
|
||||
"lora",
|
||||
"perplexity",
|
||||
"server",
|
||||
"manage",
|
||||
"upload",
|
||||
@@ -23,7 +25,12 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
subcommand = sys.argv.pop(1)
|
||||
if subcommand not in subcommands:
|
||||
if subcommand in subcommands:
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
elif subcommand == "--version":
|
||||
from mlx_lm import __version__
|
||||
|
||||
print(__version__)
|
||||
else:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.26.1"
|
||||
__version__ = "0.29.0"
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import argparse
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import batch_generate, load, stream_generate
|
||||
from mlx_lm.generate import DEFAULT_MODEL
|
||||
from mlx_lm.utils import pipeline_load
|
||||
|
||||
|
||||
def setup_arg_parser():
|
||||
"""Set up and return the argument parser."""
|
||||
parser = argparse.ArgumentParser(description="LLM benchmarking script")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
help=(
|
||||
"The path to the local model directory or Hugging Face repo. "
|
||||
f"If no model is specified, then {DEFAULT_MODEL} is used."
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt-tokens",
|
||||
"-p",
|
||||
default=512,
|
||||
help="Length of prompt",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generation-tokens",
|
||||
"-g",
|
||||
default=1024,
|
||||
help="Length of completion",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
"-b",
|
||||
default=1,
|
||||
help="Batch size",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-trials",
|
||||
"-n",
|
||||
default=5,
|
||||
help="Number of timing trials",
|
||||
type=int,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
mx.random.seed(0)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
if group.size() > 1:
|
||||
model, tokenizer, config = pipeline_load(args.model, return_config=True)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
tokenizer._eos_token_ids = {}
|
||||
|
||||
prompt_tokens = args.prompt_tokens
|
||||
generation_tokens = args.generation_tokens
|
||||
batch_size = args.batch_size
|
||||
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
|
||||
):
|
||||
pass
|
||||
return response
|
||||
|
||||
def batch_bench():
|
||||
return batch_generate(
|
||||
model, tokenizer, prompts, max_tokens=generation_tokens
|
||||
).stats
|
||||
|
||||
if batch_size == 1:
|
||||
_bench = single_bench
|
||||
else:
|
||||
_bench = batch_bench
|
||||
|
||||
rprint("Running warmup..")
|
||||
_bench()
|
||||
|
||||
report_keys = ["prompt_tps", "generation_tps", "peak_memory"]
|
||||
rprint(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
|
||||
responses = []
|
||||
for i in range(args.num_trials):
|
||||
response = _bench()
|
||||
responses.append(response)
|
||||
results = [(k, getattr(response, k)) for k in report_keys]
|
||||
results = [f"{k}={v:.3f}" for k, v in results]
|
||||
rprint(f"Trial {i+1}: " + ", ".join(results))
|
||||
|
||||
def avg(k):
|
||||
vals = (getattr(response, k) for response in responses)
|
||||
return sum(vals) / args.num_trials
|
||||
|
||||
results = [(k, avg(k)) for k in report_keys]
|
||||
results = [f"{k}={v:.3f}" for k, v in results]
|
||||
rprint(f"Averages: " + ", ".join(results))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+17
-2
@@ -27,6 +27,11 @@ def setup_arg_parser():
|
||||
help="The path to the local model directory or Hugging Face repo.",
|
||||
default=DEFAULT_MODEL,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
action="store_true",
|
||||
help="Enable trusting remote code for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--adapter-path",
|
||||
type=str,
|
||||
@@ -69,6 +74,11 @@ def setup_arg_parser():
|
||||
default=DEFAULT_MAX_TOKENS,
|
||||
help="Maximum number of tokens to generate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--system-prompt",
|
||||
default=None,
|
||||
help="System prompt to be used for the chat template",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -82,7 +92,9 @@ def main():
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={"trust_remote_code": True},
|
||||
tokenizer_config={
|
||||
"trust_remote_code": True if args.trust_remote_code else None
|
||||
},
|
||||
)
|
||||
|
||||
def print_help():
|
||||
@@ -104,7 +116,10 @@ def main():
|
||||
if query == "h":
|
||||
print_help()
|
||||
continue
|
||||
messages = [{"role": "user", "content": query}]
|
||||
messages = []
|
||||
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)
|
||||
for response in stream_generate(
|
||||
model,
|
||||
|
||||
+36
-14
@@ -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,
|
||||
@@ -19,10 +18,9 @@ from .utils import (
|
||||
|
||||
|
||||
def mixed_quant_predicate_builder(
|
||||
recipe: str, model: nn.Module
|
||||
recipe: str, model: nn.Module, group_size: int = 64
|
||||
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
|
||||
high_bits = 6
|
||||
group_size = 64
|
||||
|
||||
if recipe == "mixed_2_6":
|
||||
low_bits = 2
|
||||
@@ -34,7 +32,7 @@ def mixed_quant_predicate_builder(
|
||||
elif recipe == "mixed_4_6":
|
||||
low_bits = 4
|
||||
else:
|
||||
raise ValueError("Invalid quant recipe {recipe}")
|
||||
raise ValueError(f"Invalid quant recipe {recipe}")
|
||||
|
||||
down_keys = [k for k, _ in model.named_modules() if "down_proj" in k]
|
||||
if len(down_keys) == 0:
|
||||
@@ -49,7 +47,6 @@ def mixed_quant_predicate_builder(
|
||||
def mixed_quant_predicate(
|
||||
path: str,
|
||||
module: nn.Module,
|
||||
config: dict,
|
||||
) -> Union[bool, dict]:
|
||||
"""Implements mixed quantization predicates with similar choices to, for example, llama.cpp's Q4_K_M.
|
||||
Ref: https://github.com/ggerganov/llama.cpp/blob/917786f43d0f29b7c77a0c56767c0fa4df68b1c5/src/llama.cpp#L5265
|
||||
@@ -65,7 +62,9 @@ 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:
|
||||
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}
|
||||
if "down_proj" in path and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
@@ -88,6 +87,7 @@ def convert(
|
||||
quantize: bool = False,
|
||||
q_group_size: int = 64,
|
||||
q_bits: int = 4,
|
||||
q_mode: str = "affine",
|
||||
dtype: Optional[str] = None,
|
||||
upload_repo: str = None,
|
||||
revision: Optional[str] = None,
|
||||
@@ -108,13 +108,18 @@ 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):
|
||||
quant_predicate = mixed_quant_predicate_builder(quant_predicate, model)
|
||||
quant_predicate = mixed_quant_predicate_builder(
|
||||
quant_predicate, model, q_group_size
|
||||
)
|
||||
|
||||
if dtype is None:
|
||||
dtype = config.get("torch_dtype", None)
|
||||
@@ -137,7 +142,12 @@ def convert(
|
||||
if quantize:
|
||||
print("[INFO] Quantizing")
|
||||
model, config = quantize_model(
|
||||
model, config, q_group_size, q_bits, quant_predicate=quant_predicate
|
||||
model,
|
||||
config,
|
||||
q_group_size,
|
||||
q_bits,
|
||||
mode=q_mode,
|
||||
quant_predicate=quant_predicate,
|
||||
)
|
||||
|
||||
if dequantize:
|
||||
@@ -148,11 +158,10 @@ def convert(
|
||||
|
||||
save(
|
||||
mlx_path,
|
||||
model_path,
|
||||
hf_path,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_path,
|
||||
)
|
||||
|
||||
if upload_repo is not None:
|
||||
@@ -183,6 +192,13 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-mode",
|
||||
help="The quantization mode.",
|
||||
type=str,
|
||||
default="affine",
|
||||
choices=["affine", "mxfp4"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quant-predicate",
|
||||
help=f"Mixed-bit quantization recipe.",
|
||||
@@ -210,6 +226,12 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
help="Trust remote code when loading tokenizer.",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
+148
-55
@@ -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
|
||||
@@ -23,10 +23,12 @@ from lm_eval.api.registry import register_model
|
||||
from lm_eval.models import huggingface
|
||||
from tqdm import tqdm
|
||||
|
||||
from .generate import stream_generate
|
||||
from .models.base import create_causal_mask
|
||||
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):
|
||||
@@ -37,6 +39,13 @@ def _rstrip_until(s, untils):
|
||||
return s[: min(f)]
|
||||
|
||||
|
||||
def _lstrip(s, pattern):
|
||||
"""Truncate the prefix of the string after the first occurrence of pattern."""
|
||||
if (idx := s.find(pattern)) != -1:
|
||||
return s[idx + len(pattern) :]
|
||||
return s
|
||||
|
||||
|
||||
def _pad_inputs(inputs):
|
||||
lengths = np.array([len(x) for x in inputs])
|
||||
maxlen = lengths.max()
|
||||
@@ -71,14 +80,20 @@ class MLXLM(LM):
|
||||
path_or_hf_repo: str,
|
||||
max_tokens: Optional[int] = None,
|
||||
use_chat_template: Optional[bool] = None,
|
||||
trust_remote_code: bool = False,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._model, self.tokenizer = load(path_or_hf_repo)
|
||||
self._max_tokens = max_tokens or self.tokenizer.model_max_length
|
||||
tokenizer_config = {"trust_remote_code": True if trust_remote_code else None}
|
||||
self._model, self.tokenizer = load(
|
||||
path_or_hf_repo, tokenizer_config=tokenizer_config
|
||||
)
|
||||
self._max_tokens = max_tokens
|
||||
self._batch_size = 8
|
||||
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]
|
||||
@@ -95,30 +110,28 @@ class MLXLM(LM):
|
||||
inputs, targets = inputs[..., :-1], inputs[..., 1:]
|
||||
|
||||
cache = cache or make_prompt_cache(self._model)
|
||||
lengths += cache[0].offset
|
||||
|
||||
offset = 0
|
||||
scores, is_greedy = [], []
|
||||
for i in range(0, inputs.shape[1], step_size):
|
||||
inp = inputs[:, i : i + step_size]
|
||||
T = inp.shape[1]
|
||||
|
||||
offset = cache[0].offset
|
||||
mask = create_causal_mask(T, offset, lengths=lengths)
|
||||
|
||||
logits = self._model(inp, cache=cache, mask=mask)
|
||||
logits = self._model(inp, cache=cache)
|
||||
log_probs = nn.log_softmax(logits.astype(mx.float32))
|
||||
|
||||
score = mx.take_along_axis(
|
||||
log_probs, targets[:, i : i + step_size, mx.newaxis], axis=-1
|
||||
)[..., 0]
|
||||
|
||||
ig = targets[:, i : i + step_size] == mx.argmax(logits, axis=-1)
|
||||
ig = mx.where(mx.arange(T) + offset < lengths[:, None], ig, False)
|
||||
ig = mx.where(mx.arange(offset, T + offset) < lengths[:, None], ig, False)
|
||||
|
||||
mx.eval(score, ig)
|
||||
mx.clear_cache()
|
||||
|
||||
is_greedy.append(ig)
|
||||
scores.append(score)
|
||||
offset += T
|
||||
|
||||
scores = mx.concatenate(scores, axis=1)
|
||||
is_greedy = mx.concatenate(is_greedy, axis=1)
|
||||
@@ -166,7 +179,7 @@ class MLXLM(LM):
|
||||
indices = []
|
||||
for v in group_reqs.values():
|
||||
idx, resp = zip(*v)
|
||||
indices.extend(idx)
|
||||
indices.append(idx)
|
||||
responses.append(resp)
|
||||
|
||||
# split data accross ranks
|
||||
@@ -181,7 +194,8 @@ class MLXLM(LM):
|
||||
max_completed_l = max(len(s) for s in full_sequences)
|
||||
|
||||
# compute truncation length
|
||||
truncation = max(0, max_completed_l - self._max_tokens - 1)
|
||||
max_tokens = self._max_tokens or DEFAULT_MAX_TOKENS
|
||||
truncation = max(0, max_completed_l - max_tokens - 1)
|
||||
orig_prefix_l = len(prefix)
|
||||
prefix_l = max(len(prefix) - truncation, 0)
|
||||
prefix = prefix[len(prefix) - prefix_l :]
|
||||
@@ -212,31 +226,36 @@ class MLXLM(LM):
|
||||
scores[-1] += mx.sum(score).item()
|
||||
is_greedy[-1] &= mx.all(ig).item()
|
||||
|
||||
scores = mx.array(scores)
|
||||
is_greedy = mx.array(is_greedy)
|
||||
|
||||
if long_completions > 0:
|
||||
logging.info(
|
||||
f"Prefix eliminated for {long_completions} requests with "
|
||||
+ "completion longer than context."
|
||||
)
|
||||
|
||||
# All gather the results across nodes
|
||||
num_results = len(requests)
|
||||
per_group = mx.distributed.all_max(len(scores), stream=mx.cpu).item()
|
||||
scores = scores + [0] * (per_group - len(scores))
|
||||
is_greedy = is_greedy + [False] * (per_group - len(is_greedy))
|
||||
scores = mx.array(scores)
|
||||
is_greedy = mx.array(is_greedy)
|
||||
scores = mx.distributed.all_gather(scores, stream=mx.cpu)
|
||||
is_greedy = mx.distributed.all_gather(is_greedy, stream=mx.cpu)
|
||||
mx.eval(scores, is_greedy)
|
||||
|
||||
# all gather the results across groups
|
||||
if group.size() > 1:
|
||||
per_group = int(np.ceil(num_results / group.size()))
|
||||
scores = mx.pad(scores, ((0, per_group - len(scores)),))
|
||||
is_greedy = mx.pad(is_greedy, ((0, per_group - len(is_greedy))))
|
||||
scores = mx.distributed.all_gather(scores[mx.newaxis], stream=mx.cpu)
|
||||
is_greedy = mx.distributed.all_gather(is_greedy[mx.newaxis], stream=mx.cpu)
|
||||
mx.eval(scores, is_greedy)
|
||||
scores = scores.T.reshape(-1)
|
||||
is_greedy = is_greedy.T.reshape(-1)
|
||||
|
||||
inv_sort = mx.argsort(mx.array(indices))
|
||||
# Arrange the indices to match the scores from each node and then
|
||||
# inverse sort the scores
|
||||
all_indices = []
|
||||
for rank in range(group.size()):
|
||||
rank_indices = [
|
||||
idx for question in indices[rank :: group.size()] for idx in question
|
||||
]
|
||||
rank_indices += [num_results] * (per_group - len(rank_indices))
|
||||
all_indices.extend(rank_indices)
|
||||
inv_sort = mx.argsort(mx.array(all_indices))
|
||||
scores = scores[:num_results][inv_sort]
|
||||
is_greedy = is_greedy[:num_results][inv_sort]
|
||||
|
||||
return list(zip(scores.tolist(), is_greedy.tolist()))
|
||||
|
||||
def loglikelihood_rolling(self, requests) -> list[float]:
|
||||
@@ -276,8 +295,8 @@ class MLXLM(LM):
|
||||
)
|
||||
inputs = self._tokenize([req.args[0] for req in requests])
|
||||
all_scores = []
|
||||
for i in tqdm(range(0, len(texts), self._batch_size)):
|
||||
batch = texts[i : i + self._batch_size]
|
||||
for i in tqdm(range(0, len(inputs), self._batch_size)):
|
||||
batch = inputs[i : i + self._batch_size]
|
||||
scores, lengths, _ = self._score_fn(batch)
|
||||
mask = mx.arange(scores.shape[-1]) < lengths[:, None]
|
||||
all_scores.extend((mask * scores).sum(axis=-1).tolist())
|
||||
@@ -298,32 +317,77 @@ 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])
|
||||
# contrary to the doc the second element of the tuple contains
|
||||
# The second element of the tuple contains:
|
||||
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
|
||||
completions = []
|
||||
|
||||
for context, opt in tqdm(zip(contexts, options), total=len(contexts)):
|
||||
until = opt["until"]
|
||||
context = self.tokenizer.encode(
|
||||
# Tokenize all contexts
|
||||
contexts = [
|
||||
self.tokenizer.encode(
|
||||
context, add_special_tokens=not self.use_chat_template
|
||||
)
|
||||
max_tokens = min(
|
||||
opt.get("max_gen_tokens", self._max_tokens),
|
||||
self.tokenizer.model_max_length - len(context),
|
||||
)
|
||||
text = ""
|
||||
for response in stream_generate(
|
||||
self._model, self.tokenizer, prompt=context, max_tokens=max_tokens
|
||||
):
|
||||
text += response.text
|
||||
if any(u in text for u in until):
|
||||
text = _rstrip_until(text, until)
|
||||
completions.append(text)
|
||||
break
|
||||
else:
|
||||
completions.append(text)
|
||||
for context in contexts
|
||||
]
|
||||
|
||||
# TODO consider multi-token, per-prompt stop conditions
|
||||
max_tokens = [
|
||||
self._max_tokens or opt.get("max_gen_tokens", DEFAULT_MAX_TOKENS)
|
||||
for opt in options
|
||||
]
|
||||
|
||||
completions = batch_generate(
|
||||
model=self._model,
|
||||
tokenizer=self.tokenizer,
|
||||
prompts=contexts,
|
||||
max_tokens=max_tokens,
|
||||
verbose=True,
|
||||
sampler=self._sampler,
|
||||
).texts
|
||||
|
||||
for e, (text, opt) in enumerate(zip(completions, options)):
|
||||
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
|
||||
|
||||
|
||||
@@ -341,7 +405,9 @@ def main():
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
help="Maximum nunber of tokens to generate. Defaults to the model's max context length.",
|
||||
help="Maximum number of tokens to generate. When set, this value takes"
|
||||
" precedence over task specific defaults.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
@@ -372,7 +438,20 @@ def main():
|
||||
apply_chat_template, e.g. '{"enable_thinking":false}'""",
|
||||
default="{}",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--confirm-run-unsafe-code",
|
||||
action="store_true",
|
||||
help="Confirm that you want to run tasks that execute untrusted code.",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
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)
|
||||
@@ -383,10 +462,23 @@ def main():
|
||||
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
# Initialize the communication if in distributed mode
|
||||
world = mx.distributed.init()
|
||||
mx.eval(mx.distributed.all_sum(1, stream=mx.cpu))
|
||||
if world.size() > 1 and world.rank() == 0:
|
||||
print(f"Evaluating with {world.size()} nodes")
|
||||
|
||||
sampler = make_sampler(
|
||||
temp=args.temp,
|
||||
top_p=args.top_p,
|
||||
top_k=args.top_k,
|
||||
)
|
||||
lm = MLXLM(
|
||||
args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
use_chat_template=args.apply_chat_template,
|
||||
trust_remote_code=args.trust_remote_code,
|
||||
sampler=sampler,
|
||||
)
|
||||
MLXLM.apply_chat_template = chat_template_fn(**args.chat_template_args)
|
||||
|
||||
@@ -401,6 +493,7 @@ def main():
|
||||
numpy_random_seed=args.seed,
|
||||
torch_random_seed=args.seed,
|
||||
fewshot_random_seed=args.seed,
|
||||
confirm_run_unsafe_code=args.confirm_run_unsafe_code,
|
||||
)
|
||||
|
||||
file_keys = ["eval", args.model.replace("/", "_"), version("lm_eval")]
|
||||
@@ -408,7 +501,7 @@ def main():
|
||||
file_keys += [f"{args.num_shots:02d}"]
|
||||
file_keys += args.tasks
|
||||
filename = "_".join(file_keys)
|
||||
if mx.distributed.init().rank() == 0:
|
||||
if world.rank() == 0:
|
||||
output_path = output_dir / filename
|
||||
output_path.write_text(json.dumps(results["results"], indent=4))
|
||||
print("Results:")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from mlx_lm import batch_generate, load
|
||||
|
||||
# Specify the checkpoint
|
||||
checkpoint = "mlx-community/Llama-3.2-3B-Instruct-4bit"
|
||||
|
||||
# Load the corresponding model and tokenizer
|
||||
model, tokenizer = load(path_or_hf_repo=checkpoint)
|
||||
|
||||
# A batch of prompts
|
||||
prompts = [
|
||||
"Write a story about Einstein.",
|
||||
"Why is the sky blue?",
|
||||
"What time is it?",
|
||||
"How tall is Mt Everest?",
|
||||
]
|
||||
|
||||
# Apply the chat template and encode to tokens
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts
|
||||
]
|
||||
|
||||
# Set `verbose=True` to see generation statistics
|
||||
result = batch_generate(
|
||||
model, tokenizer, prompts, verbose=False, return_prompt_caches=True
|
||||
)
|
||||
print(result.texts[-1])
|
||||
|
||||
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])
|
||||
@@ -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
|
||||
@@ -37,8 +37,9 @@ val_batches: 25
|
||||
# Adam learning rate.
|
||||
learning_rate: 1e-5
|
||||
|
||||
# Whether to report the logs to WandB
|
||||
# wand: "wandb-project"
|
||||
# Services to report logs to (comma-separated): wandb, swanlab, or both ('wandb,swanlab').
|
||||
# report_to: wandb,swanlab
|
||||
# project_name: "Your-awesome-mlx-project-name"
|
||||
|
||||
# Number of training steps between loss reporting.
|
||||
steps_per_report: 10
|
||||
@@ -46,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
|
||||
|
||||
@@ -89,4 +93,3 @@ lora_parameters:
|
||||
# valid_split: "train[-100:]"
|
||||
# prompt_feature: "text"
|
||||
# completion_feature: "summary"
|
||||
|
||||
|
||||
@@ -17,71 +17,11 @@ https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import resource
|
||||
from pathlib import Path
|
||||
|
||||
import mlx.core as mx
|
||||
from huggingface_hub import snapshot_download
|
||||
from mlx.utils import tree_flatten
|
||||
|
||||
from mlx_lm import load, stream_generate
|
||||
from mlx_lm.utils import load_model, load_tokenizer
|
||||
|
||||
# Needed for 8 bit model
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
|
||||
|
||||
|
||||
def download(repo: str, allow_patterns: list[str]) -> Path:
|
||||
return Path(
|
||||
snapshot_download(
|
||||
repo,
|
||||
allow_patterns=allow_patterns,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def shard_and_load(repo):
|
||||
# Get model path with everything but weight safetensors
|
||||
model_path = download(
|
||||
args.model,
|
||||
allow_patterns=["*.json", "*.py", "tokenizer.model", "*.tiktoken", "*.txt"],
|
||||
)
|
||||
|
||||
# Lazy load and shard model to figure out
|
||||
# which weights we need
|
||||
model, config = load_model(model_path, lazy=True, strict=False)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
model.model.pipeline(group)
|
||||
|
||||
# Figure out which files we need for the local shard
|
||||
with open(model_path / "model.safetensors.index.json", "r") as fid:
|
||||
weight_index = json.load(fid)["weight_map"]
|
||||
|
||||
local_files = set()
|
||||
for k, _ in tree_flatten(model.parameters()):
|
||||
local_files.add(weight_index[k])
|
||||
|
||||
# Download weights for local shard
|
||||
download(args.model, allow_patterns=local_files)
|
||||
|
||||
# Load and shard the model, and load the weights
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
{"trust_remote_code": True},
|
||||
eos_token_ids=config.get("eos_token_id", None),
|
||||
)
|
||||
model, _ = load_model(model_path, lazy=True, strict=False)
|
||||
model.model.pipeline(group)
|
||||
mx.eval(model.parameters())
|
||||
|
||||
# Synchronize processes before generation to avoid timeout if downloading
|
||||
# model for the first time.
|
||||
mx.eval(mx.distributed.all_sum(mx.array(1.0), stream=mx.cpu))
|
||||
return model, tokenizer
|
||||
|
||||
from mlx_lm import stream_generate
|
||||
from mlx_lm.utils import pipeline_load
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
|
||||
@@ -112,7 +52,7 @@ if __name__ == "__main__":
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model, tokenizer = shard_and_load(args.model)
|
||||
model, tokenizer = pipeline_load(args.model)
|
||||
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
|
||||
+12
-20
@@ -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,18 @@ def main() -> None:
|
||||
if fused_linears:
|
||||
model.update_modules(tree_unflatten(fused_linears))
|
||||
|
||||
if args.de_quantize:
|
||||
print("De-quantizing model")
|
||||
if args.dequantize:
|
||||
print("Dequantizing model")
|
||||
model = dequantize(model)
|
||||
config.pop("quantization", None)
|
||||
|
||||
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 +96,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)
|
||||
|
||||
|
||||
|
||||
+508
-30
@@ -7,6 +7,7 @@ import json
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -24,7 +25,13 @@ from transformers import PreTrainedTokenizer
|
||||
|
||||
from .models import cache
|
||||
from .models.cache import (
|
||||
ArraysCache,
|
||||
BatchKVCache,
|
||||
BatchRotatingKVCache,
|
||||
CacheList,
|
||||
KVCache,
|
||||
QuantizedKVCache,
|
||||
RotatingKVCache,
|
||||
load_prompt_cache,
|
||||
)
|
||||
from .sample_utils import make_sampler
|
||||
@@ -61,6 +68,11 @@ 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(
|
||||
"--adapter-path",
|
||||
type=str,
|
||||
@@ -276,16 +288,11 @@ class GenerationResponse:
|
||||
|
||||
|
||||
def maybe_quantize_kv_cache(prompt_cache, quantized_kv_start, kv_group_size, kv_bits):
|
||||
if (
|
||||
kv_bits is not None
|
||||
and not isinstance(prompt_cache[0], cache.QuantizedKVCache)
|
||||
and prompt_cache[0].offset > quantized_kv_start
|
||||
):
|
||||
for i in range(len(prompt_cache)):
|
||||
if isinstance(prompt_cache[i], cache.KVCache):
|
||||
prompt_cache[i] = prompt_cache[i].to_quantized(
|
||||
group_size=kv_group_size, bits=kv_bits
|
||||
)
|
||||
if kv_bits is None:
|
||||
return
|
||||
for e, c in enumerate(prompt_cache):
|
||||
if hasattr(c, "to_quantized") and c.offset >= quantized_kv_start:
|
||||
prompt_cache[e] = c.to_quantized(group_size=kv_group_size, bits=kv_bits)
|
||||
|
||||
|
||||
def generate_step(
|
||||
@@ -293,7 +300,7 @@ def generate_step(
|
||||
model: nn.Module,
|
||||
*,
|
||||
max_tokens: int = 256,
|
||||
sampler: Optional[Callable[mx.array, mx.array]] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = None,
|
||||
max_kv_size: Optional[int] = None,
|
||||
prompt_cache: Optional[Any] = None,
|
||||
@@ -301,7 +308,7 @@ def generate_step(
|
||||
kv_bits: Optional[int] = None,
|
||||
kv_group_size: int = 64,
|
||||
quantized_kv_start: int = 0,
|
||||
prompt_progress_callback: Optional[Callable[int, int]] = None,
|
||||
prompt_progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> Generator[Tuple[mx.array, mx.array], None, None]:
|
||||
"""
|
||||
@@ -327,7 +334,7 @@ def generate_step(
|
||||
kv_group_size (int): Group size for KV cache quantization. Default: ``64``.
|
||||
quantized_kv_start (int): Step to begin using a quantized KV cache.
|
||||
when ``kv_bits`` is non-None. Default: ``0``.
|
||||
prompt_progress_callback (Callable[int, int]): A call-back which takes the
|
||||
prompt_progress_callback (Callable[[int, int], None]): A call-back which takes the
|
||||
prompt tokens processed so far and the total number of prompt tokens.
|
||||
input_embeddings (mx.array, optional): Input embeddings to use instead of or in
|
||||
conjunction with prompt tokens. Default: ``None``.
|
||||
@@ -410,22 +417,25 @@ def generate_step(
|
||||
len(input_embeddings) if input_embeddings is not None else len(prompt)
|
||||
)
|
||||
prompt_processed_tokens = 0
|
||||
while total_prompt_tokens - prompt_processed_tokens > prefill_step_size:
|
||||
prompt_progress_callback(prompt_processed_tokens, total_prompt_tokens)
|
||||
while total_prompt_tokens - prompt_processed_tokens > 1:
|
||||
remaining = (total_prompt_tokens - prompt_processed_tokens) - 1
|
||||
n_to_process = min(prefill_step_size, remaining)
|
||||
_model_call(
|
||||
input_tokens=prompt[:prefill_step_size][None],
|
||||
input_tokens=prompt[:n_to_process][None],
|
||||
input_embeddings=(
|
||||
input_embeddings[:prefill_step_size][None]
|
||||
input_embeddings[:n_to_process][None]
|
||||
if input_embeddings is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
quantize_cache_fn(prompt_cache)
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
prompt_processed_tokens += n_to_process
|
||||
prompt_progress_callback(prompt_processed_tokens, total_prompt_tokens)
|
||||
prompt_processed_tokens += prefill_step_size
|
||||
prompt = prompt[prefill_step_size:]
|
||||
prompt = prompt[n_to_process:]
|
||||
input_embeddings = (
|
||||
input_embeddings[prefill_step_size:]
|
||||
input_embeddings[n_to_process:]
|
||||
if input_embeddings is not None
|
||||
else input_embeddings
|
||||
)
|
||||
@@ -456,9 +466,9 @@ def speculative_generate_step(
|
||||
model: nn.Module,
|
||||
draft_model: nn.Module,
|
||||
*,
|
||||
num_draft_tokens=2,
|
||||
num_draft_tokens: int = 2,
|
||||
max_tokens: int = 256,
|
||||
sampler: Optional[Callable[mx.array, mx.array]] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = None,
|
||||
prompt_cache: Optional[Any] = None,
|
||||
prefill_step_size: int = 512,
|
||||
@@ -477,7 +487,7 @@ def speculative_generate_step(
|
||||
speculative decoding. Default: ``2``.
|
||||
max_tokens (int): The maximum number of tokens. Use``-1`` for an infinite
|
||||
generator. Default: ``256``.
|
||||
sampler (Callable[mx.array, mx.array], optional): A sampler for sampling a
|
||||
sampler (Callable[[mx.array], mx.array], optional): A sampler for sampling a
|
||||
token from a vector of log probabilities. Default: ``None``.
|
||||
logits_processors (List[Callable[[mx.array, mx.array], mx.array]], optional):
|
||||
A list of functions that take tokens and logits and return the processed
|
||||
@@ -630,6 +640,7 @@ def stream_generate(
|
||||
model: nn.Module,
|
||||
tokenizer: Union[PreTrainedTokenizer, TokenizerWrapper],
|
||||
prompt: Union[str, mx.array, List[int]],
|
||||
max_tokens: int = 256,
|
||||
draft_model: Optional[nn.Module] = None,
|
||||
**kwargs,
|
||||
) -> Generator[GenerationResponse, None, None]:
|
||||
@@ -641,6 +652,8 @@ def stream_generate(
|
||||
tokenizer (PreTrainedTokenizer): The tokenizer.
|
||||
prompt (Union[str, mx.array, List[int]]): The input prompt string or
|
||||
integer tokens.
|
||||
max_tokens (int): The maximum number of tokens to generate.
|
||||
Default: ``256``.
|
||||
draft_model (Optional[nn.Module]): An optional draft model. If provided
|
||||
then speculative decoding is used. The draft model must use the same
|
||||
tokenizer as the main model. Default: ``None``.
|
||||
@@ -665,6 +678,8 @@ def stream_generate(
|
||||
|
||||
detokenizer = tokenizer.detokenizer
|
||||
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
|
||||
if draft_model is None:
|
||||
kwargs.pop("num_draft_tokens", None)
|
||||
token_generator = generate_step(prompt, model, **kwargs)
|
||||
@@ -674,11 +689,11 @@ def stream_generate(
|
||||
)
|
||||
else:
|
||||
kwargs.pop("max_kv_size", None)
|
||||
kwargs.pop("prompt_progress_callback", None)
|
||||
token_generator = speculative_generate_step(
|
||||
prompt, model, draft_model, **kwargs
|
||||
)
|
||||
with wired_limit(model, [generation_stream]):
|
||||
detokenizer.reset()
|
||||
tic = time.perf_counter()
|
||||
for n, (token, logprobs, from_draft) in enumerate(token_generator):
|
||||
if n == 0:
|
||||
@@ -689,6 +704,8 @@ def stream_generate(
|
||||
break
|
||||
|
||||
detokenizer.add_token(token)
|
||||
if (n + 1) == max_tokens:
|
||||
break
|
||||
|
||||
yield GenerationResponse(
|
||||
text=detokenizer.last_segment,
|
||||
@@ -723,7 +740,6 @@ def generate(
|
||||
tokenizer: Union[PreTrainedTokenizer, TokenizerWrapper],
|
||||
prompt: Union[str, List[int]],
|
||||
verbose: bool = False,
|
||||
formatter: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
@@ -738,11 +754,6 @@ def generate(
|
||||
kwargs: The remaining options get passed to :func:`stream_generate`.
|
||||
See :func:`stream_generate` for more details.
|
||||
"""
|
||||
if formatter is not None:
|
||||
print(
|
||||
"[Warning] Text formatting is deprecated and no longer used. "
|
||||
"The argument will be removed in a future version."
|
||||
)
|
||||
if verbose:
|
||||
print("=" * 10)
|
||||
|
||||
@@ -770,6 +781,473 @@ def generate(
|
||||
return text
|
||||
|
||||
|
||||
def _left_pad_prompts(prompts, max_length=None):
|
||||
if max_length is None:
|
||||
max_length = max(len(p) for p in prompts)
|
||||
return mx.array([[0] * (max_length - len(p)) + p for p in prompts])
|
||||
|
||||
|
||||
def _right_pad_prompts(prompts, max_length=None):
|
||||
if max_length is None:
|
||||
max_length = max(len(p) for p in prompts)
|
||||
return mx.array([p + [0] * (max_length - len(p)) for p in prompts])
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchStats:
|
||||
"""
|
||||
An data object to hold generation stats.
|
||||
|
||||
Args:
|
||||
prompt_tokens (int): The number of prompt tokens processed.
|
||||
prompt_tps (float): The prompt processing tokens-per-second.
|
||||
prompt_time (float): The time in seconds spent in prompt processing.
|
||||
generation_tokens (int): The number of generated tokens.
|
||||
generation_tps (float): The tokens-per-second for generation.
|
||||
generation_time (float): The time in seconds spent in generation .
|
||||
peak_memory (float): The peak memory used so far in GB.
|
||||
"""
|
||||
|
||||
prompt_tokens: int = 0
|
||||
prompt_tps: float = 0
|
||||
prompt_time: float = 0
|
||||
generation_tokens: int = 0
|
||||
generation_tps: float = 0
|
||||
generation_time: float = 0
|
||||
peak_memory: float = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchResponse:
|
||||
"""
|
||||
An data object to hold a batch generation response.
|
||||
|
||||
Args:
|
||||
texts: (List[str]): The generated text for each prompt.
|
||||
stats (BatchStats): Statistics about the generation.
|
||||
"""
|
||||
|
||||
texts: List[str]
|
||||
stats: BatchStats
|
||||
caches: Optional[List[List[Any]]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Batch:
|
||||
uids: List[int]
|
||||
y: mx.array
|
||||
logprobs: mx.array
|
||||
max_tokens: List[int]
|
||||
num_tokens: List[int]
|
||||
cache: List[Any]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.uids)
|
||||
|
||||
def filter(self, keep_idx: List[int]):
|
||||
self.uids = [self.uids[k] for k in keep_idx]
|
||||
self.logprobs = [self.logprobs[k] for k in keep_idx]
|
||||
self.max_tokens = [self.max_tokens[k] for k in keep_idx]
|
||||
self.num_tokens = [self.num_tokens[k] for k in keep_idx]
|
||||
keep_idx = mx.array(keep_idx, mx.int32)
|
||||
self.y = self.y[keep_idx]
|
||||
for c in self.cache:
|
||||
c.filter(keep_idx)
|
||||
|
||||
def extend(self, other):
|
||||
self.uids.extend(other.uids)
|
||||
self.y = mx.concatenate([self.y, other.y])
|
||||
self.logprobs.extend(other.logprobs)
|
||||
self.num_tokens.extend(other.num_tokens)
|
||||
self.max_tokens.extend(other.max_tokens)
|
||||
for c, o in zip(self.cache, other.cache):
|
||||
c.extend(o)
|
||||
|
||||
def extract_cache(self, idx):
|
||||
return [c.extract(idx) for c in self.cache]
|
||||
|
||||
|
||||
def _make_cache(model, left_padding):
|
||||
"""
|
||||
Convert a list of regular caches into their corresponding
|
||||
batch-aware caches.
|
||||
"""
|
||||
|
||||
def to_batch_cache(c):
|
||||
if isinstance(c, KVCache):
|
||||
return BatchKVCache(left_padding)
|
||||
elif isinstance(c, ArraysCache):
|
||||
c.left_padding = mx.array(left_padding)
|
||||
return c
|
||||
elif isinstance(c, RotatingKVCache):
|
||||
if c.keep > 0:
|
||||
raise ValueError("RotatingKVCache with keep tokens is not supported.")
|
||||
return BatchRotatingKVCache(c.max_size, left_padding)
|
||||
elif isinstance(c, CacheList):
|
||||
return CacheList(*(to_batch_cache(sub_c) for sub_c in c.caches))
|
||||
else:
|
||||
raise ValueError(f"{type(c)} does not yet support batching")
|
||||
|
||||
if hasattr(model, "make_cache"):
|
||||
cache = model.make_cache()
|
||||
return [to_batch_cache(c) for c in cache]
|
||||
else:
|
||||
return [BatchKVCache(left_padding) for _ in model.layers]
|
||||
|
||||
|
||||
def _merge_caches(caches):
|
||||
batch_cache = []
|
||||
for i in range(len(caches[0])):
|
||||
cache = None
|
||||
if isinstance(caches[0][i], KVCache):
|
||||
cache = BatchKVCache.merge([c[i] for c in caches])
|
||||
elif isinstance(caches[0][i], RotatingKVCache):
|
||||
cache = BatchRotatingKVCache.merge([c[i] for c in caches])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{type(caches[0][i])} does not yet support batching with history"
|
||||
)
|
||||
batch_cache.append(cache)
|
||||
return batch_cache
|
||||
|
||||
|
||||
class BatchGenerator:
|
||||
|
||||
@dataclass
|
||||
class Response:
|
||||
uid: int
|
||||
token: int
|
||||
logprobs: mx.array
|
||||
finish_reason: Optional[str]
|
||||
prompt_cache: Callable[[], List[Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
max_tokens: int = 128,
|
||||
stop_tokens: Optional[set] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
completion_batch_size: int = 32,
|
||||
prefill_batch_size: int = 8,
|
||||
prefill_step_size: int = 2048,
|
||||
prompt_progress_callback: Optional[
|
||||
Callable[[List[Tuple[int, int, int]]], None]
|
||||
] = None,
|
||||
):
|
||||
self.model = model
|
||||
self.unprocessed_prompts = []
|
||||
self.max_tokens = max_tokens
|
||||
self.stop_tokens = stop_tokens or set()
|
||||
self.sampler = sampler or (lambda x: mx.argmax(x, axis=-1))
|
||||
self.uid_count = 0
|
||||
self.prefill_step_size = prefill_step_size
|
||||
self.prefill_batch_size = prefill_batch_size
|
||||
self.completion_batch_size = max(completion_batch_size, prefill_batch_size)
|
||||
self.prompt_progress_callback = prompt_progress_callback or (lambda *_: None)
|
||||
self._stats = BatchStats()
|
||||
|
||||
self.active_batch = None
|
||||
|
||||
if mx.metal.is_available():
|
||||
self._old_wired_limit = mx.set_wired_limit(
|
||||
mx.metal.device_info()["max_recommended_working_set_size"]
|
||||
)
|
||||
else:
|
||||
self._old_wired_limit = None
|
||||
|
||||
def close(self):
|
||||
if self._old_wired_limit is not None:
|
||||
mx.synchronize(generation_stream)
|
||||
mx.set_wired_limit(self._old_wired_limit)
|
||||
self._old_wired_limit = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def insert(
|
||||
self, prompts, max_tokens: Union[List[int], int, None] = None, caches=None
|
||||
):
|
||||
uids = []
|
||||
|
||||
if max_tokens is None or isinstance(max_tokens, int):
|
||||
max_tokens = [max_tokens or self.max_tokens] * len(prompts)
|
||||
|
||||
if caches is None:
|
||||
caches = [None] * len(prompts)
|
||||
for i in range(len(prompts)):
|
||||
if caches[i] is None:
|
||||
caches[i] = cache.make_prompt_cache(self.model)
|
||||
|
||||
for p, m, c in zip(prompts, max_tokens, caches):
|
||||
self.unprocessed_prompts.append((self.uid_count, p, m, c))
|
||||
uids.append(self.uid_count)
|
||||
self.uid_count += 1
|
||||
# Sort in ascending order of length
|
||||
self.unprocessed_prompts = sorted(
|
||||
self.unprocessed_prompts, key=lambda x: len(x[1]) + cache.cache_length(x[3])
|
||||
)
|
||||
return uids
|
||||
|
||||
def remove(self, uids: List[int]):
|
||||
uids = set(uids)
|
||||
if self.active_batch is not None:
|
||||
batch = self.active_batch
|
||||
keep_idx = [e for e, uid in enumerate(batch.uids) if uid not in uids]
|
||||
if len(keep_idx) > 0:
|
||||
batch.filter(keep_idx)
|
||||
else:
|
||||
self.active_batch = None
|
||||
|
||||
for i in reversed(range(len(self.unprocessed_prompts))):
|
||||
if self.unprocessed_prompts[i][0] in uids:
|
||||
self.unprocessed_prompts.pop(i)
|
||||
|
||||
def _process_prompts(self, prompts):
|
||||
uids, inputs, max_tokens, caches = zip(*prompts)
|
||||
|
||||
cache_lengths = [cache.cache_length(c) for c in caches]
|
||||
max_cache_length = max(cache_lengths)
|
||||
lengths = [len(p) for p in inputs]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
|
||||
self._stats.prompt_tokens += sum(lengths)
|
||||
|
||||
processed_tokens = 0
|
||||
|
||||
# New prompts so
|
||||
# 1. Left-pad the inputs
|
||||
# 2. Process
|
||||
if max_cache_length == 0:
|
||||
inputs = _left_pad_prompts(inputs, max_length=max_length)
|
||||
prompt_cache = _make_cache(self.model, padding)
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
self.model(inputs[:, :n_to_process], cache=prompt_cache)
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
inputs = inputs[:, n_to_process:]
|
||||
processed_tokens += n_to_process
|
||||
self.prompt_progress_callback(
|
||||
[
|
||||
(uid, processed_tokens, length)
|
||||
for uid, length in zip(uids, lengths)
|
||||
]
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
# Further prompt processing so we need to
|
||||
# 1. Merge the KV caches and prepare for right padded prompts
|
||||
# 2. Right pad the inputs
|
||||
# 2. Process
|
||||
# 3. Finalize the KV caches so they are left padded again
|
||||
else:
|
||||
last_inputs = mx.array([p[-1:] for p in inputs])
|
||||
inputs = _right_pad_prompts(inputs, max_length=max_length)
|
||||
prompt_cache = _merge_caches(caches)
|
||||
|
||||
for c in prompt_cache:
|
||||
c.prepare(lengths=lengths, right_padding=padding)
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
self.model(inputs[:, :n_to_process], cache=prompt_cache)
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
inputs = inputs[:, n_to_process:]
|
||||
processed_tokens += n_to_process
|
||||
self.prompt_progress_callback(
|
||||
[
|
||||
(uid, processed_tokens, length)
|
||||
for uid, length in zip(uids, lengths)
|
||||
]
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
for c in prompt_cache:
|
||||
c.finalize()
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
mx.clear_cache()
|
||||
inputs = last_inputs
|
||||
|
||||
y, logprobs = self._step(inputs, prompt_cache)
|
||||
mx.async_eval(y, logprobs)
|
||||
return Batch(
|
||||
list(uids), y, logprobs, list(max_tokens), [0] * len(uids), prompt_cache
|
||||
)
|
||||
|
||||
def _step(self, input_tokens: mx.array, prompt_cache: List[Any]):
|
||||
logits = self.model(input_tokens, cache=prompt_cache)
|
||||
logits = logits[:, -1, :]
|
||||
logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
|
||||
sampled = self.sampler(logprobs)
|
||||
return sampled, list(logprobs)
|
||||
|
||||
def stats(self):
|
||||
self._stats.prompt_tps = self._stats.prompt_tokens / self._stats.prompt_time
|
||||
self._stats.generation_tps = (
|
||||
self._stats.generation_tokens / self._stats.generation_time
|
||||
)
|
||||
self._stats.peak_memory = mx.get_peak_memory() / 1e9
|
||||
return self._stats
|
||||
|
||||
def _next(self):
|
||||
tic = time.perf_counter()
|
||||
|
||||
prompt_processing = False
|
||||
batch = self.active_batch
|
||||
num_active = len(batch) if batch else 0
|
||||
num_to_add = self.completion_batch_size - num_active
|
||||
while num_to_add >= self.prefill_batch_size:
|
||||
prompts = self.unprocessed_prompts[: self.prefill_batch_size]
|
||||
# Finish processing the last examples of the last batch
|
||||
if len(prompts) == 0 and num_active > 0:
|
||||
break
|
||||
# No more prompts and no more completions, all done
|
||||
elif len(prompts) == 0:
|
||||
self.active_batch = None
|
||||
return []
|
||||
# Process prompts
|
||||
if batch is not None and not prompt_processing:
|
||||
# Finish any active completion tokens
|
||||
mx.eval(batch.y, batch.logprobs)
|
||||
self._stats.generation_time += time.perf_counter() - tic
|
||||
tic = time.perf_counter()
|
||||
|
||||
batch = self._process_prompts(prompts)
|
||||
self.unprocessed_prompts = self.unprocessed_prompts[
|
||||
self.prefill_batch_size :
|
||||
]
|
||||
prompt_processing = True
|
||||
# If there was no active batch, set it
|
||||
if self.active_batch is None:
|
||||
self.active_batch = batch
|
||||
else:
|
||||
self.active_batch.extend(batch)
|
||||
|
||||
num_active = len(self.active_batch)
|
||||
num_to_add -= len(batch)
|
||||
|
||||
batch = self.active_batch
|
||||
y, logprobs = batch.y, batch.logprobs
|
||||
batch.y, batch.logprobs = self._step(y[:, None], batch.cache)
|
||||
mx.async_eval(batch.y, batch.logprobs)
|
||||
|
||||
y = y.tolist()
|
||||
toc = time.perf_counter()
|
||||
if prompt_processing:
|
||||
self._stats.prompt_time += toc - tic
|
||||
else:
|
||||
self._stats.generation_time += toc - tic
|
||||
keep_idx = []
|
||||
end_idx = []
|
||||
responses = []
|
||||
|
||||
for e, (t, uid, num_tok, max_tok) in enumerate(
|
||||
zip(y, batch.uids, batch.num_tokens, batch.max_tokens)
|
||||
):
|
||||
cache = None
|
||||
num_tok += 1
|
||||
batch.num_tokens[e] = num_tok
|
||||
if t in self.stop_tokens:
|
||||
finish_reason = "stop"
|
||||
end_idx.append(e)
|
||||
elif num_tok >= max_tok:
|
||||
finish_reason = "length"
|
||||
end_idx.append(e)
|
||||
else:
|
||||
finish_reason = None
|
||||
keep_idx.append(e)
|
||||
if finish_reason is not None:
|
||||
cache = batch.extract_cache(e)
|
||||
responses.append(self.Response(uid, t, logprobs[e], finish_reason, cache))
|
||||
|
||||
# Remove any finished completions
|
||||
if len(end_idx):
|
||||
if len(keep_idx) > 0:
|
||||
batch.filter(keep_idx)
|
||||
else:
|
||||
self.active_batch = None
|
||||
|
||||
self._stats.generation_tokens += len(responses)
|
||||
return responses
|
||||
|
||||
def next(self):
|
||||
with mx.stream(generation_stream):
|
||||
return self._next()
|
||||
|
||||
|
||||
def batch_generate(
|
||||
model,
|
||||
tokenizer,
|
||||
prompts: List[int],
|
||||
prompt_caches: Optional[List[List[Any]]] = None,
|
||||
max_tokens: Union[int, List[int]] = 128,
|
||||
verbose: bool = False,
|
||||
return_prompt_caches: bool = False,
|
||||
**kwargs,
|
||||
) -> BatchResponse:
|
||||
"""
|
||||
Generate responses for the given batch of prompts.
|
||||
|
||||
Args:
|
||||
model (nn.Module): The language model.
|
||||
tokenizer (PreTrainedTokenizer): The tokenizer.
|
||||
prompt (List[List[int]]): The input prompts.
|
||||
prompt_caches (List[List[Any]], optional): Pre-computed prompt-caches
|
||||
for each input prompt. Note, unlike ``generate_step``, the caches
|
||||
won't be updated in-place.
|
||||
verbose (bool): If ``True``, print tokens and timing information.
|
||||
Default: ``False``.
|
||||
max_tokens (Union[int, List[int]): Maximum number of output tokens. This
|
||||
can be per prompt if a list is provided.
|
||||
return_prompt_caches (bool): Return the prompt caches in the batch
|
||||
responses. Default: ``False``.
|
||||
kwargs: The remaining options get passed to :obj:`BatchGenerator`.
|
||||
See :obj:`BatchGenerator` for more details.
|
||||
"""
|
||||
|
||||
gen = BatchGenerator(model, stop_tokens=tokenizer.eos_token_ids, **kwargs)
|
||||
num_samples = len(prompts)
|
||||
fin = 0
|
||||
if verbose:
|
||||
print(f"[batch_generate] Finished processing 0/{num_samples} ...", end="\r")
|
||||
|
||||
uids = gen.insert(prompts, max_tokens, caches=prompt_caches)
|
||||
results = {uid: [] for uid in uids}
|
||||
prompt_caches = {}
|
||||
while responses := gen.next():
|
||||
for r in responses:
|
||||
if r.finish_reason is not None:
|
||||
if return_prompt_caches:
|
||||
prompt_caches[r.uid] = r.prompt_cache
|
||||
if verbose:
|
||||
fin += 1
|
||||
print(
|
||||
f"[batch_generate] Finished processing {fin}/{num_samples} ...",
|
||||
end="\r",
|
||||
)
|
||||
if r.finish_reason != "stop":
|
||||
results[r.uid].append(r.token)
|
||||
gen.close()
|
||||
if verbose:
|
||||
print(f"[batch_generate] Finished processing {fin}/{num_samples}")
|
||||
|
||||
# Return results in correct order
|
||||
texts = [tokenizer.decode(results[uid]) for uid in uids]
|
||||
stats = gen.stats()
|
||||
caches = [prompt_caches[uid] for uid in uids] if return_prompt_caches else None
|
||||
if verbose:
|
||||
print(
|
||||
f"[batch_generate] Prompt: {stats.prompt_tokens} tokens, {stats.prompt_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
print(
|
||||
f"[batch_generate] Generation: {stats.generation_tokens} tokens, "
|
||||
f"{stats.generation_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
print(f"[batch_generate] Peak memory: {stats.peak_memory:.3f} GB")
|
||||
return BatchResponse(texts, stats, caches)
|
||||
|
||||
|
||||
def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
@@ -798,7 +1276,7 @@ def main():
|
||||
tokenizer_config = (
|
||||
{} if not using_cache else json.loads(metadata["tokenizer_config"])
|
||||
)
|
||||
tokenizer_config["trust_remote_code"] = True
|
||||
tokenizer_config["trust_remote_code"] = True if args.trust_remote_code else None
|
||||
|
||||
model_path = args.model
|
||||
if using_cache:
|
||||
|
||||
+29
-16
@@ -3,6 +3,7 @@ import math
|
||||
import os
|
||||
import re
|
||||
import types
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import mlx.core as mx
|
||||
@@ -11,7 +12,7 @@ import mlx.optimizers as optim
|
||||
import numpy as np
|
||||
import yaml
|
||||
|
||||
from .tuner.callbacks import WandBCallback
|
||||
from .tuner.callbacks import get_reporting_callbacks
|
||||
from .tuner.datasets import CacheDataset, load_dataset
|
||||
from .tuner.trainer import TrainingArgs, TrainingCallback, evaluate, train
|
||||
from .tuner.utils import (
|
||||
@@ -39,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",
|
||||
@@ -50,7 +51,7 @@ CONFIG_DEFAULTS = {
|
||||
"sgd": {},
|
||||
"adafactor": {},
|
||||
},
|
||||
"data": "data/",
|
||||
"data": "mlx-community/WikiSQL",
|
||||
"seed": 0,
|
||||
"num_layers": 16,
|
||||
"batch_size": 4,
|
||||
@@ -67,10 +68,12 @@ CONFIG_DEFAULTS = {
|
||||
"max_seq_length": 2048,
|
||||
"config": None,
|
||||
"grad_checkpoint": False,
|
||||
"grad_accumulation_steps": 1,
|
||||
"lr_schedule": None,
|
||||
"lora_parameters": {"rank": 8, "dropout": 0.0, "scale": 20.0},
|
||||
"mask_prompt": False,
|
||||
"wandb": None,
|
||||
"report_to": None,
|
||||
"project_name": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +109,7 @@ def build_parser():
|
||||
parser.add_argument(
|
||||
"--optimizer",
|
||||
type=str,
|
||||
choices=["adam", "adamw", "sgd", "adafactor"],
|
||||
choices=["adam", "adamw", "muon", "sgd", "adafactor"],
|
||||
default=None,
|
||||
help="Optimizer to use for training: adam, adamw, sgd, or adafactor.",
|
||||
)
|
||||
@@ -139,6 +142,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,
|
||||
@@ -183,10 +191,16 @@ def build_parser():
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wandb",
|
||||
"--report-to",
|
||||
type=str,
|
||||
default=None,
|
||||
help="WandB project name to report training metrics. Disabled if None.",
|
||||
help="Services to report logs to ('wandb', 'swanlab', or 'wandb,swanlab').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-name",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Project name for logging. Defaults to the name of the root directory.",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, help="The PRNG seed")
|
||||
return parser
|
||||
@@ -247,6 +261,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
|
||||
@@ -296,17 +311,15 @@ 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:
|
||||
training_callback = WandBCallback(
|
||||
project_name=args.wandb,
|
||||
log_dir=args.adapter_path,
|
||||
config=vars(args),
|
||||
wrapped_callback=training_callback,
|
||||
)
|
||||
training_callback = get_reporting_callbacks(
|
||||
args.report_to,
|
||||
project_name=args.project_name,
|
||||
log_dir=args.adapter_path,
|
||||
config=vars(args),
|
||||
)
|
||||
|
||||
print("Loading pretrained model")
|
||||
model, tokenizer = load(args.model)
|
||||
model, tokenizer = load(args.model, tokenizer_config={"trust_remote_code": True})
|
||||
|
||||
print("Loading datasets")
|
||||
train_set, valid_set, test_set = load_dataset(args, tokenizer)
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
attention_bias: bool
|
||||
mlp_only_layers: List[int]
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
decoder_sparse_step: int
|
||||
n_shared_experts: int
|
||||
moe_intermediate_size: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
num_key_value_heads: int
|
||||
rope_theta: float
|
||||
max_position_embeddings: int
|
||||
norm_topk_prob: bool
|
||||
|
||||
|
||||
class KlearAttention(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,
|
||||
self.num_attention_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
self.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
self.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
|
||||
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.rope = nn.RoPE(
|
||||
self.head_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)
|
||||
|
||||
queries = self.q_norm(
|
||||
queries.reshape(B, L, self.num_attention_heads, -1)
|
||||
).transpose(0, 2, 1, 3)
|
||||
keys = self.k_norm(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 KlearMLP(nn.Module):
|
||||
def __init__(self, dim, hidden_dim):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class KlearSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.num_experts = args.num_experts
|
||||
self.top_k = args.num_experts_per_tok
|
||||
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.experts = SwitchGLU(
|
||||
args.hidden_size, args.moe_intermediate_size, args.num_experts
|
||||
)
|
||||
self.shared_experts = KlearMLP(
|
||||
args.hidden_size,
|
||||
hidden_dim=args.moe_intermediate_size * args.n_shared_experts,
|
||||
)
|
||||
self.coefficient = nn.Linear(args.hidden_size, 2)
|
||||
self.expert_bias = mx.zeros((self.num_experts,), dtype=mx.float32)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
routing_weights = mx.sigmoid(self.gate(x).astype(mx.float32))
|
||||
biased_weights = routing_weights + self.expert_bias.reshape((1, 1, -1))
|
||||
k = self.top_k
|
||||
inds = mx.argpartition(-biased_weights, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(routing_weights, inds, axis=-1)
|
||||
if self.norm_topk_prob:
|
||||
scores = scores / mx.sum(scores, axis=-1, keepdims=True)
|
||||
scores = scores.astype(x.dtype)
|
||||
expert_out = self.experts(x, inds)
|
||||
y_experts = (expert_out * scores[..., None]).sum(axis=-2)
|
||||
coef = mx.softmax(self.coefficient(x), axis=-1, precise=True)
|
||||
shared = self.shared_experts(x)
|
||||
y = y_experts * coef[..., :1] + shared * coef[..., 1:]
|
||||
return y
|
||||
|
||||
|
||||
class KlearDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = KlearAttention(args)
|
||||
|
||||
if (layer_idx not in args.mlp_only_layers) and (
|
||||
args.num_experts > 0 and (layer_idx + 1) % args.decoder_sparse_step == 0
|
||||
):
|
||||
self.mlp = KlearSparseMoeBlock(args)
|
||||
else:
|
||||
self.mlp = KlearMLP(args.hidden_size, args.intermediate_size)
|
||||
|
||||
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))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class KlearModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
KlearDecoderLayer(args=args, layer_idx=i)
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
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)
|
||||
|
||||
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 = KlearModel(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)
|
||||
|
||||
def sanitize(self, weights):
|
||||
if "model.layers.0.mlp.experts.0.gate_proj.weight" not in weights:
|
||||
return weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.mlp.experts"
|
||||
for name in ["gate_proj", "up_proj", "down_proj"]:
|
||||
stacked = [
|
||||
weights.pop(f"{prefix}.{e}.{name}.weight")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.{name}.weight"] = mx.stack(stacked)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.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
|
||||
@@ -50,7 +50,7 @@ class FusedLoRALinear(nn.Module):
|
||||
]
|
||||
self.lora_b = [mx.zeros((r, od)) for od in output_dims]
|
||||
|
||||
def fuse(self, de_quantize: bool = False):
|
||||
def fuse(self, dequantize: bool = False):
|
||||
linear = self.linear
|
||||
weight = linear.weight
|
||||
is_quantized = isinstance(linear, FusedQuantizedLinear)
|
||||
@@ -79,7 +79,7 @@ class FusedLoRALinear(nn.Module):
|
||||
delta = mx.concatenate(deltas, axis=0)
|
||||
fused_linear.weight = weight + delta
|
||||
|
||||
if is_quantized and not de_quantize:
|
||||
if is_quantized and not dequantize:
|
||||
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
|
||||
|
||||
return fused_linear
|
||||
@@ -350,18 +350,16 @@ class AFMModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embedding(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
cache[-1] = ConcatenateKVCache()
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
@@ -382,10 +380,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embedding.as_linear(out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
layer_types: List[str]
|
||||
vocab_size: int = 200192
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 6144
|
||||
moe_intermediate_size: int = 1024
|
||||
num_hidden_layers: int = 32
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 4
|
||||
head_dim: int = 64
|
||||
max_position_embeddings: int = 131072
|
||||
rms_norm_eps: float = 1e-5
|
||||
rope_theta: float = 10000
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
# MoE config
|
||||
num_experts: int = 128
|
||||
num_experts_per_tok: int = 8
|
||||
num_shared_experts: int = 1
|
||||
num_dense_layers: int = 2
|
||||
route_norm: bool = True
|
||||
route_scale: float = 2.826
|
||||
score_func: str = "sigmoid"
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
sliding_window: int = 2048
|
||||
mup_enabled: bool = True
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, is_local_attention: bool = False):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.is_local_attention = is_local_attention
|
||||
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
|
||||
if is_local_attention:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False, # traditional
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
else:
|
||||
self.rope = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
if self.is_local_attention and self.rope is not None:
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
gate = mx.sigmoid(self.gate_proj(x))
|
||||
output = output * gate
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class MoERouter(nn.Module):
|
||||
"""Router module that wraps the gate for proper weight naming."""
|
||||
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.gate(x)
|
||||
|
||||
|
||||
class AfmoeMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts = args.num_experts
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.route_norm = args.route_norm
|
||||
self.route_scale = args.route_scale
|
||||
self.score_func = args.score_func
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
|
||||
self.router = MoERouter(args)
|
||||
|
||||
self.expert_bias = mx.zeros((args.num_experts,))
|
||||
|
||||
self.experts = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
if args.num_shared_experts > 0:
|
||||
shared_intermediate_size = (
|
||||
args.moe_intermediate_size * args.num_shared_experts
|
||||
)
|
||||
self.shared_experts = MLP(args, intermediate_size=shared_intermediate_size)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
|
||||
if self.score_func == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
|
||||
# Add expert bias for selection
|
||||
selection_scores = scores + self.expert_bias
|
||||
|
||||
# Group-based expert selection if n_group > 1
|
||||
if self.n_group > 1:
|
||||
selection_scores = mx.unflatten(
|
||||
selection_scores, axis=-1, shape=(self.n_group, -1)
|
||||
)
|
||||
group_scores = mx.topk(selection_scores, 2, axis=-1).sum(
|
||||
axis=-1, keepdims=True
|
||||
)
|
||||
k = self.n_group - self.topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
selection_scores = mx.put_along_axis(
|
||||
selection_scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
selection_scores = mx.flatten(selection_scores, -2, -1)
|
||||
|
||||
# Select top-k experts
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.argpartition(-selection_scores, kth=k - 1, axis=-1)[..., :k]
|
||||
|
||||
selected_scores = mx.take_along_axis(scores, inds, axis=-1)
|
||||
|
||||
if self.route_norm and self.num_experts_per_tok > 1:
|
||||
denominator = selected_scores.sum(axis=-1, keepdims=True)
|
||||
selected_scores = selected_scores / denominator
|
||||
|
||||
selected_scores = selected_scores * self.route_scale
|
||||
|
||||
y = self.experts(x, inds)
|
||||
y = (y * selected_scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
if self.args.num_shared_experts > 0:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.self_attn = Attention(args, is_local_attention=use_sliding)
|
||||
|
||||
if layer_idx < args.num_dense_layers:
|
||||
self.mlp = MLP(args)
|
||||
else:
|
||||
self.mlp = AfmoeMoE(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.pre_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
r = self.post_attention_layernorm(r)
|
||||
h = x + r
|
||||
|
||||
r = self.mlp(self.pre_mlp_layernorm(h))
|
||||
r = self.post_mlp_layernorm(r)
|
||||
return h + r
|
||||
|
||||
|
||||
class AfmoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
self.mup_enabled = args.mup_enabled
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(
|
||||
args=args, layer_idx=idx, use_sliding=layer_type == "sliding_attention"
|
||||
)
|
||||
for idx, layer_type in enumerate(self.layer_types)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for idx, layer in enumerate(self.layers):
|
||||
if layer.use_sliding:
|
||||
self.swa_idx = idx
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if self.mup_enabled:
|
||||
h = h * math.sqrt(self.hidden_size)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
swa_mask = None
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = AfmoeModel(args)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove unused precomputed rotary freqs
|
||||
weights = {k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
# Stack experts weights for SwitchGLU
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if l < self.args.num_dense_layers:
|
||||
continue
|
||||
prefix = f"model.layers.{l}"
|
||||
for n in ["up_proj", "down_proj", "gate_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{n}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{n}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.experts.{n}.{k}"] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if "router.gate" in path:
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,219 @@
|
||||
# Copyright © 2023-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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
mlp_bias: bool
|
||||
num_attention_heads: int
|
||||
attention_bias: bool
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
rope_theta: float
|
||||
post_norm: bool
|
||||
qk_norm: bool
|
||||
tie_word_embeddings: bool
|
||||
rope_traditional: bool = False
|
||||
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__()
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, args.intermediate_size, bias=args.mlp_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
args.intermediate_size, args.hidden_size, bias=args.mlp_bias
|
||||
)
|
||||
self.act_fn = XieLU()
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(self.act_fn(self.up_proj(x)))
|
||||
|
||||
|
||||
class ApertusAttention(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
|
||||
)
|
||||
|
||||
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.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
args.rope_traditional,
|
||||
args.rope_scaling,
|
||||
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.num_attention_heads, -1)
|
||||
).transpose(0, 2, 1, 3)
|
||||
keys = self.k_norm(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 ApertusDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = ApertusAttention(args)
|
||||
self.mlp = ApertusMLP(args)
|
||||
|
||||
self.attention_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.feedforward_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:
|
||||
h = x + self.self_attn(self.attention_layernorm(x), mask, cache)
|
||||
out = h + self.mlp(self.feedforward_layernorm(h))
|
||||
return out
|
||||
|
||||
|
||||
class ApertusModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
ApertusDecoderLayer(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,
|
||||
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=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 = ApertusModel(args)
|
||||
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)
|
||||
return self.lm_head(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()
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -96,7 +96,10 @@ class Attention(nn.Module):
|
||||
k = k.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
|
||||
v = v.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
if cache is None:
|
||||
cache = (None, None)
|
||||
|
||||
if cache[0] is not None:
|
||||
offset = cache[1].offset
|
||||
last_k, last_v = cache[0][0], cache[0][1]
|
||||
else:
|
||||
@@ -110,7 +113,7 @@ class Attention(nn.Module):
|
||||
q = self.rope(q, offset=offset)
|
||||
k = self.rope(k, offset=offset)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is not None:
|
||||
k, v = cache[1].update_and_fetch(k, v)
|
||||
if L > 0:
|
||||
cache[0][0] = k_init[:, :, -1:, :]
|
||||
@@ -167,17 +170,40 @@ class BaichuanModel(nn.Module):
|
||||
self.layers = [DecoderLayer(config, i) for i in range(config.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self, inputs: mx.array, mask: mx.array = None, cache: Any = None
|
||||
) -> mx.array:
|
||||
self.sliding_window = config.sliding_window
|
||||
self.first_swa_idx = None
|
||||
if config.sliding_window_layers:
|
||||
self.first_swa_idx = config.sliding_window_layers[0]
|
||||
|
||||
self.first_global_idx = None
|
||||
self.swa_layers = set(config.sliding_window_layers)
|
||||
for i in range(config.num_hidden_layers):
|
||||
if i in self.swa_layers:
|
||||
continue
|
||||
self.first_global_idx = i
|
||||
break
|
||||
|
||||
def __call__(self, inputs: mx.array, cache: Any = None) -> mx.array:
|
||||
x = self.embed_tokens(inputs)
|
||||
if mask is None:
|
||||
if cache is not None:
|
||||
c = [cache[0][1]]
|
||||
mask = create_attention_mask(x, c)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
for layer, c in zip(self.layers, cache):
|
||||
cache = [(None, None)] * len(self.layers)
|
||||
|
||||
if self.first_global_idx is None:
|
||||
c_global = None
|
||||
else:
|
||||
c_global = cache[self.first_global_idx][1]
|
||||
|
||||
if self.first_swa_idx is None:
|
||||
c_swa = None
|
||||
else:
|
||||
c_swa = cache[self.first_swa_idx][1]
|
||||
|
||||
global_mask = create_attention_mask(x, c_global)
|
||||
swa_mask = create_attention_mask(x, c_swa, window_size=self.sliding_window)
|
||||
|
||||
for l, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
mask = swa_mask if l in self.swa_layers else global_mask
|
||||
x = layer(x, mask, c)
|
||||
return self.norm(x)
|
||||
|
||||
@@ -215,10 +241,8 @@ class Model(nn.Module):
|
||||
weights["lm_head.weight"] = w
|
||||
return weights
|
||||
|
||||
def __call__(
|
||||
self, inputs: mx.array, mask: mx.array = None, cache: Any = None
|
||||
) -> mx.array:
|
||||
outputs = self.model(inputs, mask, cache)
|
||||
def __call__(self, inputs: mx.array, cache: Any = None) -> mx.array:
|
||||
outputs = self.model(inputs, cache)
|
||||
return self.lm_head(outputs)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
# 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
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
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
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
|
||||
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
|
||||
score_function: str = "softmax"
|
||||
n_group: int = 1
|
||||
topk_group: int = 4
|
||||
moe_shared_expert_intermediate_size: Optional[int] = None
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(gate, up):
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@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__()
|
||||
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 BailingMoeAttention(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.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)
|
||||
|
||||
if (rope_dim := args.rotary_dim) is None:
|
||||
rope_dim = int(self.head_dim * args.partial_rotary_factor)
|
||||
self.rope = initialize_rope(
|
||||
rope_dim,
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
score_function,
|
||||
):
|
||||
|
||||
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, scores.dtype), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_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) + 1e-20
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores.astype(in_type)
|
||||
|
||||
|
||||
class BailingMoeGate(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):
|
||||
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 BailingMoeSparseMoeBlock(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 = BailingMoeGate(args)
|
||||
shared_dim = (
|
||||
args.moe_shared_expert_intermediate_size or args.moe_intermediate_size
|
||||
)
|
||||
self.shared_experts = (
|
||||
BailingMoeMLP(
|
||||
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):
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = aggregate_expert_outputs(out, topk_weight)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class BailingMoeDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.attention = BailingMoeAttention(args)
|
||||
|
||||
self.mlp = (
|
||||
BailingMoeSparseMoeBlock(args)
|
||||
if (
|
||||
args.num_experts is not None and layer_idx >= args.first_k_dense_replace
|
||||
)
|
||||
else BailingMoeMLP(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 = self.attention(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class BailingMoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
BailingMoeDecoderLayer(args, layer_idx=i)
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
h = self.word_embeddings(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.norm_head = args.norm_head
|
||||
self.model_type = args.model_type
|
||||
self.model = BailingMoeModel(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.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}"
|
||||
|
||||
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
|
||||
@@ -0,0 +1,594 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
max_position_embeddings: int
|
||||
moe_intermediate_size: int
|
||||
num_experts: int
|
||||
num_shared_experts: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_experts_per_tok: int
|
||||
num_hidden_layers: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
vocab_size: int
|
||||
first_k_dense_replace: int
|
||||
layer_group_size: int
|
||||
group_norm_size: int
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
rope_traditional: bool = False
|
||||
use_bias: bool = False
|
||||
use_qkv_bias: bool = False
|
||||
norm_head: bool = False
|
||||
norm_softmax: bool = False
|
||||
use_qk_norm: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
moe_router_enable_expert_bias: bool = False
|
||||
moe_router_enable_routed_scaling: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
score_function: str = "softmax"
|
||||
n_group: int = 1
|
||||
topk_group: int = 4
|
||||
use_rmsnorm: bool = True
|
||||
moe_shared_expert_intermediate_size: Optional[int] = None
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
head_dim: Optional[int] = None
|
||||
|
||||
|
||||
def recurrent_gla(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
g: mx.array,
|
||||
scale: float,
|
||||
h: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
"""
|
||||
Recurrence per (b, h):
|
||||
h_t = h_{t-1} * exp(g_t)
|
||||
h_t = h_t + k_t^T @ v_t
|
||||
y_t = (q_t @ h_t) * scale
|
||||
Returns y with shape [B, H, T, Dv].
|
||||
"""
|
||||
B, Hq, L, K = q.shape
|
||||
Hv = k.shape[1]
|
||||
V = v.shape[-1]
|
||||
|
||||
outputs = []
|
||||
exp_g = mx.exp(g)[:, None, None].astype(q.dtype)
|
||||
q = q * scale
|
||||
for t in range(L):
|
||||
q_t = q[:, :, t : t + 1]
|
||||
k_t = k[:, :, t : t + 1]
|
||||
v_t = v[:, :, t : t + 1]
|
||||
h_up = k_t.transpose(0, 1, 3, 2) @ v_t
|
||||
if h is not None:
|
||||
h = h * exp_g + h_up
|
||||
else:
|
||||
h = h_up
|
||||
o_t = q_t @ h
|
||||
outputs.append(o_t)
|
||||
|
||||
return mx.concatenate(outputs, axis=2), h
|
||||
|
||||
|
||||
class GroupRMSNorm(nn.Module):
|
||||
def __init__(self, dims: int, eps: float = 1e-5, groups: int = 1):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((dims,))
|
||||
self.groups = groups
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
shape = x.shape
|
||||
x = mx.unflatten(x, axis=-1, shape=(self.groups, -1))
|
||||
x = mx.fast.rms_norm(x, weight=None, eps=self.eps)
|
||||
return self.weight * mx.flatten(x, -2)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.intermediate_size = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
self.intermediate_size, args.hidden_size, bias=args.use_bias
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim or args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
|
||||
q_size = self.num_attention_heads * self.head_dim
|
||||
kv_size = self.num_key_value_heads * self.head_dim
|
||||
q, k, v = mx.split(qkv, [q_size, q_size + kv_size], axis=-1)
|
||||
|
||||
queries = q.reshape(B, L, self.num_attention_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = k.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = v.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
class LinearAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_attention_heads
|
||||
self.head_dim = args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.num_key_value_groups = self.num_attention_heads // self.num_key_value_heads
|
||||
assert self.num_key_value_groups == 1, "Grouped linear not yet supported."
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
self.g_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.g_norm = GroupRMSNorm(
|
||||
args.num_attention_heads * self.head_dim,
|
||||
eps=args.rms_norm_eps,
|
||||
groups=args.group_norm_size,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
self._slope = self._get_slopes()
|
||||
|
||||
def _get_slopes(self) -> mx.array:
|
||||
n = self.num_attention_heads
|
||||
|
||||
def power_of_2_slopes(n):
|
||||
return [2 ** (-(2 ** -(math.log2(n) - 3)) * (i + 1)) for i in range(n)]
|
||||
|
||||
if math.log2(n).is_integer():
|
||||
slopes = power_of_2_slopes(n)
|
||||
else:
|
||||
p = 2 ** math.floor(math.log2(n))
|
||||
slopes = power_of_2_slopes(p) + power_of_2_slopes(2 * p)[::2][: n - p]
|
||||
|
||||
slopes = mx.array(slopes, dtype=mx.float32)
|
||||
denom = max(1, self.num_hidden_layers - 1)
|
||||
layer_pos = max(0, self.layer_idx - 1)
|
||||
layer_factor = 1 - (layer_pos / denom) + 1e-5
|
||||
return -slopes * layer_factor
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
qkv_mix = qkv.reshape(
|
||||
B,
|
||||
L,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads),
|
||||
self.head_dim,
|
||||
)
|
||||
q, k, v = mx.split(
|
||||
qkv_mix,
|
||||
[
|
||||
self.num_attention_heads,
|
||||
self.num_attention_heads + self.num_key_value_heads,
|
||||
],
|
||||
axis=2,
|
||||
)
|
||||
|
||||
queries = q.transpose(0, 2, 1, 3)
|
||||
keys = k.transpose(0, 2, 1, 3)
|
||||
values = v.transpose(0, 2, 1, 3)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
if cache is None:
|
||||
cache = [None]
|
||||
output, cache[0] = recurrent_gla(
|
||||
q=queries,
|
||||
k=keys,
|
||||
v=values,
|
||||
g=self._slope,
|
||||
scale=self.scale,
|
||||
h=cache[0],
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
output = self.g_norm(output) * mx.sigmoid(self.g_proj(x))
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
def group_expert_select(
|
||||
gates: mx.array,
|
||||
e_score_correction_bias: mx.array,
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
norm_topk_prob: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
in_type = gates.dtype
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
orig_scores = scores
|
||||
if e_score_correction_bias is not None:
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores.astype(in_type)
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.enable_routed_scaling = args.moe_router_enable_routed_scaling
|
||||
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.expert_bias = (
|
||||
mx.zeros((args.num_experts,))
|
||||
if args.moe_router_enable_expert_bias
|
||||
else None
|
||||
)
|
||||
self.score_function = args.score_function
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return group_expert_select(
|
||||
self.gate_proj(x),
|
||||
self.expert_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
self.score_function,
|
||||
)
|
||||
|
||||
|
||||
class SparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
self.gate = Gate(args)
|
||||
shared_dim = (
|
||||
args.moe_shared_expert_intermediate_size or args.moe_intermediate_size
|
||||
)
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args=args,
|
||||
intermediate_size=shared_dim * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts > 0 and args.moe_router_enable_shared_expert
|
||||
else None
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = (out * topk_weight[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_global = (
|
||||
(layer_idx + 1) % args.layer_group_size == 0
|
||||
or layer_idx
|
||||
>= args.num_hidden_layers // args.layer_group_size * args.layer_group_size
|
||||
)
|
||||
|
||||
if self.is_global:
|
||||
self.attention = Attention(args)
|
||||
else:
|
||||
self.attention = LinearAttention(args, layer_idx=layer_idx)
|
||||
|
||||
self.mlp = (
|
||||
SparseMoeBlock(args)
|
||||
if (
|
||||
args.num_experts is not None and layer_idx >= args.first_k_dense_replace
|
||||
)
|
||||
else MLP(args)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
if self.is_global:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache, offset=offset)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gla_idx = 0
|
||||
self.attn_idx = args.layer_group_size - 1
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.word_embeddings(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
offset = 0
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
gla_mask = create_ssm_mask(h, cache[self.gla_idx])
|
||||
if cache[self.attn_idx] is not None:
|
||||
offset = cache[self.attn_idx].offset
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_global else gla_mask
|
||||
h = layer(h, mask, c, offset=offset)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.norm_head = args.norm_head
|
||||
self.model_type = args.model_type
|
||||
self.model = LanguageModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.word_embeddings.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if self.norm_head:
|
||||
w = weights["lm_head.weight"]
|
||||
dtype = w.dtype
|
||||
weight_norm = (
|
||||
mx.linalg.norm(w.astype(mx.float32), axis=0, keepdims=True) + 1e-7
|
||||
)
|
||||
weights["lm_head.weight"] = (w / weight_norm).astype(dtype)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Handle MoE layers
|
||||
if l >= self.args.first_k_dense_replace:
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(
|
||||
to_join
|
||||
)
|
||||
|
||||
if f"{prefix}.mlp.gate.weight" in weights:
|
||||
gate_weight = weights.pop(f"{prefix}.mlp.gate.weight")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.weight"] = gate_weight
|
||||
|
||||
if f"{prefix}.mlp.gate.bias" in weights:
|
||||
gate_bias = weights.pop(f"{prefix}.mlp.gate.bias")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.bias"] = gate_bias
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate.gate_proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for l in self.layers:
|
||||
if l.is_global:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(ArraysCache(size=1))
|
||||
return caches
|
||||
+32
-28
@@ -7,8 +7,6 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
from mlx.utils import tree_map
|
||||
|
||||
from .cache import QuantizedKVCache
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseModelArgs:
|
||||
@@ -27,7 +25,8 @@ def create_causal_mask(
|
||||
N: int,
|
||||
offset: int = 0,
|
||||
window_size: Optional[int] = None,
|
||||
lengths: Optional[mx.array] = None,
|
||||
right_padding: Optional[mx.array] = None,
|
||||
left_padding: Optional[mx.array] = None,
|
||||
):
|
||||
rinds = mx.arange(offset + N)
|
||||
linds = mx.arange(offset, offset + N) if offset else rinds
|
||||
@@ -35,34 +34,31 @@ def create_causal_mask(
|
||||
rinds = rinds[None]
|
||||
mask = linds >= rinds
|
||||
if window_size is not None:
|
||||
mask = mask & (linds <= rinds + window_size)
|
||||
if lengths is not None:
|
||||
lengths = lengths[:, None, None, None]
|
||||
mask = mask & (rinds < lengths)
|
||||
mask = mask & (linds < rinds + window_size)
|
||||
if right_padding is not None:
|
||||
mask = mask & (rinds < mx.expand_dims((offset + N) - right_padding, (1, 2, 3)))
|
||||
if left_padding is not None:
|
||||
mask = mask & (mx.expand_dims(left_padding, (1, 2, 3)) <= rinds)
|
||||
return mask
|
||||
|
||||
|
||||
def create_attention_mask(
|
||||
h: mx.array, cache: Optional[Any] = None, return_array: bool = False
|
||||
h, cache=None, window_size: Optional[int] = None, return_array: bool = False
|
||||
):
|
||||
T = h.shape[1]
|
||||
if T > 1:
|
||||
offset = 0
|
||||
window_size = None
|
||||
if cache is not None and cache[0] is not None:
|
||||
c = cache[0]
|
||||
offset = c.offset
|
||||
if hasattr(c, "max_size"):
|
||||
window_size = c.max_size
|
||||
offset = min(window_size, offset)
|
||||
return_array = return_array or offset + T > window_size
|
||||
if return_array:
|
||||
return create_causal_mask(T, offset, window_size=window_size)
|
||||
else:
|
||||
return "causal"
|
||||
else:
|
||||
mask = None
|
||||
return mask
|
||||
N = h.shape[1]
|
||||
if cache and hasattr(cache, "make_mask"):
|
||||
return cache.make_mask(N, return_array=return_array, window_size=window_size)
|
||||
if N == 1:
|
||||
return None
|
||||
if return_array or (window_size and N > window_size):
|
||||
return create_causal_mask(N, window_size=window_size)
|
||||
return "causal"
|
||||
|
||||
|
||||
def create_ssm_mask(h, cache=None):
|
||||
if cache and hasattr(cache, "make_mask"):
|
||||
return cache.make_mask(h.shape[1])
|
||||
return None
|
||||
|
||||
|
||||
def quantized_scaled_dot_product_attention(
|
||||
@@ -116,8 +112,11 @@ def scaled_dot_product_attention(
|
||||
cache,
|
||||
scale: float,
|
||||
mask: Optional[mx.array],
|
||||
sinks: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
if isinstance(cache, QuantizedKVCache):
|
||||
if hasattr(cache, "bits"):
|
||||
if sinks is not None:
|
||||
raise ValueError("Quantized SDPA does not support attention sinks.")
|
||||
return quantized_scaled_dot_product_attention(
|
||||
queries,
|
||||
keys,
|
||||
@@ -129,5 +128,10 @@ def scaled_dot_product_attention(
|
||||
)
|
||||
else:
|
||||
return mx.fast.scaled_dot_product_attention(
|
||||
queries, keys, values, scale=scale, mask=mask
|
||||
queries,
|
||||
keys,
|
||||
values,
|
||||
scale=scale,
|
||||
mask=mask,
|
||||
sinks=sinks,
|
||||
)
|
||||
|
||||
+4
-12
@@ -93,11 +93,6 @@ class Attention(nn.Module):
|
||||
return output
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def relu2(x):
|
||||
return mx.square(nn.relu(x))
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -116,7 +111,7 @@ class MLP(nn.Module):
|
||||
self.ffn_sub_norm = nn.RMSNorm(args.intermediate_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
x = relu2(self.gate_proj(x)) * self.up_proj(x)
|
||||
x = nn.relu2(self.gate_proj(x)) * self.up_proj(x)
|
||||
x = self.ffn_sub_norm(x)
|
||||
x = self.down_proj(x)
|
||||
return x
|
||||
@@ -163,17 +158,15 @@ class LlamaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -192,10 +185,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
+669
-21
@@ -1,11 +1,14 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
import copy
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_map, tree_unflatten
|
||||
|
||||
from .base import create_causal_mask
|
||||
|
||||
|
||||
def make_prompt_cache(
|
||||
model: nn.Module,
|
||||
@@ -71,10 +74,10 @@ def load_prompt_cache(file_name, return_metadata=False):
|
||||
arrays = tree_unflatten(list(arrays.items()))
|
||||
cache_metadata = tree_unflatten(list(cache_metadata.items()))
|
||||
info, metadata, classes = cache_metadata
|
||||
cache = [globals()[c]() for c in classes]
|
||||
for c, state, meta_state in zip(cache, arrays, info):
|
||||
c.state = state
|
||||
c.meta_state = meta_state
|
||||
cache = [
|
||||
globals()[c].from_state(state, meta_state)
|
||||
for c, state, meta_state in zip(classes, arrays, info)
|
||||
]
|
||||
if return_metadata:
|
||||
return cache, metadata
|
||||
return cache
|
||||
@@ -106,6 +109,21 @@ def trim_prompt_cache(cache: List[Any], num_tokens: int) -> List[Any]:
|
||||
return [c.trim(num_tokens) for c in cache][0]
|
||||
|
||||
|
||||
def cache_length(cache: List[Any]):
|
||||
return max(len(c) for c in cache)
|
||||
|
||||
|
||||
def create_attention_mask(
|
||||
N: int, offset: int, return_array: bool, window_size: Optional[int]
|
||||
):
|
||||
if N == 1:
|
||||
return None
|
||||
if return_array:
|
||||
return create_causal_mask(N, offset, window_size=window_size)
|
||||
else:
|
||||
return "causal"
|
||||
|
||||
|
||||
class _BaseCache:
|
||||
@property
|
||||
def state(self):
|
||||
@@ -128,6 +146,32 @@ class _BaseCache:
|
||||
def is_trimmable(self):
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
"""The length of a cache is meant to represent the number of elements
|
||||
that we need to process in the attention. For instance for KVCache it
|
||||
is the size of the state, for RotatingKVCache it would be up to
|
||||
max_size etc."""
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
"""When an object defines __len__ then python defines the bool operator
|
||||
as len(obj) != 0. This, for instance, doesn't allow us to write
|
||||
|
||||
cache = cache or make_cache()
|
||||
|
||||
which is why we are overriding that behaviour with a constant bool
|
||||
operator return True.
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state, meta_state):
|
||||
# Create an instance of cls without calling __init__
|
||||
obj = cls.__new__(cls)
|
||||
obj.state = state
|
||||
obj.meta_state = meta_state
|
||||
return obj
|
||||
|
||||
|
||||
class ConcatenateKVCache(_BaseCache):
|
||||
"""ConcatenateKVCache the simplest KV cache implementation.
|
||||
@@ -162,13 +206,25 @@ class ConcatenateKVCache(_BaseCache):
|
||||
self.keys, self.values = v
|
||||
self.offset = self.keys.shape[-2]
|
||||
|
||||
def is_trimmable(self):
|
||||
return True
|
||||
|
||||
def trim(self, n):
|
||||
n = min(self.offset, n)
|
||||
self.offset -= n
|
||||
return n
|
||||
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
|
||||
class QuantizedKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, group_size: int = 64, bits: int = 8):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.step = 256
|
||||
self.group_size = group_size
|
||||
self.bits = bits
|
||||
|
||||
@@ -230,11 +286,11 @@ class QuantizedKVCache(_BaseCache):
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(map(str, (self.step, self.offset, self.group_size, self.bits)))
|
||||
return tuple(map(str, (self.offset, self.group_size, self.bits)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.step, self.offset, self.group_size, self.bits = map(int, v)
|
||||
self.offset, self.group_size, self.bits = map(int, v)
|
||||
|
||||
def is_trimmable(self):
|
||||
return True
|
||||
@@ -244,13 +300,17 @@ class QuantizedKVCache(_BaseCache):
|
||||
self.offset -= n
|
||||
return n
|
||||
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
|
||||
class KVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.step = 256
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
prev = self.offset
|
||||
@@ -276,6 +336,9 @@ class KVCache(_BaseCache):
|
||||
self.values[..., prev : self.offset, :] = values
|
||||
return self.keys[..., : self.offset, :], self.values[..., : self.offset, :]
|
||||
|
||||
def __len__(self):
|
||||
return self.offset
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.offset == self.keys.shape[2]:
|
||||
@@ -309,16 +372,19 @@ class KVCache(_BaseCache):
|
||||
)
|
||||
return quant_cache
|
||||
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
|
||||
class RotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, max_size=None, keep=0, step=256):
|
||||
def __init__(self, max_size, keep=0):
|
||||
self.keep = keep
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.max_size = max_size
|
||||
self.step = step
|
||||
self._idx = 0
|
||||
|
||||
def _trim(self, trim_size, v, append=None):
|
||||
@@ -358,10 +424,11 @@ class RotatingKVCache(_BaseCache):
|
||||
# preserve context
|
||||
self.keys = self._temporal_order(self.keys)
|
||||
self.values = self._temporal_order(self.values)
|
||||
self._idx = self.keys.shape[2]
|
||||
|
||||
# The largest size is self.max_size + S to ensure
|
||||
# The largest size is self.max_size + S - 1 to ensure
|
||||
# every token gets at least self.max_size context
|
||||
trim_size = self._idx - self.max_size
|
||||
trim_size = self._idx - self.max_size + 1
|
||||
self.keys = self._trim(trim_size, self.keys, keys)
|
||||
self.values = self._trim(trim_size, self.values, values)
|
||||
self.offset += keys.shape[2]
|
||||
@@ -416,6 +483,9 @@ class RotatingKVCache(_BaseCache):
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
return min(self.offset, self.max_size)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.offset < self.keys.shape[2]:
|
||||
@@ -429,13 +499,11 @@ class RotatingKVCache(_BaseCache):
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(
|
||||
map(str, (self.keep, self.max_size, self.step, self.offset, self._idx))
|
||||
)
|
||||
return tuple(map(str, (self.keep, self.max_size, self.offset, self._idx)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.keep, self.max_size, self.step, self.offset, self._idx = map(
|
||||
self.keep, self.max_size, self.offset, self._idx = map(
|
||||
int,
|
||||
v,
|
||||
)
|
||||
@@ -452,10 +520,37 @@ class RotatingKVCache(_BaseCache):
|
||||
def to_quantized(self, group_size: int = 64, bits: int = 4) -> QuantizedKVCache:
|
||||
raise NotImplementedError("RotatingKVCache Quantization NYI")
|
||||
|
||||
def make_mask(
|
||||
self, N: int, window_size: Optional[int] = None, return_array: bool = False
|
||||
):
|
||||
if N > 1:
|
||||
window_size = window_size or self.max_size
|
||||
offset = min(self.max_size - 1, self.offset)
|
||||
if offset + N > window_size or return_array:
|
||||
return create_causal_mask(N, offset, window_size=window_size)
|
||||
else:
|
||||
return "causal"
|
||||
else:
|
||||
if window_size is None:
|
||||
return None
|
||||
# May need a mask for when window_size < max_size
|
||||
if self.offset >= window_size and self.max_size > window_size:
|
||||
idx = self._idx
|
||||
if idx >= self.max_size:
|
||||
idx = 0
|
||||
if self.offset < self.max_size:
|
||||
mask_size = self.offset + 1
|
||||
else:
|
||||
mask_size = self.max_size
|
||||
mask = mx.arange(mask_size) >= (mask_size - window_size)
|
||||
mask = mx.roll(mask, shift=idx + 1)
|
||||
return mask
|
||||
|
||||
|
||||
class ArraysCache(_BaseCache):
|
||||
def __init__(self, size):
|
||||
def __init__(self, size, left_padding: Optional[List[int]] = None):
|
||||
self.cache = [None] * size
|
||||
self.left_padding = mx.array(left_padding) if left_padding else None
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
self.cache[idx] = value
|
||||
@@ -471,14 +566,34 @@ class ArraysCache(_BaseCache):
|
||||
def state(self, v):
|
||||
self.cache = v
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
self.cache = [c[batch_indices] for c in self.cache]
|
||||
self.left_padding = None
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
self.cache = [mx.concatenate([c, o]) for c, o in zip(self.cache, other.cache)]
|
||||
self.left_padding = None
|
||||
|
||||
def make_mask(self, N: int):
|
||||
if self.cache[0] is None and self.left_padding is not None:
|
||||
return mx.arange(N) >= self.left_padding[:, None]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class MambaCache(ArraysCache):
|
||||
def __init__(self):
|
||||
super().__init__(size=2)
|
||||
def __init__(self, left_padding: Optional[List[int]] = None):
|
||||
super().__init__(size=2, left_padding=left_padding)
|
||||
|
||||
|
||||
class ChunkedKVCache(KVCache):
|
||||
def __init__(self, chunk_size=None):
|
||||
def __init__(self, chunk_size):
|
||||
super().__init__()
|
||||
self.chunk_size = chunk_size
|
||||
self.start_position = 0
|
||||
@@ -529,13 +644,21 @@ class ChunkedKVCache(KVCache):
|
||||
self.chunk_size, self.start_position = map(int, v)
|
||||
|
||||
|
||||
class CacheList(KVCache):
|
||||
class CacheList(_BaseCache):
|
||||
def __init__(self, *caches):
|
||||
self.caches = caches
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.caches[idx]
|
||||
|
||||
def is_trimmable(self):
|
||||
return all(c.is_trimmable() for c in self.caches)
|
||||
|
||||
def trim(self, n):
|
||||
for c in self.caches:
|
||||
m = c.trim(n)
|
||||
return m
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return [s for c in self.caches for s in c.state]
|
||||
@@ -548,3 +671,528 @@ class CacheList(KVCache):
|
||||
l = len(c.state)
|
||||
c.state = v[start : start + l]
|
||||
start += l
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
for c in self.caches:
|
||||
c.filter(batch_indices)
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
for c, o in zip(self.caches, other.caches):
|
||||
c.extend(o)
|
||||
|
||||
|
||||
def dynamic_roll(x, shifts, axis):
|
||||
n = x.shape[axis]
|
||||
expand_shifts = (...,) + (None,) * (x.ndim - axis)
|
||||
expand_indices = expand_shifts[:-1]
|
||||
idx = (mx.arange(n)[expand_indices] - shifts[expand_shifts]) % n
|
||||
rolled = mx.take_along_axis(x, idx, axis=axis)
|
||||
return rolled
|
||||
|
||||
|
||||
class BatchKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, left_padding: List[int]):
|
||||
"""
|
||||
The BatchKV cache expects inputs to be left-padded.
|
||||
|
||||
E.g. the following prompts:
|
||||
|
||||
[1, 3, 5]
|
||||
[7]
|
||||
[2, 6, 8, 9]
|
||||
|
||||
Should be padded like so:
|
||||
|
||||
[0, 1, 3, 5]
|
||||
[0, 0, 0, 7]
|
||||
[2, 6, 8, 9]
|
||||
|
||||
And ``left_padding`` specifies the amount of padding for each.
|
||||
In this case, ``left_padding = [1, 3, 0]``.
|
||||
"""
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.left_padding = mx.array(left_padding)
|
||||
self.offset = mx.array([-l for l in left_padding])
|
||||
self._idx = 0
|
||||
|
||||
self._right_padding = None
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
prev = self._idx
|
||||
if self.keys is None or (prev + keys.shape[2]) > self.keys.shape[2]:
|
||||
B, n_kv_heads, _, k_head_dim = keys.shape
|
||||
v_head_dim = values.shape[3]
|
||||
n_steps = (self.step + keys.shape[2] - 1) // self.step
|
||||
k_shape = (B, n_kv_heads, n_steps * self.step, k_head_dim)
|
||||
v_shape = (B, n_kv_heads, n_steps * self.step, v_head_dim)
|
||||
new_k = mx.zeros(k_shape, keys.dtype)
|
||||
new_v = mx.zeros(v_shape, values.dtype)
|
||||
if self.keys is not None:
|
||||
if prev % self.step != 0:
|
||||
self.keys = self.keys[..., :prev, :]
|
||||
self.values = self.values[..., :prev, :]
|
||||
self.keys = mx.concatenate([self.keys, new_k], axis=2)
|
||||
self.values = mx.concatenate([self.values, new_v], axis=2)
|
||||
else:
|
||||
self.keys, self.values = new_k, new_v
|
||||
|
||||
self.offset += keys.shape[2]
|
||||
self._idx += keys.shape[2]
|
||||
self.keys[..., prev : self._idx, :] = keys
|
||||
self.values[..., prev : self._idx, :] = values
|
||||
return self.keys[..., : self._idx, :], self.values[..., : self._idx, :]
|
||||
|
||||
def __len__(self):
|
||||
return self._idx
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
raise ValueError(
|
||||
"Left padding can only be added to an empty BatchKVCache"
|
||||
)
|
||||
left_padding = mx.array(left_padding)
|
||||
self.left_padding += left_padding
|
||||
self.offset -= left_padding
|
||||
|
||||
if right_padding is not None and max(right_padding) > 0:
|
||||
self._right_padding = mx.array(right_padding)
|
||||
|
||||
def finalize(self):
|
||||
if self._right_padding is not None:
|
||||
padding = self._right_padding
|
||||
self.keys = dynamic_roll(self.keys, padding[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, padding[:, None], axis=2)
|
||||
self.offset -= padding
|
||||
self.left_padding += padding
|
||||
self._right_padding = None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
k, v = self.keys, self.values
|
||||
if self._idx < k.shape[2]:
|
||||
k = k[..., : self._idx, :]
|
||||
v = v[..., : self._idx, :]
|
||||
return k, v, self.offset, self.left_padding
|
||||
|
||||
@state.setter
|
||||
def state(self, v):
|
||||
self.keys, self.values, self.offset, self.left_padding = v
|
||||
self._idx = self.keys.shape[2]
|
||||
|
||||
def is_trimmable(self):
|
||||
return True
|
||||
|
||||
def trim(self, n):
|
||||
n = min(self._idx, n)
|
||||
self._idx -= n
|
||||
self.offset -= n
|
||||
return n
|
||||
|
||||
def make_mask(self, N: int, return_array: bool = False, **kwargs):
|
||||
return create_causal_mask(
|
||||
N, offset=self._idx, left_padding=self.left_padding, **kwargs
|
||||
)
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
self.keys = self.keys[batch_indices]
|
||||
self.values = self.values[batch_indices]
|
||||
self.offset = self.offset[batch_indices]
|
||||
self.left_padding = self.left_padding[batch_indices]
|
||||
|
||||
# Shift left to reduce padding
|
||||
min_left_pad = self.left_padding.min().item()
|
||||
if min_left_pad > 0:
|
||||
self.keys = self.keys[..., min_left_pad:, :]
|
||||
self.values = self.values[..., min_left_pad:, :]
|
||||
self._idx -= min_left_pad
|
||||
self.left_padding -= min_left_pad
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
max_idx = max(self._idx, other._idx)
|
||||
max_size = max(self.keys.shape[2], other.keys.shape[2])
|
||||
|
||||
# Pad the keys and values so they are right-justified
|
||||
# with the index and the same size
|
||||
def pad(c):
|
||||
left = max_idx - c._idx
|
||||
right = max_size - c.keys.shape[2] - left
|
||||
k, v = c.keys, c.values
|
||||
if right < 0:
|
||||
k = k[..., :right, :]
|
||||
v = v[..., :right, :]
|
||||
right = 0
|
||||
if left != 0 or right != 0:
|
||||
pad = [(0, 0), (0, 0), (left, right), (0, 0)]
|
||||
k = mx.pad(k, pad)
|
||||
v = mx.pad(v, pad)
|
||||
left_padding = c.left_padding + left
|
||||
return k, v, c.offset, left_padding
|
||||
|
||||
self.keys, self.values, self.offset, self.left_padding = map(
|
||||
mx.concatenate, zip(*(pad(self), pad(other)))
|
||||
)
|
||||
self._idx = max_idx
|
||||
|
||||
def extract(self, idx):
|
||||
cache = KVCache()
|
||||
padding = self.left_padding[idx].item()
|
||||
cache.keys = mx.contiguous(self.keys[idx : idx + 1, :, padding : self._idx])
|
||||
cache.values = mx.contiguous(self.values[idx : idx + 1, :, padding : self._idx])
|
||||
cache.offset = cache.keys.shape[2]
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
lengths = [len(c) for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
H = max(c.keys.shape[1] for c in caches if c.keys is not None)
|
||||
Dk = max(c.keys.shape[3] for c in caches if c.keys is not None)
|
||||
Dv = max(c.values.shape[3] for c in caches if c.values is not None)
|
||||
dt = next(iter(c.keys.dtype for c in caches if c.keys is not None))
|
||||
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
keys[i : i + 1, :, p : p + c.offset] = c.keys[..., : c.offset, :]
|
||||
values[i : i + 1, :, p : p + c.offset] = c.values[..., : c.offset, :]
|
||||
|
||||
cache = cls(padding)
|
||||
cache.keys = keys
|
||||
cache.values = values
|
||||
cache.offset += keys.shape[2]
|
||||
cache._idx = keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
class BatchRotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, max_size, left_padding: List[int]):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
|
||||
self.left_padding = mx.array(left_padding)
|
||||
self.offset = mx.array([-l for l in left_padding])
|
||||
|
||||
self.max_size = max_size
|
||||
self._idx = 0
|
||||
self._offset = 0
|
||||
self.rotated = False
|
||||
|
||||
# Lengths for right_padded inputs to make sure that padding tokens do
|
||||
# not evict valid tokens.
|
||||
self._lengths = None
|
||||
|
||||
def _trim(self, trim_size, v, append=None):
|
||||
if trim_size > 0:
|
||||
v = v[..., trim_size:, :]
|
||||
if append is not None:
|
||||
return mx.concatenate([v, append], axis=2)
|
||||
return v
|
||||
|
||||
def _temporal_order(self):
|
||||
"""
|
||||
Rearrange the cache into temporal order.
|
||||
"""
|
||||
if self.rotated:
|
||||
self.keys = mx.roll(self.keys, -self._idx, axis=2)
|
||||
self.values = mx.roll(self.values, -self._idx, axis=2)
|
||||
self._idx = self.keys.shape[2]
|
||||
self.rotated = False
|
||||
|
||||
def _update_concat(self, keys, values):
|
||||
if self.keys is None:
|
||||
self.keys = keys
|
||||
self.values = values
|
||||
else:
|
||||
# Put the keys/values in temporal order to
|
||||
# preserve context
|
||||
self._temporal_order()
|
||||
|
||||
# Slice off the end if needed
|
||||
if self.keys.shape[2] > self._idx:
|
||||
self.keys = self.keys[..., : self._idx, :]
|
||||
self.values = self.values[..., : self._idx, :]
|
||||
|
||||
# Roll right sequences that are padded to make sure that we don't
|
||||
# trim valid cache entries
|
||||
if self._lengths is not None:
|
||||
roll = mx.maximum(0, self.offset - self._lengths)
|
||||
self.keys = dynamic_roll(self.keys, roll[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, roll[:, None], axis=2)
|
||||
self.left_padding += roll
|
||||
self.offset -= roll
|
||||
|
||||
# The largest size is self.max_size + S - 1 to ensure
|
||||
# every token gets at least self.max_size context
|
||||
trim_size = self._idx - self.max_size + 1
|
||||
if trim_size > 0:
|
||||
self.left_padding -= trim_size
|
||||
self.keys = self._trim(trim_size, self.keys, keys)
|
||||
self.values = self._trim(trim_size, self.values, values)
|
||||
self.offset += keys.shape[2]
|
||||
self._offset += keys.shape[2]
|
||||
self._idx = self.keys.shape[2]
|
||||
return self.keys, self.values
|
||||
|
||||
def _update_in_place(self, keys, values):
|
||||
if self._lengths is not None:
|
||||
raise RuntimeError(
|
||||
"finalize() should be called before deocoding with BatchRotatingKVCache"
|
||||
)
|
||||
|
||||
# May not have hit the max size yet, so potentially
|
||||
# keep growing the cache
|
||||
B, n_kv_heads, S, k_head_dim = keys.shape
|
||||
prev = self._offset
|
||||
if self.keys is None or (
|
||||
prev >= self.keys.shape[2] and self.keys.shape[2] < self.max_size
|
||||
):
|
||||
v_head_dim = values.shape[3]
|
||||
new_size = min(self.step, self.max_size - prev)
|
||||
k_shape = (B, n_kv_heads, new_size, k_head_dim)
|
||||
v_shape = (B, n_kv_heads, new_size, v_head_dim)
|
||||
new_k = mx.zeros(k_shape, keys.dtype)
|
||||
new_v = mx.zeros(v_shape, values.dtype)
|
||||
if self.keys is not None:
|
||||
self.keys = mx.concatenate([self.keys, new_k], axis=2)
|
||||
self.values = mx.concatenate([self.values, new_v], axis=2)
|
||||
else:
|
||||
self.keys, self.values = new_k, new_v
|
||||
self._idx = prev
|
||||
|
||||
# Trim if needed
|
||||
trim_size = self.keys.shape[2] - self.max_size
|
||||
if trim_size > 0:
|
||||
self.keys = self._trim(trim_size, self.keys)
|
||||
self.values = self._trim(trim_size, self.values)
|
||||
self._idx = self.max_size
|
||||
self.left_padding -= trim_size
|
||||
|
||||
# Rotate
|
||||
if self._idx == self.max_size:
|
||||
self.rotated = True
|
||||
self._idx = 0
|
||||
if self.rotated:
|
||||
self.left_padding -= S
|
||||
|
||||
# Assign
|
||||
self.keys[..., self._idx : self._idx + S, :] = keys
|
||||
self.values[..., self._idx : self._idx + S, :] = values
|
||||
self._offset += S
|
||||
self.offset += S
|
||||
self._idx += S
|
||||
|
||||
# If the buffer is not full, slice off the end
|
||||
if self._offset < self.max_size:
|
||||
return (
|
||||
self.keys[..., : self._offset, :],
|
||||
self.values[..., : self._offset, :],
|
||||
)
|
||||
return self.keys, self.values
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
if keys.shape[2] == 1:
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
return min(self._offset, self.max_size)
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
raise ValueError(
|
||||
"Left padding can only be added to an empty BatchRotatingKVCache"
|
||||
)
|
||||
left_padding = mx.array(left_padding)
|
||||
self.left_padding += left_padding
|
||||
self.offset -= left_padding
|
||||
|
||||
if right_padding is not None and max(right_padding) > 0:
|
||||
self._lengths = mx.array(lengths) + self.offset
|
||||
|
||||
def finalize(self):
|
||||
if self._lengths is not None:
|
||||
roll = mx.maximum(0, self.offset - self._lengths)
|
||||
self.keys = dynamic_roll(self.keys, roll[:, None], axis=2)
|
||||
self.values = dynamic_roll(self.values, roll[:, None], axis=2)
|
||||
self.left_padding += roll
|
||||
self.offset -= roll
|
||||
self._lengths = None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
k, v = self.keys, self.values
|
||||
if self._offset < k.shape[2]:
|
||||
k, v = k[..., : self._offset, :], v[..., : self._offset, :]
|
||||
return k, v, self.offset, self.left_padding
|
||||
|
||||
@state.setter
|
||||
def state(self, v):
|
||||
self.keys, self.values, self.offset, self.left_padding = v
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(map(str, (self.max_size, self._offset, self._idx, self.rotated)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.max_size, self._offset, self._idx = map(
|
||||
int,
|
||||
v[:3],
|
||||
)
|
||||
self.rotated = bool(v[3])
|
||||
|
||||
def is_trimmable(self):
|
||||
return self._offset < self.max_size
|
||||
|
||||
def trim(self, n):
|
||||
n = min(self._offset, n)
|
||||
self._offset -= n
|
||||
self._idx -= n
|
||||
self.offset -= n
|
||||
return n
|
||||
|
||||
def to_quantized(self, group_size: int = 64, bits: int = 4) -> QuantizedKVCache:
|
||||
raise NotImplementedError("BatchRotatingKVCache Quantization NYI")
|
||||
|
||||
def make_mask(
|
||||
self, N: int, window_size: Optional[int] = None, return_array: bool = False
|
||||
):
|
||||
left_padding = self.left_padding
|
||||
window_size = window_size or self.max_size
|
||||
offset = min(self.max_size - 1, self._offset)
|
||||
rinds = mx.arange(offset + N)
|
||||
linds = mx.arange(offset, offset + N) if offset else rinds
|
||||
linds = linds[:, None]
|
||||
rinds = rinds[None]
|
||||
mask = linds >= rinds
|
||||
mask &= linds < rinds + window_size
|
||||
if (trim_size := self._idx - self.max_size + int(N > 1)) > 0:
|
||||
left_padding = left_padding - trim_size
|
||||
|
||||
rotated = N == 1 and (self.rotated or self._idx >= self.max_size)
|
||||
if rotated:
|
||||
left_padding = left_padding - 1
|
||||
|
||||
mask = mask & (rinds >= mx.expand_dims(left_padding, (1, 2, 3)))
|
||||
|
||||
if rotated:
|
||||
idx = self._idx
|
||||
if idx >= self.max_size:
|
||||
idx = 0
|
||||
mask = mx.roll(mask, shift=idx + 1, axis=-1)
|
||||
|
||||
return mask
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
self.keys = self.keys[batch_indices]
|
||||
self.values = self.values[batch_indices]
|
||||
self.offset = self.offset[batch_indices]
|
||||
self.left_padding = self.left_padding[batch_indices]
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
if (self.rotated != other.rotated) or self._idx != other._idx:
|
||||
self._temporal_order()
|
||||
other._temporal_order()
|
||||
|
||||
max_idx = max(self._idx, other._idx)
|
||||
max_size = max(self.keys.shape[2], other.keys.shape[2])
|
||||
|
||||
def pad(c):
|
||||
left = max_idx - c._idx
|
||||
right = max_size - c.keys.shape[2] - left
|
||||
k, v = c.keys, c.values
|
||||
if right < 0:
|
||||
k = k[..., :right, :]
|
||||
v = v[..., :right, :]
|
||||
right = 0
|
||||
if left != 0 or right != 0:
|
||||
pad = [(0, 0), (0, 0), (left, right), (0, 0)]
|
||||
k = mx.pad(k, pad)
|
||||
v = mx.pad(v, pad)
|
||||
left_padding = c.left_padding + left
|
||||
return k, v, c.offset, left_padding
|
||||
|
||||
self.keys, self.values, self.offset, self.left_padding = map(
|
||||
mx.concatenate, zip(*(pad(self), pad(other)))
|
||||
)
|
||||
self._idx = max_idx
|
||||
self._offset = max(self._offset, other._offset)
|
||||
|
||||
def extract(self, idx):
|
||||
cache = RotatingKVCache(self.max_size)
|
||||
padding = self.left_padding[idx].item()
|
||||
offset = self.offset[idx].item()
|
||||
cache.keys = self.keys[idx : idx + 1]
|
||||
cache.values = self.values[idx : idx + 1]
|
||||
cache._idx = self._idx
|
||||
if self.rotated:
|
||||
cache.keys = mx.roll(cache.keys, -self._idx, axis=2)
|
||||
cache.values = mx.roll(cache.values, -self._idx, axis=2)
|
||||
cache._idx = self.max_size
|
||||
if padding > 0:
|
||||
cache.keys = mx.contiguous(cache.keys[:, :, padding : cache._idx])
|
||||
cache.values = mx.contiguous(cache.values[:, :, padding : cache._idx])
|
||||
cache.offset = offset
|
||||
cache._idx = cache.keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
if not all(c.max_size == caches[0].max_size for c in caches):
|
||||
raise ValueError(
|
||||
"BatchRotatingKVCache can only merge caches with the same maximum size"
|
||||
)
|
||||
|
||||
offsets = [c.offset for c in caches]
|
||||
lengths = [len(c) for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
H = max(c.keys.shape[1] for c in caches if c.keys is not None)
|
||||
Dk = max(c.keys.shape[3] for c in caches if c.keys is not None)
|
||||
Dv = max(c.values.shape[3] for c in caches if c.values is not None)
|
||||
dt = next(iter(c.keys.dtype for c in caches if c.keys is not None))
|
||||
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
keys[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.keys)
|
||||
values[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.values)
|
||||
|
||||
cache = cls(caches[0].max_size, padding)
|
||||
cache.keys = keys
|
||||
cache.values = values
|
||||
cache.offset = mx.array(offsets)
|
||||
cache._idx = keys.shape[2]
|
||||
cache._offset = keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
@@ -155,17 +155,15 @@ class CohereModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -182,10 +180,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = out * self.model.args.logit_scale
|
||||
return out
|
||||
|
||||
@@ -83,11 +83,6 @@ class Attention(nn.Module):
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
if self.use_sliding_window and isinstance(mask, mx.array):
|
||||
key_len = keys.shape[-2]
|
||||
if mask.shape[-1] != key_len:
|
||||
mask = mask[..., -key_len:]
|
||||
|
||||
# TODO: maybe remove cast once fused mask is supported since attention
|
||||
# may be in higher precision
|
||||
sdpa_type = mx.float32 if queries.dtype == mx.float16 else queries.dtype
|
||||
@@ -148,6 +143,7 @@ class CohereModel(nn.Module):
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
assert self.vocab_size > 0
|
||||
self.window_size = args.sliding_window
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args, layer_idx=i)
|
||||
@@ -160,7 +156,6 @@ class CohereModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
@@ -168,10 +163,9 @@ class CohereModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
if mask is None:
|
||||
j = self.args.sliding_window_pattern
|
||||
full_mask = create_attention_mask(h, cache[j - 1 : j])
|
||||
sliding_window_mask = create_attention_mask(h, cache)
|
||||
j = self.args.sliding_window_pattern
|
||||
full_mask = create_attention_mask(h, cache[j - 1])
|
||||
swa_mask = create_attention_mask(h, cache[0], window_size=self.window_size)
|
||||
|
||||
for i, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
is_global = (
|
||||
@@ -179,13 +173,9 @@ class CohereModel(nn.Module):
|
||||
== self.args.sliding_window_pattern - 1
|
||||
)
|
||||
|
||||
local_mask = mask
|
||||
if mask is None and is_global:
|
||||
local_mask = full_mask
|
||||
elif mask is None:
|
||||
local_mask = sliding_window_mask
|
||||
mask = full_mask if is_global else swa_mask
|
||||
|
||||
h = layer(h, local_mask, c)
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -200,10 +190,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = out * self.model.args.logit_scale
|
||||
return out
|
||||
|
||||
@@ -196,17 +196,15 @@ class DBRX(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.wte(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.blocks)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.blocks, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -224,10 +222,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
|
||||
@@ -210,15 +210,14 @@ class DeepseekModel(nn.Module):
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -237,9 +236,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -8,6 +8,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -355,7 +356,7 @@ class DeepseekV2DecoderLayer(nn.Module):
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV2Model(nn.Module):
|
||||
class DeepseekV2Model(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -364,58 +365,34 @@ 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,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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]]
|
||||
@@ -435,9 +412,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
@@ -455,4 +431,4 @@ class Model(nn.Module):
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
return self.model.pipeline_layers
|
||||
|
||||
+50
-166
@@ -9,6 +9,8 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -33,9 +35,9 @@ class ModelArgs(BaseModelArgs):
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: Optional[int] = None
|
||||
topk_group: Optional[int] = None
|
||||
num_experts_per_tok: Optional[int] = None
|
||||
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
|
||||
@@ -45,99 +47,6 @@ class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool = False
|
||||
|
||||
|
||||
def yarn_find_correction_dim(
|
||||
num_rotations, dim, base=10000, max_position_embeddings=2048
|
||||
):
|
||||
return (dim * math.log(max_position_embeddings / (num_rotations * 2 * math.pi))) / (
|
||||
2 * math.log(base)
|
||||
)
|
||||
|
||||
|
||||
def yarn_find_correction_range(
|
||||
low_rot, high_rot, dim, base=10000, max_position_embeddings=2048
|
||||
):
|
||||
low = math.floor(
|
||||
yarn_find_correction_dim(low_rot, dim, base, max_position_embeddings)
|
||||
)
|
||||
high = math.ceil(
|
||||
yarn_find_correction_dim(high_rot, dim, base, max_position_embeddings)
|
||||
)
|
||||
return max(low, 0), min(high, dim - 1)
|
||||
|
||||
|
||||
def yarn_get_mscale(scale=1, mscale=1):
|
||||
if scale <= 1:
|
||||
return 1.0
|
||||
return 0.1 * mscale * math.log(scale) + 1.0
|
||||
|
||||
|
||||
def yarn_linear_ramp_mask(min_val, max_val, dim):
|
||||
if min_val == max_val:
|
||||
max_val += 0.001 # Prevent singularity
|
||||
|
||||
linear_func = (mx.arange(dim, dtype=mx.float32) - min_val) / (max_val - min_val)
|
||||
return mx.clip(linear_func, 0, 1)
|
||||
|
||||
|
||||
class DeepseekV3YarnRotaryEmbedding(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
max_position_embeddings=2048,
|
||||
base=10000,
|
||||
scaling_factor=1.0,
|
||||
original_max_position_embeddings=4096,
|
||||
beta_fast=32,
|
||||
beta_slow=1,
|
||||
mscale=1,
|
||||
mscale_all_dim=0,
|
||||
):
|
||||
super().__init__()
|
||||
self.mscale = yarn_get_mscale(scaling_factor, mscale) / yarn_get_mscale(
|
||||
scaling_factor, mscale_all_dim
|
||||
)
|
||||
freq_extra = base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)
|
||||
freq_inter = scaling_factor * freq_extra
|
||||
low, high = yarn_find_correction_range(
|
||||
beta_fast,
|
||||
beta_slow,
|
||||
dim,
|
||||
base,
|
||||
original_max_position_embeddings,
|
||||
)
|
||||
freq_mask = 1.0 - yarn_linear_ramp_mask(low, high, dim // 2)
|
||||
self._freqs = (freq_inter * freq_extra) / (
|
||||
freq_inter * freq_mask + freq_extra * (1 - freq_mask)
|
||||
)
|
||||
|
||||
def __call__(self, x, offset=0):
|
||||
if self.mscale != 1.0:
|
||||
x = self.mscale * x
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
x.shape[-1],
|
||||
traditional=True,
|
||||
base=None,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
freqs=self._freqs,
|
||||
)
|
||||
|
||||
|
||||
# A clipped silu to prevent fp16 from overflowing
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def clipped_silu(x):
|
||||
return mx.clip(x * mx.sigmoid(x), a_min=-100, a_max=100)
|
||||
|
||||
|
||||
class ClippedSilu(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, x):
|
||||
return clipped_silu(x)
|
||||
|
||||
|
||||
class DeepseekV3Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -189,35 +98,19 @@ class DeepseekV3Attention(nn.Module):
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if mscale_all_dim:
|
||||
mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
|
||||
self.scale = self.scale * mscale * mscale
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
rope_kwargs = {
|
||||
key: self.config.rope_scaling[key]
|
||||
for key in [
|
||||
"original_max_position_embeddings",
|
||||
"beta_fast",
|
||||
"beta_slow",
|
||||
"mscale",
|
||||
"mscale_all_dim",
|
||||
]
|
||||
if key in self.config.rope_scaling
|
||||
}
|
||||
self.rope = DeepseekV3YarnRotaryEmbedding(
|
||||
dim=self.qk_rope_head_dim,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_factor=scaling_factor,
|
||||
base=self.rope_theta,
|
||||
**rope_kwargs,
|
||||
)
|
||||
else:
|
||||
self.rope = nn.RoPE(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -295,18 +188,18 @@ def group_expert_select(
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
k = top_k
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
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)
|
||||
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]
|
||||
@@ -354,7 +247,6 @@ class DeepseekV3MoE(nn.Module):
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
activation=ClippedSilu(),
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
@@ -404,7 +296,7 @@ class DeepseekV3DecoderLayer(nn.Module):
|
||||
return h + r
|
||||
|
||||
|
||||
class DeepseekV3Model(nn.Module):
|
||||
class DeepseekV3Model(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -413,56 +305,34 @@ 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,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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]]
|
||||
@@ -482,9 +352,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
@@ -503,7 +372,22 @@ class Model(nn.Module):
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
# Remap for int4
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if k.endswith("weight_shape"):
|
||||
base = k.replace("weight_shape", "")
|
||||
new_weights[base + "weight"] = weights[base + "weight_packed"].view(
|
||||
mx.uint32
|
||||
)
|
||||
s = weights[base + "weight_scale"]
|
||||
new_weights[base + "scales"] = s
|
||||
new_weights[base + "biases"] = -8 * s
|
||||
elif not (k.endswith("weight_scale") or k.endswith("weight_packed")):
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Dequantize fp8
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
@@ -537,7 +421,7 @@ class Model(nn.Module):
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "deepseek_v32"
|
||||
vocab_size: int = 102400
|
||||
hidden_size: int = 4096
|
||||
index_head_dim: int = 128
|
||||
index_n_heads: int = 64
|
||||
index_topk: int = 2048
|
||||
intermediate_size: int = 11008
|
||||
moe_intermediate_size: int = 1407
|
||||
num_hidden_layers: int = 30
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 32
|
||||
n_shared_experts: Optional[int] = None
|
||||
n_routed_experts: Optional[int] = None
|
||||
routed_scaling_factor: float = 1.0
|
||||
kv_lora_rank: int = 512
|
||||
q_lora_rank: int = 1536
|
||||
qk_rope_head_dim: int = 64
|
||||
v_head_dim: int = 128
|
||||
qk_nope_head_dim: int = 128
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
num_experts_per_tok: int = 1
|
||||
moe_layer_freq: int = 1
|
||||
first_k_dense_replace: int = 0
|
||||
max_position_embeddings: int = 2048
|
||||
rms_norm_eps: float = 1e-6
|
||||
rope_theta: float = 10000.0
|
||||
rope_scaling: Dict = None
|
||||
attention_bias: bool = False
|
||||
|
||||
|
||||
class Indexer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.dim = args.hidden_size
|
||||
self.n_heads = args.index_n_heads
|
||||
self.head_dim = args.index_head_dim
|
||||
self.rope_head_dim = args.qk_rope_head_dim
|
||||
self.index_topk = args.index_topk
|
||||
self.q_lora_rank = args.q_lora_rank
|
||||
self.wq_b = nn.Linear(
|
||||
self.q_lora_rank, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.wk = nn.Linear(self.dim, self.head_dim, bias=False)
|
||||
self.k_norm = nn.LayerNorm(self.head_dim)
|
||||
self.weights_proj = nn.Linear(self.dim, self.n_heads, bias=False)
|
||||
self.softmax_scale = self.head_dim**-0.5
|
||||
self.rope = initialize_rope(
|
||||
dims=args.qk_rope_head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
qr: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
# Computes top_k indices for attention
|
||||
b, s, _ = x.shape
|
||||
q = self.wq_b(qr)
|
||||
q = q.reshape(b, s, self.n_heads, self.head_dim).swapaxes(1, 2)
|
||||
q_pe, q_nope = mx.split(q, [self.rope_head_dim], axis=-1)
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
|
||||
q_pe = self.rope(q_pe, offset=offset)
|
||||
q = mx.concatenate([q_pe, q_nope], axis=-1)
|
||||
|
||||
k = self.wk(x)
|
||||
k = self.k_norm(k)
|
||||
k = mx.reshape(k, (b, 1, s, self.head_dim))
|
||||
k_pe, k_nope = mx.split(k, [self.rope_head_dim], axis=-1)
|
||||
k_pe = self.rope(k_pe, offset=offset)
|
||||
k = mx.concatenate([k_pe, k_nope], axis=-1)
|
||||
if cache is not None:
|
||||
k, _ = cache.update_and_fetch(k, mx.zeros([b, 1, s, 0]))
|
||||
if k.shape[2] <= self.index_topk:
|
||||
return None
|
||||
scores = q @ k.swapaxes(-1, -2)
|
||||
scores = mx.maximum(scores, 0)
|
||||
weights = self.weights_proj(x) * (self.n_heads**-0.5 * self.softmax_scale)
|
||||
weights = weights.swapaxes(-1, -2)[..., None]
|
||||
scores = scores * weights
|
||||
scores = scores.sum(axis=1)
|
||||
if mask is not None:
|
||||
scores = mx.where(mask, scores, -float("inf"))
|
||||
return mx.argpartition(scores, kth=-self.index_topk, axis=-1)[
|
||||
..., -self.index_topk :
|
||||
]
|
||||
|
||||
|
||||
class DeepseekV32Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_heads = config.num_attention_heads
|
||||
self.max_position_embeddings = config.max_position_embeddings
|
||||
self.rope_theta = config.rope_theta
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.qk_rope_head_dim = config.qk_rope_head_dim
|
||||
self.kv_lora_rank = config.kv_lora_rank
|
||||
self.v_head_dim = config.v_head_dim
|
||||
self.qk_nope_head_dim = config.qk_nope_head_dim
|
||||
self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
|
||||
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=1e-6)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.indexer = Indexer(config)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qr = self.q_a_layernorm(self.q_a_proj(x))
|
||||
q = self.q_b_proj(qr)
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
|
||||
kv = kv.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if cache is not None:
|
||||
q_pe = self.rope(q_pe, cache[0].offset)
|
||||
k_pe = self.rope(k_pe, cache[0].offset)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys, values = cache[0].update_and_fetch(
|
||||
mx.concatenate([k_nope, k_pe], axis=-1), values
|
||||
)
|
||||
else:
|
||||
cache = [None] * 2
|
||||
q_pe = self.rope(q_pe)
|
||||
k_pe = self.rope(k_pe)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys = mx.concatenate([k_nope, k_pe], axis=-1)
|
||||
|
||||
queries = mx.concatenate([q_nope, q_pe], axis=-1)
|
||||
topk_indices = self.indexer(x, qr, mask, cache=cache[1])
|
||||
if topk_indices is not None:
|
||||
k_seq = keys.shape[2]
|
||||
sparse_mask = mx.zeros((B, L, k_seq), dtype=mx.bool_)
|
||||
sparse_mask = mx.put_along_axis(
|
||||
sparse_mask, topk_indices, mx.array(True), axis=-1
|
||||
)
|
||||
sparse_mask = sparse_mask[:, None, :, :]
|
||||
if mask is not None:
|
||||
sparse_mask = sparse_mask & mask
|
||||
mask = sparse_mask
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache[0], scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DeepseekV32MLP(nn.Module):
|
||||
def __init__(
|
||||
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV32MoE(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.num_experts_per_tok = config.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
if config.n_shared_experts is not None:
|
||||
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
|
||||
self.shared_experts = DeepseekV32MLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DeepseekV32DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = DeepseekV32Attention(config)
|
||||
self.mlp = (
|
||||
DeepseekV32MoE(config)
|
||||
if (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
and layer_idx % config.moe_layer_freq == 0
|
||||
)
|
||||
else DeepseekV32MLP(config)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class DeepseekV32Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.layers = [
|
||||
DeepseekV32DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.start_idx = 0
|
||||
self.end_idx = len(self.layers)
|
||||
self.num_layers = self.end_idx
|
||||
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
self.num_layers = len(self.layers) - self.start_idx
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
mask = create_attention_mask(
|
||||
h, cache[0][0] if cache[0] else None, return_array=True
|
||||
)
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = DeepseekV32Model(config)
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
pad_side = (-n) % bs
|
||||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||||
weight = weight.reshape(
|
||||
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
|
||||
)
|
||||
weight = (weight * scale_inv[:, None, :, None]).reshape(
|
||||
m + pad_bottom, n + pad_side
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
|
||||
# Remove multi-token prediction layer and any unused precomputed rotary freqs
|
||||
return {
|
||||
k: v
|
||||
for k, v in weights.items()
|
||||
if not k.startswith("model.layers.61") and "rotary_emb.inv_freq" not in k
|
||||
}
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def make_cache(self):
|
||||
return [CacheList(KVCache(), KVCache()) for _ in self.layers]
|
||||
@@ -22,10 +22,9 @@ class ModelArgs(BaseModelArgs):
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
max_position_embeddings: Optional[int]
|
||||
num_key_value_heads: Optional[int]
|
||||
num_key_value_heads: int
|
||||
first_k_dense_replace: int
|
||||
moe_intermediate_size: int
|
||||
moe_layer_freq: int
|
||||
n_routed_experts: int
|
||||
n_shared_experts: int
|
||||
norm_topk_prob: bool
|
||||
@@ -48,7 +47,6 @@ class Dots1Attention(nn.Module):
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
assert args.num_key_value_heads is not None
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
head_dim = args.head_dim or args.hidden_size // n_heads
|
||||
@@ -254,17 +252,15 @@ class Dots1Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -283,10 +279,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -123,17 +123,15 @@ class Ernie45Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -152,10 +150,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -219,17 +219,15 @@ class Ernie45Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -248,10 +246,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -123,16 +123,15 @@ class ExaoneModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.wte(inputs)
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
@@ -151,10 +150,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.transformer.wte.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -149,23 +149,35 @@ class ExaoneModel(nn.Module):
|
||||
)
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
if pattern:
|
||||
self.swa_idx = pattern.index("L")
|
||||
self.full_idx = pattern.index("G")
|
||||
else:
|
||||
self.swa_idx = None
|
||||
self.full_idx = 0
|
||||
|
||||
self.window_size = args.sliding_window
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
global_mask = create_attention_mask(h, cache[self.full_idx])
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.window_size
|
||||
)
|
||||
else:
|
||||
swa_mask = None
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.self_attn.is_local else global_mask
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
@@ -183,10 +195,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
# 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 .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import CacheList, KVCache, MambaCache
|
||||
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
|
||||
|
||||
|
||||
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 = hidden_states * nn.silu(gate)
|
||||
|
||||
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 = hidden_states * nn.silu(gate)
|
||||
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 _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
) -> 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 cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
|
||||
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,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> 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)
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
if self.mamba_rms_norm:
|
||||
y = self.norm(y, gate)
|
||||
else:
|
||||
y = y * nn.silu(gate)
|
||||
|
||||
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 = self.up_proj(x) * nn.silu(self.gate_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)
|
||||
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)
|
||||
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(MambaCache(), KVCache())
|
||||
for _ in range(self.args.num_hidden_layers)
|
||||
]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,284 @@
|
||||
from functools import partial
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def _make_gated_delta_kernel(has_mask=False, vectorized=False):
|
||||
if not mx.metal.is_available():
|
||||
return None
|
||||
mask_source = "mask[b_idx * T + t]" if has_mask else "true"
|
||||
|
||||
# Configure g indexing based on whether gating is vectorized
|
||||
if vectorized:
|
||||
g_comment = "// g: [B, T, Hv, Dk]"
|
||||
g_setup = "auto g_ = g + (b_idx * T * Hv + hv_idx) * Dk;"
|
||||
g_access = "g_[s_idx]"
|
||||
g_advance = "g_ += Hv * Dk;"
|
||||
else:
|
||||
g_comment = "// g: [B, T, Hv]"
|
||||
g_setup = "auto g_ = g + b_idx * T * Hv;"
|
||||
g_access = "g_[hv_idx]"
|
||||
g_advance = "g_ += Hv;"
|
||||
|
||||
source = f"""
|
||||
auto n = thread_position_in_grid.z;
|
||||
auto b_idx = n / Hv;
|
||||
auto hv_idx = n % Hv;
|
||||
auto hk_idx = hv_idx / (Hv / Hk);
|
||||
constexpr int n_per_t = Dk / 32;
|
||||
|
||||
// q, k: [B, T, Hk, Dk]
|
||||
auto q_ = q + b_idx * T * Hk * Dk + hk_idx * Dk;
|
||||
auto k_ = k + b_idx * T * Hk * Dk + hk_idx * Dk;
|
||||
|
||||
// v, y: [B, T, Hv, Dv]
|
||||
auto v_ = v + b_idx * T * Hv * Dv + hv_idx * Dv;
|
||||
y += b_idx * T * Hv * Dv + hv_idx * Dv;
|
||||
|
||||
auto dk_idx = thread_position_in_threadgroup.x;
|
||||
auto dv_idx = thread_position_in_grid.y;
|
||||
|
||||
// state_in, state_out: [B, Hv, Dv, Dk]
|
||||
auto i_state = state_in + (n * Dv + dv_idx) * Dk;
|
||||
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]);
|
||||
}}
|
||||
|
||||
{g_comment}
|
||||
{g_setup}
|
||||
auto beta_ = beta + b_idx * T * Hv;
|
||||
|
||||
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];
|
||||
|
||||
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);
|
||||
}}
|
||||
}}
|
||||
// Increment data pointers to next time step
|
||||
q_ += Hk * Dk;
|
||||
k_ += Hk * Dk;
|
||||
v_ += Hv * Dv;
|
||||
y += Hv * Dv;
|
||||
{g_advance}
|
||||
beta_ += Hv;
|
||||
}}
|
||||
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]);
|
||||
}}
|
||||
"""
|
||||
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=f"gated_delta_step{suffix}",
|
||||
input_names=inputs,
|
||||
output_names=["y", "state_out"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
_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,
|
||||
v: mx.array,
|
||||
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.
|
||||
|
||||
Shapes:
|
||||
- q, k: [B, H, Dk]
|
||||
- v: [B, H, Dv]
|
||||
- g: [B, H] or [B, H, Dk]
|
||||
- beta: [B, H]
|
||||
- state: [B, H, Dv, Dk]
|
||||
Returns:
|
||||
- y: [B, H, Dv]
|
||||
- new_state: [B, H, Dv, Dk]
|
||||
"""
|
||||
|
||||
# Decay
|
||||
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]
|
||||
if mask is not None:
|
||||
if mask.ndim == 2:
|
||||
mask = mx.expand_dims(mask, axes=(2, 3))
|
||||
elif mask.ndim == 3:
|
||||
mask = mx.expand_dims(mask, axis=-1)
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y, state
|
||||
|
||||
|
||||
def gated_delta_kernel(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
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
|
||||
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),
|
||||
("Dk", Dk),
|
||||
("Dv", Dv),
|
||||
("Hk", Hk),
|
||||
("Hv", Hv),
|
||||
],
|
||||
grid=(32, Dv, B * Hv),
|
||||
threadgroup=(32, 4, 1),
|
||||
output_shapes=[(B, T, Hv, Dv), state.shape],
|
||||
output_dtypes=[input_type, input_type],
|
||||
)
|
||||
|
||||
|
||||
def gated_delta_ops(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
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: [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, 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)
|
||||
|
||||
if (repeat_factor := Hv // Hk) > 1:
|
||||
q = mx.repeat(q, repeat_factor, -2)
|
||||
k = mx.repeat(k, repeat_factor, -2)
|
||||
|
||||
ys = []
|
||||
for t in range(T):
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
None if mask is None else mask[:, t],
|
||||
)
|
||||
ys.append(y)
|
||||
y = mx.stack(ys, axis=1)
|
||||
return y, state
|
||||
|
||||
|
||||
def gated_delta_update(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
a: mx.array,
|
||||
b: mx.array,
|
||||
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:]
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
|
||||
|
||||
if not use_kernel or mx.default_device() != mx.gpu or not mx.metal.is_available():
|
||||
return gated_delta_ops(q, k, v, g, beta, state, mask)
|
||||
return gated_delta_kernel(q, k, v, g, beta, state, mask)
|
||||
@@ -138,18 +138,16 @@ class GemmaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
h = h * (self.args.hidden_size**0.5)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -166,10 +164,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -165,18 +165,16 @@ class GemmaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
h = h * (self.args.hidden_size**0.5)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache, return_array=True)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0], return_array=True)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -194,10 +192,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = mx.tanh(out / self.final_logit_softcapping)
|
||||
out = out * self.final_logit_softcapping
|
||||
|
||||
@@ -40,11 +40,10 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,12 +23,13 @@ class ModelArgs(BaseModelArgs):
|
||||
rms_norm_eps: float = 1.0e-6
|
||||
vocab_size: int = 262144
|
||||
num_key_value_heads: int = 1
|
||||
rope_global_base_freq: float = 1_000_000.0
|
||||
rope_theta: float = 1_000_000.0
|
||||
rope_local_base_freq: float = 10_000.0
|
||||
rope_traditional: bool = False
|
||||
query_pre_attn_scalar: float = 256
|
||||
sliding_window: int = 512
|
||||
sliding_window_pattern: int = 6
|
||||
max_position_embeddings: int = 32768
|
||||
rope_scaling: Dict = None
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
@@ -52,14 +54,12 @@ class Attention(nn.Module):
|
||||
self.k_norm = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
|
||||
self.is_sliding = (layer_idx + 1) % args.sliding_window_pattern != 0
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
head_dim,
|
||||
traditional=args.rope_traditional,
|
||||
base=(
|
||||
args.rope_local_base_freq
|
||||
if self.is_sliding
|
||||
else args.rope_global_base_freq
|
||||
),
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=(args.rope_local_base_freq if self.is_sliding else args.rope_theta),
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
@@ -87,8 +87,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
|
||||
)
|
||||
@@ -160,6 +158,8 @@ class Gemma3Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.window_size = args.sliding_window
|
||||
self.sliding_window_pattern = args.sliding_window_pattern
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
assert self.vocab_size > 0
|
||||
@@ -173,7 +173,6 @@ class Gemma3Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
@@ -186,24 +185,22 @@ class Gemma3Model(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
if mask is None:
|
||||
j = self.args.sliding_window_pattern
|
||||
full_mask = create_attention_mask(h, cache[j - 1 : j])
|
||||
sliding_window_mask = create_attention_mask(h, cache)
|
||||
global_mask = create_attention_mask(h, cache[self.sliding_window_pattern - 1])
|
||||
|
||||
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.args.sliding_window_pattern
|
||||
== self.args.sliding_window_pattern - 1
|
||||
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
|
||||
)
|
||||
|
||||
local_mask = mask
|
||||
if mask is None and is_global:
|
||||
local_mask = full_mask
|
||||
elif mask is None:
|
||||
local_mask = sliding_window_mask
|
||||
|
||||
h = layer(h, local_mask, c)
|
||||
mask = global_mask if is_global else sliding_window_mask
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -215,22 +212,25 @@ class Model(nn.Module):
|
||||
self.model_type = args.model_type
|
||||
self.model = Gemma3Model(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.tie_word_embeddings = False
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache, input_embeddings)
|
||||
out = self.lm_head(out)
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = dict(weights)
|
||||
if "lm_head.weight" not in weights:
|
||||
weights["lm_head.weight"] = weights["model.embed_tokens.weight"]
|
||||
self.tie_word_embeddings = True
|
||||
self.pop("lm_head")
|
||||
return weights
|
||||
|
||||
@property
|
||||
@@ -246,7 +246,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
|
||||
|
||||
+28
-35
@@ -151,9 +151,6 @@ class Gemma3nAttention(nn.Module):
|
||||
queries = queries.transpose(0, 2, 1, 3)
|
||||
queries = self.rope(queries, offset=offset)
|
||||
|
||||
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
|
||||
)
|
||||
@@ -176,7 +173,11 @@ class MLP(nn.Module):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.intermediate_size = config.intermediate_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size[layer_idx]
|
||||
if isinstance(config.intermediate_size, list)
|
||||
else config.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)
|
||||
@@ -269,12 +270,11 @@ class Gemma3nAltUp(nn.Module):
|
||||
)
|
||||
|
||||
all_coefs = self.correction_coefs(modalities) + 1.0
|
||||
|
||||
active_x = predictions[self.config.altup_active_idx]
|
||||
innovation = activated - active_x
|
||||
|
||||
all_coefs = all_coefs.transpose(2, 1, 0)
|
||||
corrected = innovation[None] * all_coefs[:, None]
|
||||
all_coefs = all_coefs.moveaxis(2, 0)
|
||||
corrected = innovation[None] * all_coefs[..., None]
|
||||
corrected += predictions
|
||||
|
||||
return corrected.astype(activated.dtype)
|
||||
@@ -306,7 +306,6 @@ class Gemma3nDecoderLayer(nn.Module):
|
||||
eps=config.rms_norm_eps,
|
||||
)
|
||||
self.is_sliding = self.self_attn.is_sliding
|
||||
self.sliding_window = config.sliding_window
|
||||
|
||||
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
|
||||
|
||||
@@ -351,7 +350,6 @@ class Gemma3nDecoderLayer(nn.Module):
|
||||
attn_ffw = self.mlp(attn_norm)
|
||||
attn_ffw_norm = self.post_feedforward_layernorm(attn_ffw)
|
||||
attn_ffw_laurel_gated = attn_laurel + attn_ffw_norm
|
||||
|
||||
corrected_predictions = self.altup.correct(predictions, attn_ffw_laurel_gated)
|
||||
|
||||
first_prediction = corrected_predictions[self.config.altup_active_idx]
|
||||
@@ -433,10 +431,11 @@ class LanguageModel(nn.Module):
|
||||
eps=config.rms_norm_eps,
|
||||
)
|
||||
|
||||
self.first_sliding_idx = self.config.layer_types.index("sliding_attention")
|
||||
self.first_full_idx = self.config.layer_types.index("full_attention")
|
||||
self.first_sliding_idx = config.layer_types.index("sliding_attention")
|
||||
self.first_full_idx = config.layer_types.index("full_attention")
|
||||
self.sliding_window = config.sliding_window
|
||||
|
||||
concrete_layers = self.config.layer_types[: self.first_kv_shared_layer_idx]
|
||||
concrete_layers = config.layer_types[: self.first_kv_shared_layer_idx]
|
||||
shared_full_idx = (
|
||||
len(concrete_layers) - 1 - concrete_layers[::-1].index("full_attention")
|
||||
)
|
||||
@@ -459,7 +458,6 @@ class LanguageModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array = None,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: mx.array = None,
|
||||
):
|
||||
@@ -474,15 +472,15 @@ class LanguageModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
if mask is None:
|
||||
full_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.first_full_idx :],
|
||||
)
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.first_sliding_idx :],
|
||||
)
|
||||
global_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.first_full_idx],
|
||||
)
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.first_sliding_idx],
|
||||
window_size=self.sliding_window,
|
||||
)
|
||||
h0 = h
|
||||
|
||||
# Expand hidden_states to support per-layer inputs
|
||||
@@ -493,21 +491,19 @@ class LanguageModel(nn.Module):
|
||||
h = mx.stack(h_list, axis=0)
|
||||
mags = mx.mean(h[1:] ** 2, axis=-1, keepdims=True) ** 0.5
|
||||
h[1:] = h[1:] * (target_magnitude / mx.maximum(mags, mx.finfo(h0.dtype).min))
|
||||
|
||||
for i, layer in enumerate(self.layers):
|
||||
per_layer_input = per_layer_inputs[:, :, i, :]
|
||||
|
||||
is_global = self.config.layer_types[i] == "full_attention"
|
||||
|
||||
local_mask = mask
|
||||
if mask is None and is_global:
|
||||
local_mask = full_mask
|
||||
elif mask is None:
|
||||
local_mask = sliding_window_mask
|
||||
if is_global:
|
||||
mask = global_mask
|
||||
else:
|
||||
mask = sliding_window_mask
|
||||
|
||||
h = layer(
|
||||
h,
|
||||
local_mask,
|
||||
mask,
|
||||
cache[self.layer_idx_to_cache_idx[i]],
|
||||
per_layer_input,
|
||||
)
|
||||
@@ -578,11 +574,10 @@ class Gemma3n(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def make_cache(self):
|
||||
@@ -594,17 +589,15 @@ class Model(nn.Module):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model = Gemma3n(args)
|
||||
self.model_type = args.model_type
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.model(
|
||||
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
|
||||
)
|
||||
return self.model(inputs, cache=cache, input_embeddings=input_embeddings)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
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: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: Optional[int] = None
|
||||
attention_bias: bool = False
|
||||
rope_theta: float = 10000
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class GLMAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.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 = args.head_dim or args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_attention_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.rope = nn.RoPE(dims=self.head_dim, traditional=True, 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)
|
||||
|
||||
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 GLMMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_up_proj = nn.Linear(
|
||||
args.hidden_size, 2 * args.intermediate_size, bias=False
|
||||
)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class GLMBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = GLMAttention(args)
|
||||
self.mlp = GLMMLP(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 = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class GLMModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [GLMBlock(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,
|
||||
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 = GLMModel(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 sanitize(self, weights):
|
||||
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)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -144,17 +144,15 @@ class Glm4Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -172,10 +170,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
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 .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
max_position_embeddings: int
|
||||
moe_intermediate_size: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
n_group: int
|
||||
head_dim: int
|
||||
topk_group: int
|
||||
n_shared_experts: int
|
||||
n_routed_experts: int
|
||||
routed_scaling_factor: float
|
||||
num_experts_per_tok: int
|
||||
first_k_dense_replace: int
|
||||
num_hidden_layers: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
rope_scaling: Optional[Dict]
|
||||
use_qk_norm: bool
|
||||
tie_word_embeddings: bool
|
||||
attention_bias: bool
|
||||
partial_rotary_factor: float
|
||||
scoring_func: str = "sigmoid"
|
||||
topk_method: str = "noaux_tc"
|
||||
|
||||
|
||||
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
|
||||
|
||||
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=False)
|
||||
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
if self.use_qk_norm:
|
||||
self.q_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
int(head_dim * args.partial_rotary_factor),
|
||||
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)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_heads, -1)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, -1)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
queries = queries.transpose(0, 2, 1, 3)
|
||||
keys = keys.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
|
||||
)
|
||||
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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class 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, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(config)
|
||||
self.mlp = (
|
||||
MoE(config)
|
||||
if (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
)
|
||||
else MLP(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 LanguageModel(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 = [
|
||||
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__(
|
||||
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])
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
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):
|
||||
mpt_layer = self.args.num_hidden_layers
|
||||
|
||||
# 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(f"model.layers.{mpt_layer}")
|
||||
}
|
||||
|
||||
@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
|
||||
+13
-14
@@ -1,4 +1,4 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2023 - 2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
@@ -125,26 +125,26 @@ class GPT2Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
_, L = inputs.shape
|
||||
|
||||
hidden_states = self.wte(inputs)
|
||||
|
||||
offset = 0
|
||||
if cache is not None and len(cache) > 0 and cache[0] is not None:
|
||||
offset = cache[0].offset
|
||||
|
||||
position_ids = mx.arange(offset, offset + L)
|
||||
hidden_states += self.wpe(position_ids)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(hidden_states, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
offset = 0
|
||||
if cache[0] is not None:
|
||||
offset = cache[0].offset
|
||||
|
||||
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])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
hidden_states = layer(hidden_states, mask, cache=c)
|
||||
|
||||
@@ -161,10 +161,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.wte.as_linear(out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -137,23 +137,20 @@ class GPTBigCodeModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
B, L = inputs.shape
|
||||
|
||||
hidden_states = self.wte(inputs)
|
||||
|
||||
mask = None
|
||||
if mask is not None and hidden_states.shape[1] > 1:
|
||||
mask = create_attention_mask(hidden_states, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
position_ids = mx.array(np.arange(L))
|
||||
else:
|
||||
position_ids = mx.array(np.arange(cache[0].offset, cache[0].offset + L))
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[0])
|
||||
|
||||
hidden_states += self.wpe(position_ids)
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
@@ -174,10 +171,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.transformer.wte.as_linear(out)
|
||||
else:
|
||||
|
||||
+19
-12
@@ -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):
|
||||
@@ -145,19 +155,17 @@ class GPTNeoXModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
_, L = inputs.shape
|
||||
|
||||
hidden_states = self.embed_in(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(hidden_states, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
hidden_states = layer(hidden_states, mask, cache=c)
|
||||
|
||||
@@ -177,10 +185,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, 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
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "gpt_oss"
|
||||
num_hidden_layers: int = 36
|
||||
num_local_experts: int = 128
|
||||
num_experts_per_tok: int = 4
|
||||
vocab_size: int = 201088
|
||||
rms_norm_eps: float = 1e-05
|
||||
hidden_size: int = 2880
|
||||
intermediate_size: int = 2880
|
||||
head_dim: int = 64
|
||||
num_attention_heads: int = 64
|
||||
num_key_value_heads: int = 8
|
||||
sliding_window: int = 128
|
||||
rope_theta: int = 150000
|
||||
rope_scaling: Any = None
|
||||
layer_types: list = None
|
||||
|
||||
|
||||
# These operators emulate particular methods in torch that don't exist in MLX natively
|
||||
def mlx_topk(a, k, axis=-1):
|
||||
"""MLX equivalent of torch.topk"""
|
||||
partitioned_indices = mx.argpartition(a, kth=-k, axis=axis)
|
||||
# Extract only the top k indices (last k elements after partition)
|
||||
top_k_indices = partitioned_indices[..., -k:]
|
||||
# Get the corresponding values
|
||||
top_k_values = mx.take_along_axis(a, top_k_indices, axis=axis)
|
||||
return top_k_values, top_k_indices
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(x_linear, x_glu, alpha: float = 1.702, limit: float = 7.0):
|
||||
# Clamp the input values
|
||||
x_glu = mx.clip(x_glu, a_min=None, a_max=limit)
|
||||
x_linear = mx.clip(x_linear, a_min=-limit, a_max=limit)
|
||||
|
||||
glu_scaled = alpha * x_glu
|
||||
sig = mx.sigmoid(glu_scaled)
|
||||
|
||||
out_glu = x_glu * sig
|
||||
# Note we add an extra bias of 1 to the linear layer
|
||||
return out_glu * (x_linear + 1)
|
||||
|
||||
|
||||
class SwiGLU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, x, gate):
|
||||
return swiglu(x, gate)
|
||||
|
||||
|
||||
class AttentionBlock(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.head_dim = config.head_dim
|
||||
self.num_attention_heads = config.num_attention_heads
|
||||
self.num_key_value_heads = config.num_key_value_heads
|
||||
self.num_key_value_groups = (
|
||||
config.num_attention_heads // config.num_key_value_heads
|
||||
)
|
||||
|
||||
self.sinks = mx.zeros((config.num_attention_heads,))
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
config.hidden_size, config.num_attention_heads * self.head_dim, bias=True
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
config.hidden_size, config.num_key_value_heads * self.head_dim, bias=True
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
config.hidden_size, config.num_key_value_heads * self.head_dim, bias=True
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.head_dim * config.num_attention_heads, config.hidden_size, bias=True
|
||||
)
|
||||
|
||||
self.sm_scale = 1 / math.sqrt(config.head_dim)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
config.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array, mask: mx.array, cache=None) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
D = self.head_dim
|
||||
Hk = self.num_key_value_heads
|
||||
|
||||
q = self.q_proj(x).reshape(B, L, -1, D).swapaxes(1, 2)
|
||||
k = self.k_proj(x).reshape(B, L, -1, D).swapaxes(1, 2)
|
||||
v = self.v_proj(x).reshape(B, L, -1, D).swapaxes(1, 2)
|
||||
|
||||
if cache is not None:
|
||||
q = self.rope(q, offset=cache.offset)
|
||||
k = self.rope(k, offset=cache.offset)
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
else:
|
||||
q = self.rope(q)
|
||||
k = self.rope(k)
|
||||
|
||||
v_hat = scaled_dot_product_attention(
|
||||
q, k, v, cache, self.sm_scale, mask=mask, sinks=self.sinks
|
||||
)
|
||||
|
||||
return self.o_proj(v_hat.swapaxes(1, 2).reshape(B, L, -1))
|
||||
|
||||
|
||||
class MLPBlock(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_local_experts = config.num_local_experts
|
||||
self.num_experts_per_tok = config.num_experts_per_tok
|
||||
|
||||
self.experts = SwitchGLU(
|
||||
input_dims=config.hidden_size,
|
||||
hidden_dims=config.intermediate_size,
|
||||
num_experts=config.num_local_experts,
|
||||
activation=SwiGLU(),
|
||||
bias=True,
|
||||
)
|
||||
self.router = nn.Linear(config.hidden_size, config.num_local_experts, bias=True)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
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)
|
||||
|
||||
# Experts block
|
||||
x = self.experts(x, indices)
|
||||
|
||||
x = x * mx.expand_dims(expert_weights, axis=-1)
|
||||
return x.sum(axis=-2)
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = AttentionBlock(config)
|
||||
self.mlp = MLPBlock(config)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, config.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array, mask: mx.array, cache=None) -> mx.array:
|
||||
residual = x
|
||||
x = self.input_layernorm(x)
|
||||
x = self.self_attn(x, mask, cache)
|
||||
x = residual + x
|
||||
|
||||
residual = x
|
||||
x = self.post_attention_layernorm(x)
|
||||
x = self.mlp(x)
|
||||
x = residual + x
|
||||
return x
|
||||
|
||||
|
||||
class GptOssMoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.norm = nn.RMSNorm(args.hidden_size, args.rms_norm_eps)
|
||||
self.layer_types = args.layer_types or [
|
||||
"sliding_attention",
|
||||
"full_attention",
|
||||
] * (args.num_hidden_layers // 2)
|
||||
self.layers = [TransformerBlock(args) for _ in range(args.num_hidden_layers)]
|
||||
self.window_size = args.sliding_window
|
||||
self.swa_idx = self.layer_types.index("sliding_attention")
|
||||
self.ga_idx = self.layer_types.index("full_attention")
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
if input_embeddings is not None:
|
||||
x = input_embeddings
|
||||
else:
|
||||
x = self.embed_tokens(inputs)
|
||||
|
||||
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.window_size
|
||||
)
|
||||
|
||||
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
|
||||
mask = full_mask if layer_type == "full_attention" else swa_mask
|
||||
x = layer(x, mask, c)
|
||||
x = self.norm(x)
|
||||
return x
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = GptOssMoeModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(self, inputs: mx.array, cache=None):
|
||||
return self.lm_head(self.model(inputs, cache))
|
||||
|
||||
def sanitize(self, weights):
|
||||
if any("gate_proj.weight" in k for k in weights.keys()):
|
||||
return weights # already sanitized
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "gate_up_proj" in k and "bias" not in k:
|
||||
if "_blocks" in k:
|
||||
v = v.view(mx.uint32).flatten(-2)
|
||||
k = k.replace("_blocks", ".weight")
|
||||
if "_scales" in k:
|
||||
k = k.replace("_scales", ".scales")
|
||||
new_weights[k.replace("gate_up_proj", "gate_proj")] = mx.contiguous(
|
||||
v[..., ::2, :]
|
||||
)
|
||||
new_weights[k.replace("gate_up_proj", "up_proj")] = mx.contiguous(
|
||||
v[..., 1::2, :]
|
||||
)
|
||||
elif "down_proj" in k and "bias" not in k:
|
||||
if "_blocks" in k:
|
||||
v = v.view(mx.uint32).flatten(-2)
|
||||
k = k.replace("_blocks", ".weight")
|
||||
if "_scales" in k:
|
||||
k = k.replace("_scales", ".scales")
|
||||
new_weights[k] = v
|
||||
elif "gate_up_proj_bias" in k:
|
||||
new_weights[k.replace("gate_up_proj_bias", "gate_proj.bias")] = (
|
||||
mx.contiguous(v[..., ::2])
|
||||
)
|
||||
new_weights[k.replace("gate_up_proj_bias", "up_proj.bias")] = (
|
||||
mx.contiguous(v[..., 1::2])
|
||||
)
|
||||
elif "down_proj_bias" in k:
|
||||
new_weights[k.replace("down_proj_bias", "down_proj.bias")] = v
|
||||
else:
|
||||
new_weights[k] = v
|
||||
|
||||
return new_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
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for lt in self.model.layer_types:
|
||||
if lt == "full_attention":
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
@@ -150,17 +150,15 @@ class GraniteModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs) * self.embedding_multiplier
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -180,10 +178,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@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
|
||||
logits_scaling: float
|
||||
attention_multiplier: float
|
||||
embedding_multiplier: float
|
||||
residual_multiplier: float
|
||||
max_position_embeddings: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
rope_theta: float
|
||||
num_local_experts: int
|
||||
num_experts_per_tok: int
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class GraniteMoeAttention(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 = args.attention_multiplier
|
||||
attention_bias = args.attention_bias
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=attention_bias)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False,
|
||||
args.rope_scaling,
|
||||
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 = 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
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class GraniteMoeTopKGating(nn.Module):
|
||||
def __init__(self, input_size: int, num_experts: int, top_k: int):
|
||||
super().__init__()
|
||||
self.num_experts = num_experts
|
||||
self.input_size = input_size
|
||||
self.top_k = top_k
|
||||
self.layer = nn.Linear(input_size, num_experts, bias=False)
|
||||
|
||||
def __call__(self, hidden_states: mx.array):
|
||||
logits = self.layer(hidden_states)
|
||||
top_k_idx = mx.argpartition(logits, kth=-self.top_k, axis=-1)[
|
||||
..., -self.top_k :
|
||||
]
|
||||
top_k_logits = mx.take_along_axis(logits, top_k_idx, axis=-1)
|
||||
top_k_gates = mx.softmax(top_k_logits.astype(mx.float32), axis=-1)
|
||||
return top_k_idx, top_k_gates
|
||||
|
||||
|
||||
class GraniteMoeMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.input_size = args.hidden_size
|
||||
self.hidden_size = args.intermediate_size
|
||||
self.switch_mlp = SwitchGLU(
|
||||
self.input_size, self.hidden_size, args.num_local_experts
|
||||
)
|
||||
self.router = GraniteMoeTopKGating(
|
||||
input_size=self.input_size,
|
||||
num_experts=args.num_local_experts,
|
||||
top_k=args.num_experts_per_tok,
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
token_ids, gates = self.router(x)
|
||||
y = self.switch_mlp(x, token_ids)
|
||||
return (y * gates[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
|
||||
class GraniteMoeDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = GraniteMoeAttention(args)
|
||||
self.block_sparse_moe = GraniteMoeMoE(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.residual_multiplier = args.residual_multiplier
|
||||
|
||||
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 * self.residual_multiplier
|
||||
r = self.block_sparse_moe(self.post_attention_layernorm(h))
|
||||
out = h + r * self.residual_multiplier
|
||||
return out
|
||||
|
||||
|
||||
class GraniteMoEModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
GraniteMoeDecoderLayer(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.embedding_multiplier = args.embedding_multiplier
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs) * self.embedding_multiplier
|
||||
|
||||
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, 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 = GraniteMoEModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.logits_scaling = args.logits_scaling
|
||||
|
||||
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 / self.logits_scaling
|
||||
|
||||
def sanitize(self, weights):
|
||||
if "model.layers.0.block_sparse_moe.input_linear.weight" not in weights:
|
||||
return weights
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.block_sparse_moe"
|
||||
key = f"{prefix}.input_linear.weight"
|
||||
value = weights.pop(key)
|
||||
gate_proj, up_proj = mx.split(value, 2, axis=1)
|
||||
weights[key.replace("input_linear", "switch_mlp.gate_proj")] = gate_proj
|
||||
weights[key.replace("input_linear", "switch_mlp.up_proj")] = up_proj
|
||||
key = f"{prefix}.output_linear.weight"
|
||||
weights[key.replace("output_linear", "switch_mlp.down_proj")] = weights.pop(
|
||||
key
|
||||
)
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("block_sparse_moe.router.layer"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,541 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .ssm import ssm_update
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
# Required fields (no defaults)
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
max_position_embeddings: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
embedding_multiplier: float
|
||||
attention_multiplier: float
|
||||
logits_scaling: float
|
||||
residual_multiplier: float
|
||||
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):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = mx.ones(hidden_size)
|
||||
|
||||
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)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
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_n_heads * args.mamba_d_head
|
||||
self.n_groups = args.mamba_n_groups
|
||||
self.head_dim = args.mamba_d_head
|
||||
self.time_step_limit = args.time_step_limit
|
||||
self.heads_per_group = self.num_heads // self.n_groups
|
||||
|
||||
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,
|
||||
kernel_size=args.mamba_d_conv,
|
||||
padding=0,
|
||||
groups=self.conv_dim,
|
||||
bias=args.mamba_conv_bias,
|
||||
)
|
||||
|
||||
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)
|
||||
self.A_log = mx.log(mx.arange(1, self.num_heads + 1, dtype=mx.float32))
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.norm = GraniteMoeHybridRMSNormGated(
|
||||
self.intermediate_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.mamba_proj_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
) -> 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 cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
|
||||
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,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> 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)
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache] = None,
|
||||
) -> mx.array:
|
||||
|
||||
projected = self.in_proj(hidden_states)
|
||||
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected,
|
||||
[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)
|
||||
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class GraniteMoeHybridAttention(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 = args.attention_multiplier
|
||||
attention_bias = args.attention_bias
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=attention_bias)
|
||||
|
||||
# Check if RoPE should be used based on position_embedding_type
|
||||
# If position_embedding_type is "nope", don't use RoPE
|
||||
use_rope = args.position_embedding_type != "nope"
|
||||
if use_rope:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False,
|
||||
None, # rope_scaling
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
else:
|
||||
self.rope = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = 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)
|
||||
|
||||
# Apply RoPE only if enabled
|
||||
if 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)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class GraniteMoeHybridTopKGating(nn.Module):
|
||||
def __init__(self, input_size: int, num_experts: int, top_k: int):
|
||||
super().__init__()
|
||||
self.num_experts = num_experts
|
||||
self.input_size = input_size
|
||||
self.top_k = top_k
|
||||
self.layer = nn.Linear(input_size, num_experts, bias=False)
|
||||
|
||||
def __call__(self, hidden_states: mx.array):
|
||||
logits = self.layer(hidden_states)
|
||||
top_k_idx = mx.argpartition(logits, kth=-self.top_k, axis=-1)[
|
||||
..., -self.top_k :
|
||||
]
|
||||
top_k_logits = mx.take_along_axis(logits, top_k_idx, axis=-1)
|
||||
top_k_gates = mx.softmax(top_k_logits, precise=True, axis=-1)
|
||||
return top_k_idx, top_k_gates
|
||||
|
||||
|
||||
class GraniteMoeHybridMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.input_size = args.hidden_size
|
||||
self.hidden_size = args.intermediate_size
|
||||
self.switch_mlp = SwitchGLU(
|
||||
self.input_size, self.hidden_size, args.num_local_experts
|
||||
)
|
||||
self.router = GraniteMoeHybridTopKGating(
|
||||
input_size=self.input_size,
|
||||
num_experts=args.num_local_experts,
|
||||
top_k=args.num_experts_per_tok,
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
token_ids, gates = self.router(x)
|
||||
y = self.switch_mlp(x, token_ids)
|
||||
return (y * gates[..., None]).sum(axis=-2)
|
||||
|
||||
|
||||
class GraniteMoeHybridSharedMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.input_linear = nn.Linear(
|
||||
args.hidden_size, args.shared_intermediate_size * 2, bias=False
|
||||
)
|
||||
self.output_linear = nn.Linear(
|
||||
args.shared_intermediate_size, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class GraniteMoeHybridLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_type: str):
|
||||
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)
|
||||
|
||||
if layer_type == "mamba":
|
||||
self.mamba = GraniteMoeHybridMamba2Mixer(args)
|
||||
elif layer_type == "attention":
|
||||
self.self_attn = GraniteMoeHybridAttention(args)
|
||||
else:
|
||||
raise ValueError(f"Unknown layer type: {layer_type}")
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
# First block: either Mamba or Attention
|
||||
residual = x
|
||||
hidden_states = self.input_layernorm(x)
|
||||
|
||||
if self.layer_type == "mamba":
|
||||
hidden_states = self.mamba(hidden_states, mask=mask, cache=cache)
|
||||
else:
|
||||
hidden_states = self.self_attn(hidden_states, mask=mask, cache=cache)
|
||||
|
||||
hidden_states = residual + hidden_states * self.residual_multiplier
|
||||
|
||||
# Second block: MoE + shared_mlp OR dense MLP
|
||||
residual = hidden_states
|
||||
normed = self.post_attention_layernorm(hidden_states)
|
||||
|
||||
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
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
class GraniteMoeHybridModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
GraniteMoeHybridLayer(args, layer_type) for layer_type in args.layer_types
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.embedding_multiplier = args.embedding_multiplier
|
||||
|
||||
# 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,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
hidden_states = self.embed_tokens(inputs) * self.embedding_multiplier
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
# Create masks based on what layer types exist
|
||||
attn_mask = None
|
||||
mamba_mask = None
|
||||
|
||||
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)
|
||||
|
||||
return self.norm(hidden_states)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = GraniteMoeHybridModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.logits_scaling = args.logits_scaling
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache=cache)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
|
||||
return out / self.logits_scaling
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.layers:
|
||||
if layer.layer_type == "mamba":
|
||||
caches.append(MambaCache())
|
||||
elif layer.layer_type == "attention":
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Handle conv1d weights
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
# 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"
|
||||
|
||||
input_weight = weights.pop(f"{prefix}.input_linear.weight")
|
||||
_, expert_hidden, _ = input_weight.shape
|
||||
|
||||
# Split into gate and up projections (each half of expert_hidden)
|
||||
gate_proj = input_weight[:, : expert_hidden // 2, :]
|
||||
up_proj = input_weight[:, expert_hidden // 2 :, :]
|
||||
weights[f"{prefix}.switch_mlp.gate_proj.weight"] = gate_proj
|
||||
weights[f"{prefix}.switch_mlp.up_proj.weight"] = up_proj
|
||||
|
||||
weights[f"{prefix}.switch_mlp.down_proj.weight"] = weights.pop(
|
||||
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 self.args.use_moe and path.endswith("router.layer"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -136,17 +136,15 @@ class HeliumModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -170,10 +168,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -259,17 +259,14 @@ class HunYuanModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
for i, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
if (not self.args.use_cla) or i % self.args.cla_share_factor == 0:
|
||||
shared_kv_states = None
|
||||
@@ -288,10 +285,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float = 10000
|
||||
max_position_embeddings: int = 32768
|
||||
attention_bias: bool = False
|
||||
use_qk_norm: bool = True
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
head_dim: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_scaling:
|
||||
required_keys = {"alpha", "factor", "type"}
|
||||
if not all(key in self.rope_scaling for key in required_keys):
|
||||
raise ValueError(f"rope_scaling must contain keys {required_keys}")
|
||||
|
||||
|
||||
class DynamicNTKAlphaRoPE(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dims: int,
|
||||
base: float = 10000,
|
||||
scaling_alpha: float = 1.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.dims = dims
|
||||
base = base * scaling_alpha ** (dims / (dims - 2))
|
||||
self._freqs = base ** (mx.arange(0, self.dims, 2) / self.dims)
|
||||
|
||||
def __call__(self, x, offset: int = 0):
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
self.dims,
|
||||
traditional=False,
|
||||
base=None,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
freqs=self._freqs,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
head_dim = (
|
||||
args.head_dim if args.head_dim is not None else args.hidden_size // n_heads
|
||||
)
|
||||
self.head_dim = 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.use_qk_norm = args.use_qk_norm
|
||||
if self.use_qk_norm:
|
||||
self.query_layernorm = nn.RMSNorm(head_dim, args.rms_norm_eps)
|
||||
self.key_layernorm = nn.RMSNorm(head_dim, args.rms_norm_eps)
|
||||
|
||||
scaling_alpha = 1.0
|
||||
if args.rope_scaling and "alpha" in args.rope_scaling:
|
||||
scaling_alpha = args.rope_scaling["alpha"]
|
||||
|
||||
self.rope = DynamicNTKAlphaRoPE(
|
||||
head_dim,
|
||||
base=args.rope_theta,
|
||||
scaling_alpha=scaling_alpha,
|
||||
)
|
||||
|
||||
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, 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
|
||||
)
|
||||
|
||||
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 self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(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)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
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,
|
||||
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))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class HunyuanV1DenseModel(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 = [TransformerBlock(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,
|
||||
cache=None,
|
||||
):
|
||||
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 = HunyuanV1DenseModel(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:
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
@@ -193,17 +193,14 @@ class InternLM2Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.tok_embeddings(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -222,10 +219,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.tok_embeddings.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -193,17 +193,14 @@ class InternLM2Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -222,10 +219,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
# 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 .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
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(nn.silu(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(nn.silu(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(MambaCache())
|
||||
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,575 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .gated_delta import gated_delta_update
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
intermediate_size: int
|
||||
head_dim: int
|
||||
rope_theta: float
|
||||
rms_norm_eps: float
|
||||
linear_attn_config: Dict[str, Any]
|
||||
model_max_length: int
|
||||
num_experts: int
|
||||
moe_intermediate_size: int
|
||||
kv_lora_rank: int
|
||||
rope_scaling: Optional[Dict[str, Any]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
qk_nope_head_dim: Optional[int] = None
|
||||
qk_rope_head_dim: Optional[int] = None
|
||||
v_head_dim: Optional[int] = None
|
||||
mla_use_nope: bool = False
|
||||
num_experts_per_token: int = 1
|
||||
num_shared_experts: int = 0
|
||||
moe_router_activation_func: str = "sigmoid"
|
||||
moe_renormalize: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
first_k_dense_replace: int = 0
|
||||
moe_layer_freq: int = 1
|
||||
use_grouped_topk: bool = True
|
||||
num_expert_group: int = 1
|
||||
topk_group: int = 1
|
||||
|
||||
|
||||
class KimiMLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
hidden_size: Optional[int] = None,
|
||||
intermediate_size: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
dim = hidden_size or args.hidden_size
|
||||
hidden = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.down_proj = nn.Linear(hidden, dim, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
@mx.compile
|
||||
def _group_expert_select(
|
||||
gates: mx.array,
|
||||
bias: Optional[mx.array],
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
renormalize: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates)
|
||||
elif score_function == "softmax":
|
||||
scores = mx.softmax(gates, axis=-1, precise=True)
|
||||
else:
|
||||
raise ValueError(f"Unsupported MoE router activation '{score_function}'")
|
||||
|
||||
orig_scores = scores
|
||||
if bias is not None:
|
||||
scores = scores + bias.astype(scores.dtype)
|
||||
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores,
|
||||
mx.stop_gradient(group_idx),
|
||||
mx.array(0.0, dtype=scores.dtype),
|
||||
axis=-2,
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
inds = mx.argpartition(-scores, kth=top_k - 1, axis=-1)[..., :top_k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
if top_k > 1 and renormalize:
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
|
||||
return inds, scores * routed_scaling_factor
|
||||
|
||||
|
||||
class KimiSparseMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
hidden = args.hidden_size
|
||||
experts = args.num_experts
|
||||
if experts is None:
|
||||
raise ValueError("num_experts must be specified for MoE layers")
|
||||
|
||||
self.gate = nn.Linear(hidden, experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(hidden, args.moe_intermediate_size, experts)
|
||||
self.e_score_correction_bias = mx.zeros((experts,), dtype=mx.float32)
|
||||
|
||||
if args.num_shared_experts:
|
||||
shared_hidden = args.moe_intermediate_size * args.num_shared_experts
|
||||
self.shared_experts = KimiMLP(args, intermediate_size=shared_hidden)
|
||||
else:
|
||||
self.shared_experts = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
scores = self.gate(x)
|
||||
inds, weights = _group_expert_select(
|
||||
scores,
|
||||
self.e_score_correction_bias,
|
||||
self.args.num_experts_per_token,
|
||||
self.args.num_expert_group,
|
||||
self.args.topk_group,
|
||||
self.args.routed_scaling_factor,
|
||||
self.args.moe_renormalize,
|
||||
self.args.moe_router_activation_func,
|
||||
)
|
||||
out = self.switch_mlp(x, inds)
|
||||
out = (out * weights[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class KimiMLAAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.qk_nope_head_dim = args.qk_nope_head_dim or args.head_dim
|
||||
self.qk_rope_head_dim = args.qk_rope_head_dim or 0
|
||||
self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
self.v_head_dim = args.v_head_dim or args.head_dim
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
hidden = args.hidden_size
|
||||
self.q_proj = nn.Linear(hidden, self.num_heads * self.q_head_dim, bias=False)
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
hidden,
|
||||
args.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=False,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(args.kv_lora_rank, eps=args.rms_norm_eps)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
args.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, hidden, bias=False)
|
||||
|
||||
rope_dim = self.qk_rope_head_dim or self.q_head_dim
|
||||
self.rope = initialize_rope(
|
||||
rope_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.model_max_length,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
q_states = self.q_proj(x).reshape(B, L, self.num_heads, self.q_head_dim)
|
||||
q_pass, q_rot = mx.split(q_states, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
compressed = self.kv_a_proj_with_mqa(x)
|
||||
k_pass, k_rot = mx.split(
|
||||
compressed, [compressed.shape[-1] - self.qk_rope_head_dim], axis=-1
|
||||
)
|
||||
k_pass = self.kv_a_layernorm(k_pass)
|
||||
kv = self.kv_b_proj(k_pass)
|
||||
kv = kv.reshape(
|
||||
B,
|
||||
L,
|
||||
self.num_heads,
|
||||
self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim,
|
||||
)
|
||||
k_pass, v_states = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if self.qk_rope_head_dim:
|
||||
k_rot = mx.reshape(k_rot, (B, L, 1, self.qk_rope_head_dim))
|
||||
k_rot = mx.broadcast_to(k_rot, (*k_pass.shape[:-1], self.qk_rope_head_dim))
|
||||
else:
|
||||
k_rot = mx.zeros((*k_pass.shape[:-1], 0), dtype=k_pass.dtype)
|
||||
|
||||
queries = mx.concatenate([q_pass, q_rot], axis=-1).transpose(0, 2, 1, 3)
|
||||
keys = mx.concatenate([k_pass, k_rot], axis=-1).transpose(0, 2, 1, 3)
|
||||
values = v_states.transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
out = scaled_dot_product_attention(
|
||||
queries,
|
||||
keys,
|
||||
values,
|
||||
cache,
|
||||
scale=self.scale,
|
||||
mask=mask,
|
||||
)
|
||||
out = out.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(out)
|
||||
|
||||
|
||||
class ShortConv1d(nn.Module):
|
||||
def __init__(self, channels: int, kernel_size: int):
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=channels,
|
||||
out_channels=channels,
|
||||
kernel_size=kernel_size,
|
||||
bias=False,
|
||||
groups=channels,
|
||||
padding=0,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, cache: Optional[mx.array]
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if cache is None:
|
||||
pad = mx.zeros(
|
||||
(x.shape[0], self.kernel_size - 1, x.shape[-1]), dtype=x.dtype
|
||||
)
|
||||
else:
|
||||
pad = cache
|
||||
conv_input = mx.concatenate([pad, x], axis=1)
|
||||
out = nn.silu(self.conv(conv_input))
|
||||
new_cache = conv_input[:, -self.kernel_size + 1 :, :]
|
||||
return out, new_cache
|
||||
|
||||
|
||||
class KimiDeltaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
cfg = args.linear_attn_config
|
||||
|
||||
self.layer_idx = layer_idx
|
||||
self.num_heads = cfg["num_heads"]
|
||||
self.head_dim = cfg["head_dim"]
|
||||
self.conv_kernel = cfg.get("short_conv_kernel_size", 4)
|
||||
|
||||
self.projection_dim = self.num_heads * self.head_dim
|
||||
hidden = args.hidden_size
|
||||
|
||||
self.scale = float(self.head_dim) ** -0.5
|
||||
|
||||
self.q_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.k_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.v_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
|
||||
self.q_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.k_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.v_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
|
||||
self.f_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.f_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
self.b_proj = nn.Linear(hidden, self.num_heads, bias=False)
|
||||
|
||||
self.g_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.g_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
|
||||
self.A_log = mx.expand_dims(
|
||||
mx.log(mx.random.uniform(low=1.0, high=16.0, shape=(self.num_heads,))),
|
||||
(0, 1, 3),
|
||||
)
|
||||
self.dt_bias = mx.zeros((self.projection_dim,))
|
||||
|
||||
self.o_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.o_proj = nn.Linear(self.projection_dim, hidden, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, T, _ = x.shape
|
||||
dtype = x.dtype
|
||||
|
||||
if cache is not None:
|
||||
conv_state, ssm_state = cache
|
||||
else:
|
||||
conv_state = None
|
||||
ssm_state = None
|
||||
|
||||
if conv_state is None:
|
||||
s = mx.zeros((B, self.conv_kernel - 1, self.projection_dim), dtype=dtype)
|
||||
q_state = s
|
||||
k_state = s
|
||||
v_state = s
|
||||
else:
|
||||
q_state, k_state, v_state = conv_state
|
||||
|
||||
q_conv, q_state = self.q_conv(self.q_proj(x), q_state)
|
||||
k_conv, k_state = self.k_conv(self.k_proj(x), k_state)
|
||||
v_conv, v_state = self.v_conv(self.v_proj(x), v_state)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = (q_state, k_state, v_state)
|
||||
|
||||
q = q_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
k = k_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
v = v_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
|
||||
def _l2norm(x, eps=1e-6):
|
||||
norm = mx.linalg.norm(x, axis=-1, keepdims=True)
|
||||
return x / (norm + eps)
|
||||
|
||||
q = _l2norm(q)
|
||||
k = _l2norm(k)
|
||||
q = q * self.scale
|
||||
|
||||
a_logits = self.f_b_proj(self.f_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
b_logits = self.b_proj(x).reshape(B, T, self.num_heads)
|
||||
|
||||
out, ssm_state = gated_delta_update(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a_logits,
|
||||
b_logits,
|
||||
self.A_log.reshape(self.num_heads, 1),
|
||||
self.dt_bias.reshape(self.num_heads, self.head_dim),
|
||||
state=ssm_state,
|
||||
mask=mask,
|
||||
use_kernel=not self.training,
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[1] = ssm_state
|
||||
|
||||
gate = self.g_b_proj(self.g_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
out = (
|
||||
self.o_norm(out.reshape(B, T, self.num_heads, self.head_dim))
|
||||
* mx.sigmoid(gate)
|
||||
).reshape(B, T, -1)
|
||||
return self.o_proj(out)
|
||||
|
||||
|
||||
class KimiDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.is_linear = (layer_idx + 1) in kda_layers
|
||||
|
||||
if self.is_linear:
|
||||
self.self_attn = KimiDeltaAttention(args, layer_idx)
|
||||
else:
|
||||
self.self_attn = KimiMLAAttention(args)
|
||||
|
||||
if (
|
||||
args.num_experts > 0
|
||||
and layer_idx >= args.first_k_dense_replace
|
||||
and layer_idx % args.moe_layer_freq == 0
|
||||
):
|
||||
self.mlp = KimiSparseMoE(args)
|
||||
else:
|
||||
self.mlp = KimiMLP(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
attn_cache = None if cache is None else cache
|
||||
y = self.self_attn(self.input_layernorm(x), mask, attn_cache)
|
||||
h = x + y
|
||||
z = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + z
|
||||
|
||||
|
||||
class KimiLinearModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [KimiDecoderLayer(args, i) for i in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.ssm_idx = kda_layers[0] - 1
|
||||
for i in range(len(self.layers)):
|
||||
if (i + 1) not in kda_layers:
|
||||
self.attn_idx = i
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
|
||||
for layer, layer_cache in zip(self.layers, cache):
|
||||
mask = ssm_mask if layer.is_linear else attn_mask
|
||||
h = layer(h, mask=mask, cache=layer_cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = KimiLinearModel(args)
|
||||
if args.tie_word_embeddings:
|
||||
self.lm_head = None
|
||||
else:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.lm_head is None:
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches: List[Any] = []
|
||||
for layer in self.layers:
|
||||
if layer.is_linear:
|
||||
caches.append(MambaCache())
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
||||
weights = {k: v for k, v in weights.items() if not k.startswith("model.mtp")}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
prefix = f"model.layers.{layer_idx}"
|
||||
|
||||
if isinstance(layer.mlp, KimiSparseMoE):
|
||||
src_prefix = f"{prefix}.block_sparse_moe"
|
||||
dst_prefix = f"{prefix}.mlp"
|
||||
for src, dst in [
|
||||
("w1", "gate_proj"),
|
||||
("w2", "down_proj"),
|
||||
("w3", "up_proj"),
|
||||
]:
|
||||
key = f"{src_prefix}.experts.0.{src}.weight"
|
||||
if key in weights:
|
||||
stacked = [
|
||||
weights.pop(f"{src_prefix}.experts.{i}.{src}.weight")
|
||||
for i in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{dst_prefix}.switch_mlp.{dst}.weight"] = mx.stack(
|
||||
stacked
|
||||
)
|
||||
|
||||
for name in ("gate_proj", "up_proj", "down_proj"):
|
||||
src_key = f"{src_prefix}.shared_experts.{name}.weight"
|
||||
if src_key in weights:
|
||||
weights[f"{dst_prefix}.shared_experts.{name}.weight"] = (
|
||||
weights.pop(src_key)
|
||||
)
|
||||
|
||||
gate_key = f"{src_prefix}.gate.weight"
|
||||
if gate_key in weights:
|
||||
weights[f"{dst_prefix}.gate.weight"] = weights.pop(gate_key)
|
||||
|
||||
bias_key = f"{src_prefix}.gate.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{dst_prefix}.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
attn = getattr(layer, "self_attn", None)
|
||||
if isinstance(attn, KimiDeltaAttention):
|
||||
attn_prefix = f"{prefix}.self_attn"
|
||||
for src_name, dst_name in (
|
||||
("q_conv1d", "q_conv"),
|
||||
("k_conv1d", "k_conv"),
|
||||
("v_conv1d", "v_conv"),
|
||||
):
|
||||
src_key = f"{attn_prefix}.{src_name}.weight"
|
||||
if src_key in weights:
|
||||
w = weights.pop(src_key)
|
||||
if w.ndim == 3:
|
||||
w = w.moveaxis(2, 1)
|
||||
weights[f"{attn_prefix}.{dst_name}.conv.weight"] = w
|
||||
dt_key = f"{attn_prefix}.dt_bias"
|
||||
if dt_key in weights:
|
||||
if weights[dt_key].ndim > 1:
|
||||
weights[dt_key] = mx.reshape(weights[dt_key], (-1,))
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(path: str):
|
||||
if "e_score_correction_bias" in path:
|
||||
return False
|
||||
if path.endswith("A_log") or path.endswith("dt_bias"):
|
||||
return False
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -30,9 +30,9 @@ class TextArgs(BaseModelArgs):
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: Optional[int] = None
|
||||
topk_group: Optional[int] = None
|
||||
num_experts_per_tok: Optional[int] = None
|
||||
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
|
||||
@@ -62,9 +62,8 @@ class LanguageModel(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
|
||||
@@ -79,9 +78,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(inputs, cache, mask)
|
||||
return self.language_model(inputs, cache)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def keep(key):
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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 lfm2
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
text_config: dict
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = lfm2.Model(lfm2.ModelArgs.from_dict(args.text_config))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights.pop("multi_modal_projector", None)
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return self.language_model.make_cache()
|
||||
+35
-12
@@ -5,7 +5,12 @@ from typing import Any, List, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
|
||||
|
||||
@@ -26,8 +31,19 @@ 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
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
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):
|
||||
@@ -114,13 +130,14 @@ class ShortConv(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
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]
|
||||
@@ -194,6 +211,7 @@ class Lfm2DecoderLayer(nn.Module):
|
||||
else:
|
||||
r = self.conv(
|
||||
self.operator_norm(x),
|
||||
mask=mask,
|
||||
cache=cache,
|
||||
)
|
||||
h = x + r
|
||||
@@ -214,10 +232,17 @@ class Lfm2Model(nn.Module):
|
||||
|
||||
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,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
@@ -226,15 +251,14 @@ class Lfm2Model(nn.Module):
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
first_attn_idx = self.args.full_attn_idxs[0]
|
||||
c = [cache[first_attn_idx]] if cache is not None else None
|
||||
mask = create_attention_mask(h, c)
|
||||
|
||||
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)
|
||||
@@ -250,11 +274,10 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache, input_embeddings)
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
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
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
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)
|
||||
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) :]
|
||||
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(nn.silu(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
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
block_size: int
|
||||
layer_norm_eps: float
|
||||
n_embd: int
|
||||
n_head: int
|
||||
n_kv_heads: int
|
||||
n_layer: int
|
||||
rope_theta: float
|
||||
vocab_size: int
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class Lille130mAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.n_head = args.n_head
|
||||
self.n_kv_heads = args.n_kv_heads
|
||||
self.head_dim = args.n_embd // args.n_head
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.qkv_proj = nn.Linear(
|
||||
args.n_embd, (args.n_head + 2 * args.n_kv_heads) * self.head_dim, bias=False
|
||||
)
|
||||
self.out_proj = nn.Linear(args.n_head * self.head_dim, args.n_embd, bias=False)
|
||||
|
||||
self.norm = nn.RMSNorm(args.n_embd, eps=args.layer_norm_eps)
|
||||
|
||||
self.rope = nn.RoPE(args.n_embd // args.n_head, True, 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
|
||||
|
||||
qkv = self.qkv_proj(self.norm(x))
|
||||
|
||||
q_size = self.n_head * self.head_dim
|
||||
kv_size = self.n_kv_heads * self.head_dim
|
||||
|
||||
queries, keys, values = mx.split(qkv, [q_size, q_size + kv_size], axis=-1)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_head, -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
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
class Lille130mMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
hidden_dim = 256 * round(int(8 * args.n_embd / 3) / 256)
|
||||
|
||||
self.norm = nn.RMSNorm(args.n_embd, eps=args.layer_norm_eps)
|
||||
self.gate_proj = nn.Linear(args.n_embd, hidden_dim, bias=False)
|
||||
self.up_proj = nn.Linear(args.n_embd, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, args.n_embd, bias=False)
|
||||
|
||||
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))
|
||||
|
||||
|
||||
class Lille130Block(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.attention = Lille130mAttention(args)
|
||||
self.feed_forward = Lille130mMLP(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = x + self.attention(x, mask, cache)
|
||||
out = h + self.feed_forward(h)
|
||||
return out
|
||||
|
||||
|
||||
class Lille130(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.tok_embeddings = nn.Embedding(args.vocab_size, args.n_embd)
|
||||
self.layers = [Lille130Block(args=args) for _ in range(args.n_layer)]
|
||||
self.norm = nn.RMSNorm(args.n_embd, eps=args.layer_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.tok_embeddings(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, cache=c)
|
||||
|
||||
return self.tok_embeddings.as_linear(self.norm(h))
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.transformer = Lille130(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
return self.transformer(inputs, cache=cache)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.transformer.layers
|
||||
|
||||
def sanitize(self, weights):
|
||||
return {k: v for k, v in weights.items() if "rotary_emb" not in k}
|
||||
+39
-11
@@ -1,12 +1,13 @@
|
||||
# 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@@ -28,11 +29,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):
|
||||
@@ -114,10 +120,11 @@ class MLP(nn.Module):
|
||||
|
||||
|
||||
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,17 +152,25 @@ 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,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
@@ -164,14 +179,18 @@ class LlamaModel(nn.Module):
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
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, 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)
|
||||
|
||||
@@ -188,11 +207,10 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache, input_embeddings)
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
@@ -211,3 +229,13 @@ class Model(nn.Module):
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
|
||||
+7
-16
@@ -17,7 +17,6 @@ class TextArgs(BaseModelArgs):
|
||||
attention_bias: bool
|
||||
attention_chunk_size: int
|
||||
head_dim: int
|
||||
hidden_act: str
|
||||
hidden_size: int
|
||||
interleave_moe_layer_step: int
|
||||
intermediate_size: int
|
||||
@@ -153,6 +152,7 @@ class MoE(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.top_k = args.num_experts_per_tok
|
||||
assert self.top_k == 1, "Only 1 expert per token supported"
|
||||
self.num_experts = args.num_local_experts
|
||||
self.experts = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, self.num_experts
|
||||
@@ -219,7 +219,6 @@ class LlamaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
@@ -242,21 +241,15 @@ class LlamaModel(nn.Module):
|
||||
token_pos = linds <= rinds
|
||||
chunk_mask = (block_pos == 0) & token_pos
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
else:
|
||||
chunk_mask &= mask
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
global_mask = create_attention_mask(h, cache[3])
|
||||
|
||||
for idx, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
use_chunked_attention = (idx + 1) % 4 != 0
|
||||
if use_chunked_attention:
|
||||
local_mask = chunk_mask
|
||||
else:
|
||||
local_mask = mask
|
||||
h = layer(h, local_mask, cache=c)
|
||||
mask = chunk_mask if use_chunked_attention else global_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -274,10 +267,9 @@ class LanguageModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
|
||||
@@ -291,10 +283,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
return self.language_model(inputs, mask, cache)
|
||||
return self.language_model(inputs, cache)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def to_remove(k):
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_attention_heads: int
|
||||
num_hidden_layers: int
|
||||
vocab_size: int
|
||||
intermediate_size: int
|
||||
intermediate_size_mlp: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
head_dim: int
|
||||
tie_word_embeddings: bool
|
||||
no_rope_layers: list
|
||||
use_qk_norm: bool
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, use_rope):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
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(
|
||||
args.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
self.use_rope = use_rope
|
||||
if use_rope:
|
||||
self.rope = nn.RoPE(self.head_dim, traditional=True, base=args.rope_theta)
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.rms_norm_eps = args.rms_norm_eps
|
||||
|
||||
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)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, -1)
|
||||
if self.use_qk_norm:
|
||||
queries = mx.fast.rms_norm(queries, None, self.rms_norm_eps)
|
||||
keys = mx.fast.rms_norm(keys, None, self.rms_norm_eps)
|
||||
queries = queries.transpose(0, 2, 1, 3)
|
||||
keys = keys.transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if self.use_rope:
|
||||
offset = cache.offset if cache is not None else 0
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, 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)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, dim, intermediate_size, activation=nn.silu):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(dim, intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(dim, intermediate_size, bias=False)
|
||||
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))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, use_rope):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(args, use_rope)
|
||||
|
||||
self.feed_forward = MLP(
|
||||
args.hidden_size,
|
||||
args.intermediate_size_mlp,
|
||||
)
|
||||
|
||||
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.feed_forward(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class LanguageModel(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 = [
|
||||
TransformerBlock(args=args, use_rope=args.no_rope_layers[i])
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
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)
|
||||
|
||||
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 = LanguageModel(args)
|
||||
|
||||
self.tie_word_embeddings = args.tie_word_embeddings
|
||||
if not self.tie_word_embeddings:
|
||||
self.output = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.model(inputs, cache)
|
||||
if self.tie_word_embeddings:
|
||||
return h @ self.model.embed_tokens.weight.T
|
||||
else:
|
||||
return self.output(h)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,381 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
attention_method: str
|
||||
zero_expert_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
|
||||
norm_topk_prob: bool = False
|
||||
router_bias: bool = False
|
||||
|
||||
|
||||
class LongcatFlashMLA(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.qk_rope_head_dim = args.qk_rope_head_dim
|
||||
self.qk_nope_head_dim = args.qk_nope_head_dim
|
||||
self.kv_lora_rank = args.kv_lora_rank
|
||||
self.q_lora_rank = args.q_lora_rank
|
||||
self.v_head_dim = args.v_head_dim
|
||||
|
||||
self.qk_head_dim = args.qk_nope_head_dim + args.qk_rope_head_dim
|
||||
self.scale = self.qk_head_dim**-0.5
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
self.num_attention_heads * self.qk_head_dim,
|
||||
bias=False,
|
||||
)
|
||||
else:
|
||||
self.q_a_proj = nn.Linear(
|
||||
args.hidden_size, self.q_lora_rank, bias=args.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank,
|
||||
self.num_attention_heads * self.qk_head_dim,
|
||||
bias=False,
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
args.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
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.o_proj = nn.Linear(
|
||||
self.num_attention_heads * args.v_head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
|
||||
if args.mla_scale_q_lora:
|
||||
self.mla_scale_q_lora = (args.hidden_size / self.q_lora_rank) ** 0.5
|
||||
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
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q_states = self.q_proj(x)
|
||||
else:
|
||||
q_states = 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,
|
||||
)
|
||||
|
||||
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(attn_output)
|
||||
|
||||
|
||||
class LongcatFlashMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, is_expert: bool = False):
|
||||
super().__init__()
|
||||
hidden_size = args.expert_ffn_hidden_size if is_expert else args.ffn_hidden_size
|
||||
|
||||
self.gate_proj = nn.Linear(args.hidden_size, hidden_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, hidden_size, bias=False)
|
||||
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))
|
||||
|
||||
|
||||
class LongcatFlashTopkRouter(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = args
|
||||
self.top_k = args.moe_topk
|
||||
self.n_routed_experts = args.n_routed_experts + args.zero_expert_num
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.router_bias = args.router_bias
|
||||
|
||||
self.classifier = nn.Linear(
|
||||
args.hidden_size, self.n_routed_experts, bias=self.router_bias
|
||||
)
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
|
||||
def __call__(self, hidden_states: mx.array) -> Tuple[mx.array, mx.array]:
|
||||
|
||||
dtype = hidden_states.dtype
|
||||
router_logits = self.classifier(hidden_states)
|
||||
scores = mx.softmax(router_logits, axis=-1)
|
||||
|
||||
corrected_scores = scores + self.e_score_correction_bias
|
||||
topk_indices = mx.argpartition(corrected_scores, kth=-self.top_k, axis=-1)[
|
||||
..., -self.top_k :
|
||||
]
|
||||
topk_weights = mx.take_along_axis(scores, topk_indices, axis=-1)
|
||||
|
||||
if self.norm_topk_prob:
|
||||
denominator = mx.sum(topk_weights, axis=-1, keepdims=True) + 1e-20
|
||||
topk_weights = topk_weights / denominator
|
||||
|
||||
topk_weights = topk_weights * self.routed_scaling_factor
|
||||
|
||||
return topk_indices, topk_weights.astype(dtype)
|
||||
|
||||
|
||||
class LongcatFlashMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = args
|
||||
self.num_experts_per_tok = args.moe_topk
|
||||
self.n_routed_experts = args.n_routed_experts
|
||||
self.zero_expert_num = args.zero_expert_num
|
||||
self.zero_expert_type = args.zero_expert_type
|
||||
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.expert_ffn_hidden_size,
|
||||
args.n_routed_experts,
|
||||
)
|
||||
|
||||
self.router = LongcatFlashTopkRouter(args)
|
||||
|
||||
def __call__(self, hidden_states):
|
||||
|
||||
topk_indices, topk_weights = self.router(hidden_states)
|
||||
|
||||
# Process all regular experts at once
|
||||
mask = topk_indices >= self.n_routed_experts
|
||||
topk_indices = mx.where(mask, 0, topk_indices)
|
||||
regular_weights = mx.where(mask, 0.0, topk_weights)
|
||||
|
||||
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
|
||||
|
||||
weighted_outputs = regular_outputs * regular_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
|
||||
|
||||
final_output = mx.sum(weighted_outputs, axis=-2)
|
||||
return final_output
|
||||
|
||||
|
||||
class LongcatFlashDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.mlp = LongcatFlashMoE(args)
|
||||
|
||||
self.self_attn = [LongcatFlashMLA(args) for _ in range(2)]
|
||||
self.mlps = [LongcatFlashMLP(args, False) for _ in range(2)]
|
||||
self.input_layernorm = [
|
||||
nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) for _ in range(2)
|
||||
]
|
||||
self.post_attention_layernorm = [
|
||||
nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) for _ in range(2)
|
||||
]
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
hidden_states = x
|
||||
shortcut_mlp_output = None
|
||||
|
||||
if cache is None:
|
||||
cache = (None, None)
|
||||
|
||||
for i in range(2):
|
||||
residual = hidden_states
|
||||
|
||||
hidden_states = self.input_layernorm[i](hidden_states)
|
||||
hidden_states = self.self_attn[i](hidden_states, mask=mask, cache=cache[i])
|
||||
hidden_states = residual + hidden_states
|
||||
|
||||
residual = hidden_states
|
||||
hidden_states = self.post_attention_layernorm[i](hidden_states)
|
||||
|
||||
if i == 0:
|
||||
shortcut_mlp_output = self.mlp(hidden_states)
|
||||
|
||||
hidden_states = self.mlps[i](hidden_states)
|
||||
hidden_states = residual + hidden_states
|
||||
|
||||
if i == 1:
|
||||
hidden_states = hidden_states + shortcut_mlp_output
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
class LongcatFlashModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_layers = args.num_layers
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [LongcatFlashDecoderLayer(args) for idx in range(args.num_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
if cache is None:
|
||||
cache = [(None, None)] * self.num_layers
|
||||
|
||||
mask = create_attention_mask(h, cache[0][0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
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 = LongcatFlashModel(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):
|
||||
def predicate(path, _):
|
||||
if path.endswith("classifier"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def sanitize(self, weights):
|
||||
for l in range(self.args.num_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)
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if k.startswith("model.mtp"):
|
||||
continue
|
||||
new_weights[k] = v
|
||||
return new_weights
|
||||
|
||||
def make_cache(self):
|
||||
return [CacheList(KVCache(), KVCache()) for _ in self.model.layers]
|
||||
+18
-40
@@ -50,32 +50,6 @@ class ModelArgs(BaseModelArgs):
|
||||
self.use_bcdt_rms = True
|
||||
|
||||
|
||||
class DepthWiseConv1d(nn.Module):
|
||||
def __init__(self, channels, kernel_size, bias=True, padding=0):
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.kernel_size = kernel_size
|
||||
self.padding = padding
|
||||
self.weight = mx.random.normal((self.channels, kernel_size, 1))
|
||||
self.bias = mx.zeros((channels,)) if bias else None
|
||||
|
||||
def __call__(self, x, cache=None):
|
||||
B, L, C = x.shape
|
||||
groups, K, _ = self.weight.shape
|
||||
|
||||
if cache is not None:
|
||||
x = mx.concatenate([cache, x], axis=1)
|
||||
else:
|
||||
x = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
|
||||
|
||||
y = mx.conv_general(x, self.weight, groups=groups)
|
||||
|
||||
if self.bias is not None:
|
||||
y = y + self.bias
|
||||
|
||||
return y, x[:, -K + 1 :, :]
|
||||
|
||||
|
||||
class MambaBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -97,11 +71,13 @@ class MambaBlock(nn.Module):
|
||||
self.hidden_size, self.intermediate_size * 2, bias=args.use_bias
|
||||
)
|
||||
|
||||
self.conv1d = DepthWiseConv1d(
|
||||
channels=self.intermediate_size,
|
||||
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=self.conv_kernel_size - 1,
|
||||
padding=0,
|
||||
)
|
||||
|
||||
self.x_proj = nn.Linear(
|
||||
@@ -148,13 +124,15 @@ class MambaBlock(nn.Module):
|
||||
B, T, D = x.shape
|
||||
xz = self.in_proj(x)
|
||||
x, z = xz.split(indices_or_sections=2, axis=-1)
|
||||
|
||||
conv_out, new_conv_cache = self.conv1d(x, conv_cache)
|
||||
K = self.conv_kernel_size
|
||||
if conv_cache is not None:
|
||||
x_full = mx.concatenate([conv_cache, x], axis=1)
|
||||
else:
|
||||
x_full = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
|
||||
conv_out = self.conv1d(x_full)
|
||||
new_conv_cache = x_full[:, -(K - 1) :, :]
|
||||
x = nn.silu(conv_out)
|
||||
|
||||
A = -mx.exp(self.A_log)
|
||||
|
||||
outputs = []
|
||||
current_state = state_cache
|
||||
y = []
|
||||
for t in range(T):
|
||||
@@ -228,15 +206,15 @@ class Model(nn.Module):
|
||||
|
||||
return logits
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
return weights
|
||||
|
||||
def make_cache(self):
|
||||
return [MambaCache() for _ in range(len(self.layers))]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.backbone.layers
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
return weights
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_ssm_mask
|
||||
from .cache import MambaCache
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
num_heads: int
|
||||
head_dim: int
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
state_size: int
|
||||
num_hidden_layers: int
|
||||
layer_norm_epsilon: float
|
||||
conv_kernel: int
|
||||
n_groups: int
|
||||
use_bias: bool
|
||||
use_conv_bias: bool
|
||||
tie_word_embeddings: bool
|
||||
time_step_limit: Tuple[float, float]
|
||||
time_step_rank: Union[int, str]
|
||||
ssm_state_size: Optional[int] = None
|
||||
max_position_embeddings: int = 2056
|
||||
|
||||
def __post_init__(self):
|
||||
if self.time_step_rank == "auto":
|
||||
self.time_step_rank = math.ceil(self.hidden_size / 16)
|
||||
if self.ssm_state_size is None:
|
||||
self.ssm_state_size = self.state_size
|
||||
|
||||
|
||||
class MambaRMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = mx.ones(hidden_size)
|
||||
|
||||
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)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
class Mamba2Block(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
self.num_heads = args.num_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.ssm_state_size
|
||||
self.conv_kernel_size = args.conv_kernel
|
||||
self.intermediate_size = args.num_heads * args.head_dim
|
||||
self.use_conv_bias = args.use_conv_bias
|
||||
self.n_groups = args.n_groups
|
||||
self.head_dim = args.head_dim
|
||||
self.time_step_limit = args.time_step_limit
|
||||
self.heads_per_group = self.num_heads // self.n_groups
|
||||
self.use_bias = args.use_bias
|
||||
|
||||
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,
|
||||
kernel_size=args.conv_kernel,
|
||||
padding=0,
|
||||
groups=self.conv_dim,
|
||||
bias=args.use_conv_bias,
|
||||
)
|
||||
|
||||
projection_size = self.intermediate_size + self.conv_dim + self.num_heads
|
||||
self.in_proj = nn.Linear(self.hidden_size, projection_size, bias=args.use_bias)
|
||||
|
||||
self.dt_bias = mx.ones(self.num_heads)
|
||||
self.A_log = mx.log(mx.arange(1, self.num_heads + 1, dtype=mx.float32))
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.norm = MambaRMSNormGated(
|
||||
self.intermediate_size, eps=args.layer_norm_epsilon
|
||||
)
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
) -> mx.array:
|
||||
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)
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :, :]
|
||||
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,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> 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)
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[MambaCache] = None,
|
||||
) -> mx.array:
|
||||
projected = self.in_proj(hidden_states)
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected,
|
||||
[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)
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states, B, C, dt, state, mask=mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class ResidualBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.mixer = Mamba2Block(args, layer_idx)
|
||||
self.norm = nn.RMSNorm(args.hidden_size)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, mask: Optional[mx.array], cache: Optional[MambaCache] = None
|
||||
) -> mx.array:
|
||||
output = self.mixer(self.norm(x), mask, cache)
|
||||
return output + x
|
||||
|
||||
|
||||
class Mamba2(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [ResidualBlock(args, i) for i in range(args.num_hidden_layers)]
|
||||
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.layer_norm_epsilon)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, cache: Optional[list[MambaCache]] = None
|
||||
) -> mx.array:
|
||||
hidden = self.embeddings(x)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_ssm_mask(hidden, cache[0])
|
||||
for layer, c in zip(self.layers, cache):
|
||||
hidden = layer(hidden, mask, c)
|
||||
|
||||
return self.norm_f(hidden)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.backbone = Mamba2(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[list[MambaCache]] = None
|
||||
) -> mx.array:
|
||||
hidden = self.backbone(inputs, cache)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
logits = self.backbone.embeddings.as_linear(hidden)
|
||||
else:
|
||||
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)]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.backbone.layers
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
return weights
|
||||
@@ -137,17 +137,15 @@ class MiMoModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -168,10 +166,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
|
||||
@@ -154,17 +154,15 @@ class MiniCPMModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs) * self.args.scale_emb
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -184,10 +182,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
|
||||
if not self.args.tie_word_embeddings:
|
||||
out = self.lm_head(out / (self.args.hidden_size / self.args.dim_model_base))
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
num_experts_per_tok: int
|
||||
num_local_experts: int
|
||||
shared_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
rotary_dim: int
|
||||
vocab_size: int
|
||||
tie_word_embeddings: bool = False
|
||||
scoring_func: str = "sigmoid"
|
||||
head_dim: Optional[int] = None
|
||||
use_qk_norm: bool = True
|
||||
|
||||
|
||||
class MiniMaxAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_dim = hidden_size = args.hidden_size
|
||||
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = head_dim = (
|
||||
args.head_dim or hidden_size // args.num_attention_heads
|
||||
)
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size, self.num_attention_heads * head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_attention_heads * head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.use_qk_norm = args.use_qk_norm if hasattr(args, "use_qk_norm") else False
|
||||
if self.use_qk_norm:
|
||||
self.q_norm = nn.RMSNorm(
|
||||
head_dim * self.num_attention_heads, eps=args.rms_norm_eps
|
||||
)
|
||||
self.k_norm = nn.RMSNorm(
|
||||
head_dim * self.num_key_value_heads, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
self.rope = nn.RoPE(args.rotary_dim, traditional=False, base=args.rope_theta)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MiniMaxSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_local_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, args.num_local_experts
|
||||
)
|
||||
self.e_score_correction_bias = mx.zeros((args.num_local_experts,))
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.gate(x.astype(mx.float32))
|
||||
|
||||
scores = mx.sigmoid(gates)
|
||||
orig_scores = scores
|
||||
scores = scores + self.e_score_correction_bias
|
||||
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
scores = scores / (mx.sum(scores, axis=-1, keepdims=True) + 1e-20)
|
||||
scores = scores.astype(x.dtype)
|
||||
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
return y
|
||||
|
||||
|
||||
class MiniMaxDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.self_attn = MiniMaxAttention(args)
|
||||
|
||||
self.block_sparse_moe = MiniMaxSparseMoeBlock(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = x + self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
r = r + self.block_sparse_moe(self.post_attention_layernorm(r))
|
||||
return r
|
||||
|
||||
|
||||
class MiniMaxModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [
|
||||
MiniMaxDecoderLayer(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = MiniMaxModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs=inputs, mask=mask, cache=cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
"""Dequantize FP8 weights and restructure MoE experts."""
|
||||
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
pad_side = (-n) % bs
|
||||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||||
weight = weight.reshape(
|
||||
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
|
||||
)
|
||||
weight = (weight * scale_inv[:, None, :, None]).reshape(
|
||||
m + pad_bottom, n + pad_side
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Step 2: Handle MoE expert weights restructuring
|
||||
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
|
||||
return weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
mapping = {"w1": "gate_proj", "w2": "down_proj", "w3": "up_proj"}
|
||||
for orig_name, new_name in mapping.items():
|
||||
if f"{prefix}.block_sparse_moe.experts.0.{orig_name}.weight" in weights:
|
||||
to_join = [
|
||||
weights.pop(
|
||||
f"{prefix}.block_sparse_moe.experts.{e}.{orig_name}.weight"
|
||||
)
|
||||
for e in range(self.args.num_local_experts)
|
||||
]
|
||||
weights[
|
||||
f"{prefix}.block_sparse_moe.switch_mlp.{new_name}.weight"
|
||||
] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("block_sparse_moe.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,264 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
head_dim: Optional[int] = None
|
||||
max_position_embeddings: Optional[int] = None
|
||||
num_key_value_heads: Optional[int] = None
|
||||
rope_parameters: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
layer_types: Optional[List[str]] = None
|
||||
sliding_window: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
|
||||
if self.layer_types is None:
|
||||
self.layer_types = ["full_attention"] * self.num_hidden_layers
|
||||
|
||||
|
||||
def _get_llama_4_attn_scale(
|
||||
start: int, stop: int, beta: float, max_position_embeddings: int
|
||||
):
|
||||
scaling = 1 + beta * mx.log(
|
||||
1 + mx.floor(mx.arange(start, stop) / max_position_embeddings)
|
||||
)
|
||||
return scaling[:, None]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.head_dim or args.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_parameters["rope_theta"],
|
||||
False,
|
||||
args.rope_parameters,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
attn_scale: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
# Prepare the queries, keys and values for the attention computation
|
||||
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
offset = 0
|
||||
if cache is not None:
|
||||
offset = cache.offset
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
queries = queries * attn_scale
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.args = args
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
attn_scale: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), attn_scale, mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
|
||||
for layer_type in self.layer_types
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for e, l in enumerate(self.layers):
|
||||
if l.use_sliding:
|
||||
self.swa_idx = e
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
offset = 0
|
||||
else:
|
||||
offset = cache[0].offset
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
attn_scale = _get_llama_4_attn_scale(
|
||||
offset,
|
||||
offset + inputs.shape[1],
|
||||
self.args.rope_parameters["llama_4_scaling_beta"],
|
||||
self.args.rope_parameters["original_max_position_embeddings"],
|
||||
).astype(h.dtype)
|
||||
|
||||
for layer, cache in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, attn_scale, mask, cache=cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = LanguageModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove unused precomputed rotary freqs
|
||||
weights = {
|
||||
k: v for k, v in weights.items() if "self_attn.rotary_emb.inv_freq" not in k
|
||||
}
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
new_weights[wk] = weight * scale_inv
|
||||
elif "activation_scale" in k:
|
||||
continue
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
@@ -7,7 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import llama
|
||||
from . import llama, ministral3
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class ModelArgs(BaseModelArgs):
|
||||
text_config: dict
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
if "tie_word_embeddings" not in self.text_config:
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
@@ -25,23 +26,31 @@ 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,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
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
|
||||
|
||||
@@ -161,17 +161,15 @@ class MixtralModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -189,10 +187,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "nanochat"
|
||||
hidden_size: int = 1280
|
||||
num_hidden_layers: int = 20
|
||||
num_attention_heads: int = 10
|
||||
num_key_value_heads: int = 10
|
||||
vocab_size: int = 65536
|
||||
max_position_embeddings: int = 2048
|
||||
intermediate_size: int = 5120 # 4 * hidden_size
|
||||
rope_theta: float = 10000.0
|
||||
|
||||
|
||||
def rms_norm(x):
|
||||
"""Functional RMSNorm with no learnable parameters."""
|
||||
return mx.fast.rms_norm(x, None, 1e-5)
|
||||
|
||||
|
||||
def apply_rotary_emb(x, offset, base=10000.0, freqs=None):
|
||||
"""Apply RoPE with blocked layout.
|
||||
|
||||
|
||||
Args:
|
||||
x: Input tensor in (B, H, T, D) format
|
||||
offset: Position offset for KV caching
|
||||
base: RoPE base frequency (default 10000.0)
|
||||
freqs: Precomputed negated frequencies (optional)
|
||||
|
||||
Returns:
|
||||
Tensor with RoPE applied, same shape as input
|
||||
"""
|
||||
head_dim = x.shape[-1]
|
||||
|
||||
if freqs is None:
|
||||
# Compute negated frequencies
|
||||
half_D = head_dim // 2
|
||||
freqs = -mx.exp(
|
||||
mx.arange(0.0, half_D, dtype=mx.float32) * (math.log(base) / half_D)
|
||||
)
|
||||
|
||||
# Use traditional=False + negated freqs
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
dims=head_dim,
|
||||
traditional=False,
|
||||
base=None,
|
||||
freqs=freqs,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
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 = self.hidden_size // self.num_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.rope_theta = args.rope_theta
|
||||
|
||||
self.c_q = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_k = nn.Linear(
|
||||
self.hidden_size, self.num_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_v = nn.Linear(
|
||||
self.hidden_size, self.num_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
|
||||
# Precompute negated RoPE frequencies for awni's approach
|
||||
half_D = self.head_dim // 2
|
||||
self._rope_freqs = -mx.exp(
|
||||
mx.arange(0.0, half_D, dtype=mx.float32)
|
||||
* (math.log(self.rope_theta) / half_D)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.c_q(x)
|
||||
keys = self.c_k(x)
|
||||
values = self.c_v(x)
|
||||
|
||||
# Reshape to (B, L, H, D) then transpose to (B, H, L, D)
|
||||
queries = queries.reshape(B, L, self.num_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
# Apply RoPE using precomputed frequencies (expects B, H, T, D format)
|
||||
offset = cache.offset if cache is not None else 0
|
||||
queries = apply_rotary_emb(
|
||||
queries, offset=offset, base=self.rope_theta, freqs=self._rope_freqs
|
||||
)
|
||||
keys = apply_rotary_emb(
|
||||
keys, offset=offset, base=self.rope_theta, freqs=self._rope_freqs
|
||||
)
|
||||
|
||||
# QK norm (critical feature of nanochat!)
|
||||
queries = rms_norm(queries)
|
||||
keys = rms_norm(keys)
|
||||
|
||||
# Handle KV cache after transpose
|
||||
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
|
||||
)
|
||||
|
||||
# Reshape back
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, self.hidden_size)
|
||||
return self.c_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.c_fc = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.c_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
# Critical: nanochat uses ReLU^2, not GELU!
|
||||
x = self.c_fc(x)
|
||||
x = nn.relu2(x)
|
||||
return self.c_proj(x)
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
# Pre-norm architecture with functional RMSNorm
|
||||
h = x + self.attn(rms_norm(x), mask=mask, cache=cache)
|
||||
out = h + self.mlp(rms_norm(h))
|
||||
return out
|
||||
|
||||
|
||||
class NanoChatModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.wte = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.h = [TransformerBlock(args) for _ in range(args.num_hidden_layers)]
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
h = self.wte(inputs)
|
||||
# Critical: norm after token embedding
|
||||
h = rms_norm(h)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
h = layer(h, mask=mask, cache=c)
|
||||
|
||||
# Critical: final norm before lm_head
|
||||
h = rms_norm(h)
|
||||
|
||||
return h
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def softcap(logits, cap=15.0):
|
||||
return cap * mx.tanh(logits / cap)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.transformer = NanoChatModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
out = self.transformer(inputs, cache=cache)
|
||||
logits = self.lm_head(out)
|
||||
|
||||
# Critical: logits softcap (nanochat uses softcap=15)
|
||||
logits = softcap(logits)
|
||||
|
||||
return logits
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.transformer.h
|
||||
@@ -225,13 +225,13 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
|
||||
try:
|
||||
self.act_fn = _ACT2FN[args.hidden_act]
|
||||
except KeyError:
|
||||
self.act_fn = args.hidden_act
|
||||
if self.act_fn not in _ACT2FN:
|
||||
raise ValueError(f"Unknown activation function: {args.hidden_act}")
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
|
||||
act_fn = _ACT2FN[self.act_fn]
|
||||
return self.down_proj(act_fn(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class LinearSubblockReplacement(nn.Module):
|
||||
@@ -333,19 +333,17 @@ class NemotronNASModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[List[Any]] = None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
for i, layer in enumerate(self.layers):
|
||||
h = layer(h, mask, cache=cache[i])
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -365,10 +363,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask=None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask=mask, cache=cache)
|
||||
out = self.model(inputs, cache=cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -176,17 +176,14 @@ class NemotronModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -205,10 +202,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@dataclass()
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
max_position_embeddings: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
mamba_num_heads: int
|
||||
mamba_head_dim: int
|
||||
mamba_proj_bias: bool
|
||||
ssm_state_size: int
|
||||
conv_kernel: int
|
||||
n_groups: int
|
||||
time_step_limit: Tuple[float, float]
|
||||
mlp_bias: bool
|
||||
layer_norm_epsilon: float
|
||||
rms_norm_eps: float
|
||||
use_bias: bool
|
||||
use_conv_bias: bool
|
||||
residual_in_fp32: bool
|
||||
hybrid_override_pattern: List[str]
|
||||
head_dim: Optional[int] = None
|
||||
|
||||
|
||||
class MambaRMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = mx.ones(hidden_size)
|
||||
|
||||
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)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
class NemotronHMamba2Mixer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_heads = args.mamba_num_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.ssm_state_size
|
||||
self.conv_kernel_size = args.conv_kernel
|
||||
self.intermediate_size = args.mamba_num_heads * args.mamba_head_dim
|
||||
self.n_groups = args.n_groups
|
||||
self.head_dim = args.mamba_head_dim
|
||||
self.time_step_limit = args.time_step_limit
|
||||
self.heads_per_group = self.num_heads // self.n_groups
|
||||
|
||||
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,
|
||||
kernel_size=args.conv_kernel,
|
||||
padding=0,
|
||||
groups=self.conv_dim,
|
||||
bias=args.use_conv_bias,
|
||||
)
|
||||
|
||||
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)
|
||||
self.A_log = mx.log(mx.arange(1, self.num_heads + 1, dtype=mx.float32))
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.norm = MambaRMSNormGated(
|
||||
self.intermediate_size, eps=args.layer_norm_epsilon
|
||||
)
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.mamba_proj_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
) -> mx.array:
|
||||
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)
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :, :]
|
||||
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,
|
||||
state: Optional[mx.array],
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> 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)
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[MambaCache] = None,
|
||||
) -> mx.array:
|
||||
|
||||
projected = self.in_proj(hidden_states)
|
||||
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected,
|
||||
[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)
|
||||
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class NemotronHAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.head_dim = (
|
||||
args.head_dim
|
||||
if args.head_dim is not None
|
||||
else (args.hidden_size // args.num_attention_heads)
|
||||
)
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
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_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_key_value_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
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries = self.q_proj(x).reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = (
|
||||
self.k_proj(x)
|
||||
.reshape(B, L, self.num_key_value_heads, -1)
|
||||
.transpose(0, 2, 1, 3)
|
||||
)
|
||||
values = (
|
||||
self.v_proj(x)
|
||||
.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)
|
||||
|
||||
|
||||
class NemotronHMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, args.intermediate_size, bias=args.mlp_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
args.intermediate_size, args.hidden_size, bias=args.mlp_bias
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.relu2(self.up_proj(x)))
|
||||
|
||||
|
||||
class NemotronHBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, block_type: str):
|
||||
super().__init__()
|
||||
self.residual_in_fp32 = args.residual_in_fp32
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.block_type = block_type
|
||||
|
||||
if self.block_type == "M":
|
||||
self.mixer = NemotronHMamba2Mixer(args)
|
||||
elif self.block_type == "*":
|
||||
self.mixer = NemotronHAttention(args)
|
||||
elif self.block_type == "-":
|
||||
self.mixer = NemotronHMLP(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
hidden_states = self.norm(x)
|
||||
if self.block_type == "M" or self.block_type == "*":
|
||||
hidden_states = self.mixer(hidden_states, mask=mask, cache=cache)
|
||||
else:
|
||||
hidden_states = self.mixer(hidden_states)
|
||||
|
||||
return x + hidden_states
|
||||
|
||||
|
||||
class NemotronHModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
NemotronHBlock(args, block_type)
|
||||
for block_type in args.hybrid_override_pattern
|
||||
]
|
||||
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.fa_idx = 0
|
||||
self.ssm_idx = 0
|
||||
for b in args.hybrid_override_pattern:
|
||||
if b == "*":
|
||||
break
|
||||
elif b == "M":
|
||||
self.fa_idx += 1
|
||||
for b in args.hybrid_override_pattern:
|
||||
if b == "*":
|
||||
self.ssm_idx += 1
|
||||
elif b == "M":
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
hidden_states = self.embeddings(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
ssm_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
|
||||
cache_counter = 0
|
||||
for layer in self.layers:
|
||||
if layer.block_type == "M" or layer.block_type == "*":
|
||||
c = cache[cache_counter]
|
||||
cache_counter += 1
|
||||
else:
|
||||
c = None
|
||||
|
||||
if layer.block_type == "*":
|
||||
mask = attn_mask
|
||||
else:
|
||||
mask = ssm_mask
|
||||
hidden_states = layer(hidden_states, mask=mask, cache=c)
|
||||
|
||||
return self.norm_f(hidden_states)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.backbone = NemotronHModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.model_type = args.model_type
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.backbone(inputs, cache=cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.backbone.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for l in self.layers:
|
||||
if l.block_type == "M":
|
||||
caches.append(MambaCache())
|
||||
elif l.block_type == "*":
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
return weights
|
||||
@@ -124,17 +124,15 @@ class Transformer(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.wte(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.blocks)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for block, c in zip(self.blocks, cache):
|
||||
h = block(h, mask, c)
|
||||
|
||||
@@ -154,10 +152,9 @@ class OlmoModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
return self.transformer(inputs, mask, cache)
|
||||
return self.transformer(inputs, cache)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
@@ -170,10 +167,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
return self.model(inputs, mask, cache)
|
||||
return self.model(inputs, cache)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
|
||||
@@ -163,15 +163,12 @@ class LlamaModel(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
@@ -192,9 +189,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask=None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
max_position_embeddings: int
|
||||
sliding_window: int
|
||||
rope_theta: float
|
||||
attention_bias: bool = False
|
||||
layer_types: Optional[List[str]] = None
|
||||
num_key_value_heads: Optional[int] = None
|
||||
head_dim: Optional[int] = None
|
||||
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:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
if self.layer_types is None:
|
||||
self.layer_types = [
|
||||
"full_attention" if (i + 1) % 4 == 0 else "sliding_attention"
|
||||
for i in range(self.num_hidden_layers)
|
||||
]
|
||||
|
||||
|
||||
class Olmo3Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.head_dim = args.head_dim or 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=args.attention_bias,
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
args.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
args.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
args.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(
|
||||
args.num_attention_heads * self.head_dim, eps=args.rms_norm_eps
|
||||
)
|
||||
self.k_norm = nn.RMSNorm(
|
||||
args.num_key_value_heads * self.head_dim, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
if args.layer_types[layer_idx] != "full_attention":
|
||||
self.rope = nn.RoPE(self.head_dim, traditional=False, base=args.rope_theta)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
traditional=False,
|
||||
base=args.rope_theta,
|
||||
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, _ = x.shape
|
||||
queries = self.q_norm(self.q_proj(x))
|
||||
keys = self.k_norm(self.k_proj(x))
|
||||
values = 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:
|
||||
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 Olmo3MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_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))
|
||||
|
||||
|
||||
class Olmo3DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.self_attn = Olmo3Attention(args, layer_idx=layer_idx)
|
||||
self.mlp = Olmo3MLP(args)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.args = args
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.post_attention_layernorm(self.self_attn(x, mask, cache))
|
||||
h = x + r
|
||||
r = self.post_feedforward_layernorm(self.mlp(h))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class Olmo3Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.sliding_window = args.sliding_window
|
||||
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Olmo3DecoderLayer(args=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.swa_idx = args.layer_types.index("sliding_attention")
|
||||
self.ga_idx = args.layer_types.index("full_attention")
|
||||
self.layer_types = args.layer_types
|
||||
|
||||
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)
|
||||
|
||||
full_mask = create_attention_mask(h, cache[self.ga_idx])
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
|
||||
mask = full_mask if layer_type == "full_attention" else sliding_window_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 = Olmo3Model(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
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for lt in self.model.layer_types:
|
||||
if lt == "full_attention":
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
@@ -163,13 +163,11 @@ class OlmoeModel(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
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, cache=c)
|
||||
return self.norm(h)
|
||||
@@ -188,9 +186,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask=None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -178,17 +178,14 @@ class OpenELMModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.token_embeddings(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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, cache=c)
|
||||
|
||||
@@ -207,10 +204,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
if self.args.share_input_output_layers:
|
||||
out = self.transformer.token_embeddings.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -141,14 +141,12 @@ class PhiModel(nn.Module):
|
||||
config.hidden_size, eps=config.layer_norm_eps
|
||||
)
|
||||
|
||||
def __call__(self, x, mask, cache):
|
||||
def __call__(self, x, cache):
|
||||
x = self.embed_tokens(x)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(x, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
mask = create_attention_mask(x, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
x = layer(x, mask, c)
|
||||
@@ -166,10 +164,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
y = self.model(x, mask, cache)
|
||||
y = self.model(x, cache)
|
||||
return self.lm_head(y)
|
||||
|
||||
@property
|
||||
|
||||
@@ -171,17 +171,15 @@ class Phi3Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -200,10 +198,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -258,19 +258,17 @@ class Phi3Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
if self.mup_embedding_multiplier:
|
||||
h = self.mup_embedding_multiplier * h
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache, return_array=True)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0], return_array=True)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -292,10 +290,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
if self.mup_width_multiplier:
|
||||
out = out / self.mup_width_multiplier
|
||||
|
||||
@@ -155,16 +155,13 @@ class PhiMoEModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
@@ -183,10 +180,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
|
||||
class PipelineMixin:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
self.start_idx = 0
|
||||
self.end_idx = None
|
||||
|
||||
@property
|
||||
def pipeline_layers(self):
|
||||
return self.layers[self.start_idx : self.end_idx]
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
# Keep the layer numbers the same for model loading
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
@@ -34,11 +34,10 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
mask: Optional[mx.array] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user