Compare commits

..

4 Commits

Author SHA1 Message Date
Awni Hannun 34940c5607 fix test 2025-03-27 08:35:32 -07:00
Awni Hannun 78be1bc89e use gradient accumulation 2025-03-27 08:34:15 -07:00
Awni Hannun 07be2b51cf use gradient accumulation 2025-03-27 08:33:56 -07:00
Awni Hannun d6d5d80431 enable memory efficient fine tuning for very long sequences 2025-03-27 08:33:32 -07:00
162 changed files with 2625 additions and 24920 deletions
+66
View File
@@ -0,0 +1,66 @@
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: macos.m1.large.gen1
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 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
workflows:
build_and_test:
when:
matches:
pattern: "^(?!pull/)[-\\w]+$"
value: << pipeline.git.branch >>
jobs:
- mlx_lm_build_and_test
- linux_build_and_test
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 ]
-16
View File
@@ -1,16 +0,0 @@
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 }}
-41
View File
@@ -1,41 +0,0 @@
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/
-41
View File
@@ -1,41 +0,0 @@
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/
+2 -19
View File
@@ -8,22 +8,5 @@ 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` 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`.
- Prince Canuma: Helped add support for `Starcoder2` models.
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's`Mamba v1`, and Allenai's `OLMoE`; Added support for the following training algorithms: `full-fine-tuning`.
+38 -20
View File
@@ -52,12 +52,6 @@ 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:
@@ -85,9 +79,7 @@ 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. 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.
to see how to use the API in more detail.
The `mlx-lm` package also comes with functionality to quantize and optionally
upload models to the Hugging Face Hub.
@@ -236,19 +228,45 @@ for more usage details.
### Supported Models
`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.
`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.
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.
Here are a few examples of Hugging Face models that work with this example:
Tokenizer options can also be set in the Python API. For example:
- [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:
```python
model, tokenizer = load(
-63
View File
@@ -1,63 +0,0 @@
# 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>
-170
View File
@@ -1,170 +0,0 @@
# Learned Quantization
To reduce the quality loss from quantization MLX LM has several options:
- Distilled Weight Quantization (DWQ)
- Activation-aware Weight Quantization (AWQ)[^1]
- Dynamic quantization
- GPT Quantization (GPTQ)[^2]
All methods use calibration data to tune parameters or hyper-parameters of the
model. DWQ fine-tunes non-quantized parameters (including quantization scales
and biases) using the non-quantized model as a teacher. AWQ scales and clips
the weights prior to quantization. Dynamic quantization estimates the
sensitivity of a model's outputs to each layer and uses a higher precision for
layers which have higher sensitivity. GPTQ finds quantized weights which
minimize the squared error of each layer's output given the provided input.
Dynamic quantization is the fastest to run. DWQ takes longer but typically
yields better results. You can also cascade methods. For example a dynamically
quantized model can be further refined with DWQ.
To get started, first install the requirements:
```
pip install "mlx-lm[train]"
```
### DWQ
Use `mlx_lm.dwq` to run DWQ on a given model. For example:
```bash
mlx_lm.dwq --model Qwen/Qwen3-0.6B
```
Some important options, along with their default values are:
- `--mlx-path mlx_model`: The location to save the DWQ model.
- `--bits 4`: Precision of the quantization.
- `--num-samples 1024`: Number of samples to use. Using more samples can lead to
better results but takes longer.
- `--batch-size 8`: Use a smaller batch size to reduce the memory footprint.
For a full list of options run:
```bash
mlx_lm.dwq --help
```
#### Tips
- DWQ works best distilling to lower precision, anywhere from 2-bit to 4-bit
models.
- Distilling 16-bit precision to 8-bit and even 6-bit often doesn't work well.
The loss starts out so low that it's difficult to reduce further.
- Decreasing the quantization group size (e.g. `--group-size 32`) doubles the
number of tunable parameters and can work much better.
- If the loss is oscillating and not going down consistently, try reducing the
learning rate. If it is decreasing but slowly, try increasing the learning
rate.
- As a rule of thumb, lower precision can benefit from a higher learning rate
since the loss starts out higher. Conversely, higher precision needs a lower
learning rate.
#### Memory Use
A few options to reduce memory use for DWQ:
- Distill from an 8-bit model instead of a 16-bit model. The 8-bit
models are usually as good as 16-bit precision models.
- Use a shorter maximum sequence length. The default is 2048. Using
`--max-seq-length 512` reduces the memory and still gets good results.
- Use a smaller batch size, e.g. `--batch-size 1`
### Dynamic Quantization
Use `mlx_lm.dynamic_quant` to generate a dynamic quantization of given model.
For example:
```bash
mlx_lm.dynamic_quant --model Qwen/Qwen3-0.6B
```
The script will estimate the sensitivity for each quantizable layer in the
model. It will then quantize the model using higher precision (default 5 bits)
for the more sensitive layers and lower precision (default 4 bits) for the
rest. The script also saves a JSON file with each layer's sensitivities which
saves needing to compute it multiple times to make different precision quants
of the same model.
Some important options are:
- `--target-bpw`: The target bits-per-weight. For a given set of quantization
parameters only certain ranges are possible. For example, with the default
parameters a BPW in the range `[4.5, 5.5]` is achievable.
- `--sensitivities`: A path to a precomputed sensitivities file.
- `--low-bits`: The number of bits to use for the less sensitive layers.
- `--high-bits`: The number of bits to use for the more sensitive layers.
### AWQ
Use `mlx_lm.awq` to run AWQ on a given model. For example:
```bash
mlx_lm.awq --model Qwen/Qwen3-0.6B
```
The script can take anywhere form a few minutes to several hours to run
depending on the model size and the number of samples.
Some important options, along with their default values, are:
- `--mlx-path mlx_model`: The location to save the AWQ model.
- `--bits 4`: Precision of the quantization.
- `--num-samples 32`: Number of samples to use. Using more samples can lead to
better results but takes longer.
- `--n-grid 10`: The granularity of the AWQ search. A larger grid can lead to
better results but takes longer.
For a full list of options run:
```bash
mlx_lm.awq --help
```
### GPTQ
Use `mlx_lm.gptq` to run GPTQ on a given model. For example:
```bash
mlx_lm.gptq --model Qwen/Qwen3-0.6B
```
The script can take anywhere from a few minutes to several hours depending on
the model size.
Some important options, along with their default values, are:
- `--mlx-path mlx_model`: The location to save the AWQ model.
- `--bits 4`: Precision of the quantization.
### Evaluate
Once the quantization training finishes, you can evaluate the quality of the
model on downstream tasks using `mlx_lm.evaluate`. For example:
```bash
mlx_lm.evaluate \
--model mlx_model \
--tasks winogrande boolq arc_challenge arc_easy hellaswag openbookqa piqa social_iqa
```
### Upload to Hugging Face
Use `mlx_lm.upload` to upload the quantized model to the Hugging Face Hub. For
example:
```bash
mlx_lm.upload \
--path mlx_model \
--upload-repo mlx-community/Mistral-7B-Instruct-v0.3-3bit-DWQ
```
[^1]: Refer to the [paper](https://arxiv.org/abs/2306.00978)
and [github repository](https://github.com/mit-han-lab/llm-awq) for more
details on AWQ.
[^2]: Refer to the [paper](https://arxiv.org/abs/2210.17323) for more details
on GPTQ.
+5 -25
View File
@@ -26,12 +26,6 @@ LoRA (QLoRA).[^qlora] LoRA fine-tuning works with the following model families:
## Run
First, make sure you have the training dependenices installed:
```shell
pip install "mlx-lm[train]"
```
The main command is `mlx_lm.lora`. To see a full list of command-line options run:
```shell
@@ -82,17 +76,6 @@ You can specify the output location with `--adapter-path`.
You can resume fine-tuning with an existing adapter with
`--resume-adapter-file <path_to_adapters.safetensors>`.
#### Logging
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
The default training computes a loss for every token in the sample. You can
@@ -308,7 +291,7 @@ example:
```yaml
hf_dataset:
path: "billsum"
name: "billsum"
prompt_feature: "text"
completion_feature: "summary"
```
@@ -325,12 +308,12 @@ with the same structure as above. For example:
```yaml
hf_dataset:
- path: "Open-Orca/OpenOrca"
- name: "Open-Orca/OpenOrca"
train_split: "train[:90%]"
valid_split: "train[-10%:]"
prompt_feature: "question"
completion_feature: "response"
- path: "trl-lib/ultrafeedback_binarized"
- name: "trl-lib/ultrafeedback_binarized"
train_split: "train[:90%]"
valid_split: "train[-10%:]"
chat_feature: "chosen"
@@ -371,10 +354,7 @@ 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. 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.
things down a little, but will also reduce the memory use.
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
@@ -399,7 +379,7 @@ mlx_lm.lora \
--train \
--batch-size 1 \
--num-layers 4 \
--data mlx-community/wikisql
--data wikisql
```
The above command on an M1 Max with 32 GB runs at about 250
+50
View File
@@ -0,0 +1,50 @@
# Model Merging
You can use `mlx-lm` to merge models and upload them to the Hugging
Face hub or save them locally for LoRA fine tuning.
The main command is `mlx_lm.merge`:
```shell
mlx_lm.merge --config config.yaml
```
The merged model will be saved by default in `mlx_merged_model`. To see a
full list of options run:
```shell
mlx_lm.merge --help
```
Here is an example `config.yaml`:
```yaml
models:
- OpenPipe/mistral-ft-optimized-1218
- mlabonne/NeuralHermes-2.5-Mistral-7B
method: slerp
parameters:
t:
- filter: self_attn
value: [0, 0.5, 0.3, 0.7, 1]
- filter: mlp
value: [1, 0.5, 0.7, 0.3, 0]
- value: 0.5
```
The `models` field is a list of Hugging Face repo ids. The first model in the
list is treated as the base model into which the remaining models are merged.
The `method` field is the merging method. Right now `slerp` is the only
supported method.
The `parameters` are the corresponding parameters for the given `method`.
Each parameter is a list with `filter` determining which layer the parameter
applies to and `value` determining the actual value used. The last item in
the list without a `filter` field is the default.
If `value` is a list, it specifies the start and end values for the
corresponding segment of blocks. In the example above, the models have 32
blocks. For blocks 1-8, the layers with `self_attn` in the name will use the
values `np.linspace(0, 0.5, 8)`, the same layers in the next 8 blocks (9-16)
will use `np.linspace(0.5, 0.3, 8)`, and so on.
+2 -14
View File
@@ -54,24 +54,18 @@ curl localhost:8080/v1/chat/completions \
sequences of tokens on which the generation should stop.
- `max_tokens`: (Optional) An integer specifying the maximum number of tokens
to generate. Defaults to `512`.
to generate. Defaults to `100`.
- `stream`: (Optional) A boolean indicating if the response should be
streamed. If true, responses are sent as they are generated. Defaults to
false.
- `temperature`: (Optional) A float specifying the sampling temperature.
Defaults to `0.0`.
Defaults to `1.0`.
- `top_p`: (Optional) A float specifying the nucleus sampling parameter.
Defaults to `1.0`.
- `top_k`: (Optional) An integer specifying the top-k sampling parameter.
Defaults to `0` (disabled).
- `min_p`: (Optional) A float specifying the min-p sampling parameter.
Defaults to `0.0` (disabled).
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
Defaults to `1.0`.
@@ -92,12 +86,6 @@ curl localhost:8080/v1/chat/completions \
- `adapters`: (Optional) A string path to low-rank adapters. The path must be
relative to the directory the server was started in.
- `draft_model`: (Optional) Specifies a smaller model to use for speculative
decoding. Set to `null` to unload.
- `num_draft_tokens`: (Optional) The number of draft tokens the draft model
should predict at once. Defaults to `3`.
### Response Fields
- `id`: A unique identifier for the chat.
+37
View File
@@ -0,0 +1,37 @@
### Packaging for PyPI
Install `build` and `twine`:
```
pip install --user --upgrade build
pip install --user --upgrade twine
```
Generate the source distribution and wheel:
```
python -m build
```
> [!warning]
> Use a test server first
#### Test Upload
Upload to test server:
```
python -m twine upload --repository testpypi dist/*
```
Install from test server and check that it works:
```
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps mlx-lm
```
#### Upload
```
python -m twine upload dist/*
```
+1 -10
View File
@@ -7,14 +7,5 @@ from ._version import __version__
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
from .convert import convert
from .generate import batch_generate, generate, stream_generate
from .generate import generate, stream_generate
from .utils import load
__all__ = [
"__version__",
"convert",
"batch_generate",
"generate",
"stream_generate",
"load",
]
+4 -26
View File
@@ -4,33 +4,11 @@ import importlib
import sys
if __name__ == "__main__":
subcommands = {
"quant.awq",
"quant.dwq",
"quant.dynamic_quant",
"quant.gptq",
"benchmark",
"cache_prompt",
"chat",
"convert",
"evaluate",
"fuse",
"generate",
"lora",
"perplexity",
"server",
"manage",
"upload",
}
subcommands = {"convert"}
if len(sys.argv) < 2:
raise ValueError(f"CLI requires a subcommand in {subcommands}")
subcommand = sys.argv.pop(1)
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:
if subcommand not in subcommands:
raise ValueError(f"CLI requires a subcommand in {subcommands}")
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
submodule.main()
+2 -2
View File
@@ -1,3 +1,3 @@
# Copyright © 2023-2025 Apple Inc.
# Copyright © 2023-2024 Apple Inc.
__version__ = "0.29.0"
__version__ = "0.22.2"
-126
View File
@@ -1,126 +0,0 @@
# 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()
+1 -5
View File
@@ -148,7 +148,7 @@ def main():
pass
print()
print(f"Peak memory: {mx.get_peak_memory() / 1e9:.3f} GB")
print(f"Peak memory: {mx.metal.get_peak_memory() / 1e9:.3f} GB")
print("Saving...")
metadata = {}
@@ -159,8 +159,4 @@ def main():
if __name__ == "__main__":
print(
"Calling `python -m mlx_lm.cache_prompt...` directly is deprecated."
" Use `mlx_lm.cache_prompt...` or `python -m mlx_lm cache_prompt ...` instead."
)
main()
+4 -44
View File
@@ -1,6 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
import argparse
import json
import mlx.core as mx
@@ -11,8 +12,6 @@ from .utils import load
DEFAULT_TEMP = 0.0
DEFAULT_TOP_P = 1.0
DEFAULT_XTC_PROBABILITY = 0.0
DEFAULT_XTC_THRESHOLD = 0.0
DEFAULT_SEED = None
DEFAULT_MAX_TOKENS = 256
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
@@ -27,11 +26,6 @@ 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,
@@ -43,18 +37,6 @@ def setup_arg_parser():
parser.add_argument(
"--top-p", type=float, default=DEFAULT_TOP_P, help="Sampling top-p"
)
parser.add_argument(
"--xtc-probability",
type=float,
default=DEFAULT_XTC_PROBABILITY,
help="Probability of XTC sampling to happen each next token",
)
parser.add_argument(
"--xtc-threshold",
type=float,
default=0.0,
help="Thresold the probs of each next token candidate to be sampled by XTC",
)
parser.add_argument(
"--seed",
type=int,
@@ -74,11 +56,6 @@ 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
@@ -92,9 +69,7 @@ def main():
model, tokenizer = load(
args.model,
adapter_path=args.adapter_path,
tokenizer_config={
"trust_remote_code": True if args.trust_remote_code else None
},
tokenizer_config={"trust_remote_code": True},
)
def print_help():
@@ -116,25 +91,14 @@ def main():
if query == "h":
print_help()
continue
messages = []
if args.system_prompt is not None:
messages.append({"role": "system", "content": args.system_prompt})
messages.append({"role": "user", "content": query})
messages = [{"role": "user", "content": query}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
for response in stream_generate(
model,
tokenizer,
prompt,
max_tokens=args.max_tokens,
sampler=make_sampler(
args.temp,
args.top_p,
xtc_threshold=args.xtc_threshold,
xtc_probability=args.xtc_probability,
xtc_special_tokens=(
tokenizer.encode("\n") + list(tokenizer.eos_token_ids)
),
),
sampler=make_sampler(args.temp, args.top_p),
prompt_cache=prompt_cache,
):
print(response.text, flush=True, end="")
@@ -142,8 +106,4 @@ def main():
if __name__ == "__main__":
print(
"Calling `python -m mlx_lm.chat...` directly is deprecated."
" Use `mlx_lm.chat...` or `python -m mlx_lm chat ...` instead."
)
main()
+56 -104
View File
@@ -1,70 +1,51 @@
# Copyright © 2023-2024 Apple Inc.
import argparse
import glob
import shutil
from pathlib import Path
from typing import Callable, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_map_with_path
from mlx.utils import tree_flatten
from .utils import (
dequantize_model,
load,
fetch_from_hub,
get_model_path,
quantize_model,
save,
save_config,
save_weights,
upload_to_hub,
)
def mixed_quant_predicate_builder(
recipe: str, model: nn.Module, group_size: int = 64
low_bits: int = 4, high_bits: int = 4, group_size: int = 64
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
high_bits = 6
if recipe == "mixed_2_6":
low_bits = 2
elif recipe == "mixed_3_4":
low_bits = 3
high_bits = 4
elif recipe == "mixed_3_6":
low_bits = 3
elif recipe == "mixed_4_6":
low_bits = 4
else:
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:
raise ValueError("Model does not have expected keys for mixed quant.")
# Look for the layer index location in the path:
for layer_location, k in enumerate(down_keys[0].split(".")):
if k.isdigit():
break
num_layers = len(model.layers)
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
By Alex Barron: https://gist.github.com/barronalex/84addb8078be21969f1690c1454855f3
"""
index = (
int(path.split(".")[layer_location])
if len(path.split(".")) > layer_location
else 0
)
if not hasattr(module, "to_quantized"):
return False
index = int(path.split(".")[2]) if len(path.split(".")) > 2 else 0
num_layers = config["num_hidden_layers"]
use_more_bits = (
index < num_layers // 8
or index >= 7 * num_layers // 8
or (index - num_layers // 8) % 3 == 2
)
if (
"v_proj" in path or "v_a_proj" in path or "v_b_proj" in path
) and use_more_bits:
if "v_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}
@@ -76,9 +57,19 @@ def mixed_quant_predicate_builder(
return mixed_quant_predicate
QUANT_RECIPES = ["mixed_2_6", "mixed_3_4", "mixed_3_6", "mixed_4_6"]
QUANT_RECIPES = {
"mixed_2_6": mixed_quant_predicate_builder(low_bits=3, high_bits=6),
"mixed_3_6": mixed_quant_predicate_builder(low_bits=2, high_bits=6),
}
MODEL_CONVERSION_DTYPES = ["float16", "bfloat16", "float32"]
def quant_args(arg):
if arg not in QUANT_RECIPES:
raise argparse.ArgumentTypeError(
f"Invalid q-recipe {arg!r}. Choose from: {list(QUANT_RECIPES.keys())}"
)
else:
return QUANT_RECIPES[arg]
def convert(
@@ -87,15 +78,13 @@ def convert(
quantize: bool = False,
q_group_size: int = 64,
q_bits: int = 4,
q_mode: str = "affine",
dtype: Optional[str] = None,
dtype: str = "float16",
upload_repo: str = None,
revision: Optional[str] = None,
dequantize: bool = False,
quant_predicate: Optional[
Union[Callable[[str, nn.Module, dict], Union[bool, dict]], str]
Callable[[str, nn.Module, dict], Union[bool, dict]]
] = None,
trust_remote_code: bool = False,
):
# Check the save path is empty
if isinstance(mlx_path, str):
@@ -108,64 +97,41 @@ def convert(
)
print("[INFO] Loading")
model, tokenizer, config = load(
hf_path,
revision=revision,
return_config=True,
tokenizer_config={"trust_remote_code": trust_remote_code},
lazy=True,
)
model_path = get_model_path(hf_path, revision=revision)
model, config, tokenizer = fetch_from_hub(model_path, lazy=True)
if isinstance(quant_predicate, str):
quant_predicate = mixed_quant_predicate_builder(
quant_predicate, model, q_group_size
)
if dtype is None:
dtype = config.get("torch_dtype", None)
if dtype in MODEL_CONVERSION_DTYPES:
print("[INFO] Using dtype:", dtype)
dtype = getattr(mx, dtype)
cast_predicate = getattr(model, "cast_predicate", lambda _: True)
def set_dtype(k, v):
if cast_predicate(k) and mx.issubdtype(v.dtype, mx.floating):
return v.astype(dtype)
else:
return v
model.update(tree_map_with_path(set_dtype, model.parameters()))
weights = dict(tree_flatten(model.parameters()))
dtype = getattr(mx, dtype)
weights = {k: v.astype(dtype) for k, v in weights.items()}
if quantize and dequantize:
raise ValueError("Choose either quantize or dequantize, not both.")
if quantize:
print("[INFO] Quantizing")
model, config = quantize_model(
model,
config,
q_group_size,
q_bits,
mode=q_mode,
quant_predicate=quant_predicate,
model.load_weights(list(weights.items()))
weights, config = quantize_model(
model, config, q_group_size, q_bits, quant_predicate=quant_predicate
)
if dequantize:
print("[INFO] Dequantizing")
config.pop("quantization", None)
config.pop("quantization_config", None)
model = dequantize_model(model)
weights = dict(tree_flatten(model.parameters()))
save(
mlx_path,
hf_path,
model,
tokenizer,
config,
)
del model
save_weights(mlx_path, weights, donate_weights=True)
py_files = glob.glob(str(model_path / "*.py"))
for file in py_files:
shutil.copy(file, mlx_path)
tokenizer.save_pretrained(mlx_path)
save_config(config, config_path=mlx_path / "config.json")
if upload_repo is not None:
upload_to_hub(mlx_path, upload_repo)
upload_to_hub(mlx_path, upload_repo, hf_path)
def configure_parser() -> argparse.ArgumentParser:
@@ -192,26 +158,18 @@ 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.",
choices=QUANT_RECIPES,
type=str,
help=f"Mixed-bit quantization recipe. Choices: {list(QUANT_RECIPES.keys())}",
type=quant_args,
required=False,
)
parser.add_argument(
"--dtype",
help="Type to save the non-quantized parameters. Defaults to config.json's `torch_dtype` or the current model weights dtype.",
help="Type to save the non-quantized parameters.",
type=str,
choices=MODEL_CONVERSION_DTYPES,
default=None,
choices=["float16", "bfloat16", "float32"],
default="float16",
)
parser.add_argument(
"--upload-repo",
@@ -226,12 +184,6 @@ 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
+158 -274
View File
@@ -5,14 +5,12 @@ Adapted from a PyTorch implementation by David Grangier
"""
import argparse
import collections
import copy
import json
import logging
import os
from importlib.metadata import version
from pathlib import Path
from typing import Any, Callable, Optional
from typing import Optional, Union
import lm_eval
import mlx.core as mx
@@ -20,15 +18,22 @@ import mlx.nn as nn
import numpy as np
from lm_eval.api.model import LM
from lm_eval.api.registry import register_model
from lm_eval.models import huggingface
from tqdm import tqdm
from .generate import batch_generate
from .generate import stream_generate
from .models.cache import make_prompt_cache
from .sample_utils import make_sampler
from .utils import load
DEFAULT_MAX_TOKENS = 8192
PAD = 0
def _len_longest_common_prefix(a, b):
l = 0
for item_a, item_b in zip(a, b):
if item_a != item_b:
break
l += 1
return l
def _rstrip_until(s, untils):
@@ -39,104 +44,114 @@ 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()
padded = np.stack(
[np.pad(x, (0, maxlen - len(x))) for x in inputs],
def _pad_inputs(
inputs,
maxlen,
genlen=0,
pad_left=False,
pad_multiple=32,
truncate=False,
):
# pad the prompts to the left with at least genlen tokens.
actual_maxlen = max(len(p) for p in inputs) + genlen
if actual_maxlen > maxlen:
if not truncate:
raise ValueError("Inputs are too long.")
else: # drop begining
actual_maxlen = maxlen
inputs = [p[max(0, len(p) - maxlen) :] for p in inputs]
if pad_multiple > 0:
maxlen = (actual_maxlen + pad_multiple - 1) // pad_multiple
maxlen *= pad_multiple
assert PAD == 0
lr = np.array((1, 0) if pad_left else (0, 1))
return np.stack(
[np.pad(np.array(x, np.int32), lr * (maxlen - len(x))) for x in inputs],
axis=0,
)
return mx.array(padded), mx.array(lengths)
def chat_template_fn(**extra_kwargs):
def apply_chat_template(self, chat_history, add_generation_prompt=True) -> str:
return self.tokenizer.apply_chat_template(
chat_history,
tokenize=False,
add_generation_prompt=add_generation_prompt,
continue_final_message=not add_generation_prompt,
**extra_kwargs,
)
return apply_chat_template
@register_model("mlxlm")
class MLXLM(LM):
tokenizer_name = huggingface.HFLM.tokenizer_name
apply_chat_template = chat_template_fn()
def __init__(
self,
path_or_hf_repo: str,
batch_size: int = 16,
max_tokens: Optional[int] = None,
use_chat_template: Optional[bool] = None,
trust_remote_code: bool = False,
sampler: Optional[Callable[[mx.array], mx.array]] = None,
) -> None:
super().__init__()
tokenizer_config = {"trust_remote_code": True if trust_remote_code else None}
self._model, self.tokenizer = load(
path_or_hf_repo, tokenizer_config=tokenizer_config
self._batch_size = batch_size
self._model, self.tokenizer = load(path_or_hf_repo)
self._max_tokens = max_tokens or self.tokenizer.model_max_length
self.use_chat_template = use_chat_template or (
self.tokenizer.chat_template is not None
)
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]
cache = make_prompt_cache(self._model)
for i in range(0, prompt.shape[1], step_size):
logits = self._model(prompt[:, i : i + step_size], cache=cache)
mx.eval([c.state for c in cache])
mx.clear_cache()
logprobs = nn.log_softmax(logits[:, -1, :].astype(mx.float32))
return logprobs, cache
def _score_fn(self, inputs, cache: Optional[Any] = None, step_size: int = 2048):
inputs, lengths = _pad_inputs(inputs)
def _score_fn(self, inputs, tokenize=True, step_size=32):
if tokenize:
inputs = self._tokenize(inputs)
inputs = _pad_inputs(inputs, self._max_tokens, truncate=False)
inputs = mx.array(inputs)
inputs, targets = inputs[..., :-1], inputs[..., 1:]
cache = cache or make_prompt_cache(self._model)
offset = 0
cache = make_prompt_cache(self._model)
mask = targets != PAD
scores, is_greedy = [], []
for i in range(0, inputs.shape[1], step_size):
inp = inputs[:, i : i + step_size]
T = inp.shape[1]
logits = self._model(inputs[:, i : i + step_size], cache=cache)
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(offset, T + offset) < lengths[:, None], ig, False)
ig = mask[:, i : i + step_size] * (
targets[:, i : i + step_size] == mx.argmax(logits, axis=-1)
)
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)
return scores, lengths, is_greedy
return scores, mask.sum(axis=-1), is_greedy
def _loglikelihood(self, texts, score_spans=None, tokenize=True):
# sort by length to get batches with little padding.
sorted_indices = sorted(range(len(texts)), key=lambda i: -len(texts[i]))
sorted_inputs = [texts[sorted_indices[i]] for i in range(len(texts))]
sorted_spans = None
if score_spans is not None:
sorted_spans = [score_spans[sorted_indices[i]] for i in range(len(texts))]
results = []
for i in tqdm(range(0, len(sorted_inputs), self._batch_size)):
batch = sorted_inputs[i : i + self._batch_size]
scores, length, is_greedy = self._score_fn(batch, tokenize=tokenize)
for j in range(len(batch)):
if sorted_spans is None: # full sequence score
mask = mx.arange(scores[j].shape[-1]) < length
score = (scores[j].astype(mx.float32) * mask).sum(axis=-1)
ig = (is_greedy[j].astype(mx.int32) * mask).sum(axis=-1)
else: # subsequence score
start, end = sorted_spans[i + j]
score = scores[j][start:end].astype(mx.float32).sum()
ig = is_greedy[j][start:end].astype(mx.int32).sum()
length = end - start
results.append((score.item(), ig.item(), length))
# reorder the outputs
inv_sort = np.argsort(sorted_indices)
results = [results[inv_sort[i]] for i in range(len(results))]
return results
def _tokenize(self, texts):
return [
@@ -168,63 +183,39 @@ class MLXLM(LM):
"""
logging.info("Estimating loglikelihood for %d pairs." % len(requests))
group = mx.distributed.init()
# tokenize prefix and prefix + completion for all requests.
tokenized = self._tokenize(
[t for r in requests for t in [r.args[0], r.args[0] + r.args[1]]]
)
# Group by common prefix
group_reqs = collections.defaultdict(list)
for idx, req in enumerate(requests):
group_reqs[req.args[0]].append((idx, req.args[1]))
questions = list(group_reqs.keys())
responses = []
indices = []
for v in group_reqs.values():
idx, resp = zip(*v)
indices.append(idx)
responses.append(resp)
# split data accross ranks
questions = questions[group.rank() :: group.size()]
responses = responses[group.rank() :: group.size()]
# max length (prefix + completion) and longest common prefix per question.
length_stats = {}
for prefix, completed in zip(tokenized[0::2], tokenized[1::2]):
max_completed_l, min_prefix_l = length_stats.get(prefix, (0, 1e8))
length_stats[prefix] = (
max(max_completed_l, len(completed)),
min(min_prefix_l, _len_longest_common_prefix(prefix, completed)),
)
# truncate requests for completed sequences longer than model context.
shortened = []
completion_spans = []
long_completions = 0
scores, is_greedy = [], []
for q, rs in tqdm(zip(questions, responses), total=len(questions)):
prefix = self._tokenize([q])[0]
full_sequences = self._tokenize([q + r for r in rs])
max_completed_l = max(len(s) for s in full_sequences)
for prefix, completed in zip(tokenized[0::2], tokenized[1::2]):
max_completed_l, prefix_l = length_stats[prefix]
# compute truncation length
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 :]
# If the entire prompt got truncated ignore the question
if prefix_l == 0:
truncation = max(0, max_completed_l - self._max_tokens - 1)
prefix_l = prefix_l - truncation
if prefix_l <= 0:
# completion too long, prefix is eliminated for some requests.
long_completions += 1
all_scores.extend([-float("inf")] * len(rs))
all_is_greedy.extend([False] * len(rs))
continue
# model scoring, returns num_requests x (logp, is_greedy, length).
logprobs, cache = self._process_prompt(prefix)
max_idx = mx.argmax(logprobs).item()
for s in full_sequences:
inputs = s[len(prefix) :]
# The logprobs from the last token of the prompt are
# for the first input token
scores.append(logprobs[0, inputs[0]].item())
is_greedy.append((inputs[0] == max_idx))
if len(inputs) == 1:
continue
score, _, ig = self._score_fn(
mx.array(inputs)[None, :], cache=copy.deepcopy(cache)
)
scores[-1] += mx.sum(score).item()
is_greedy[-1] &= mx.all(ig).item()
truncation = max(0, len(completed) - self._max_tokens - 1)
prefix_l = 1
# truncate the completed sequence
completed = completed[truncation:]
shortened.append(completed)
# scores do not include initial bos, substract 1 to span bounds
completion_spans.append((prefix_l - 1, len(completed) - 1))
if long_completions > 0:
logging.info(
@@ -232,31 +223,16 @@ class MLXLM(LM):
+ "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)
# model scoring, returns num_requests x (logp, is_greedy, length).
results = self._loglikelihood(
shortened,
score_spans=completion_spans,
tokenize=False,
)
return [(r[0], r[1] == r[2]) for r in results]
# 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()))
tokenizer_name = lm_eval.models.huggingface.HFLM.tokenizer_name
apply_chat_template = lm_eval.models.huggingface.HFLM.apply_chat_template
def loglikelihood_rolling(self, requests) -> list[float]:
"""Compute full log-likelihood of a string, with no truncation, for perplexity computation
@@ -293,15 +269,8 @@ class MLXLM(LM):
logging.info(
"Estimating loglikelihood rolling for %d sequences." % len(requests)
)
inputs = self._tokenize([req.args[0] for req in requests])
all_scores = []
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())
return all_scores
inputs = [req.args[0] for req in requests]
return [t[0] for t in self._loglikelihood(inputs)]
def generate_until(self, requests) -> list[str]:
"""Generate greedily until a stopping sequence
@@ -317,77 +286,32 @@ class MLXLM(LM):
continuation: str
The generated continuation.
"""
group = mx.distributed.init()
# split data accross ranks
total_requests = len(requests)
requests = requests[group.rank() :: group.size()]
logging.info("Generating continuation for %d sequences." % len(requests))
contexts, options = zip(*[req.args for req in requests])
# The second element of the tuple contains:
# contrary to the doc the second element of the tuple contains
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
completions = []
# Tokenize all contexts
contexts = [
self.tokenizer.encode(
for context, opt in tqdm(zip(contexts, options), total=len(contexts)):
until = opt["until"]
context = self.tokenizer.encode(
context, add_special_tokens=not self.use_chat_template
)
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)
]
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)
return completions
@@ -401,17 +325,15 @@ def main():
"--output-dir", default=".", help="Output directory for result files."
)
parser.add_argument("--batch-size", type=int, default=16, help="Batch size")
parser.add_argument("--num-shots", type=int, default=None, help="Number of shots")
parser.add_argument("--num-shots", type=int, default=0, help="Number of shots")
parser.add_argument(
"--max-tokens",
type=int,
help="Maximum number of tokens to generate. When set, this value takes"
" precedence over task specific defaults.",
default=None,
help="Maximum nunber of tokens to generate. Defaults to the model's max context length.",
)
parser.add_argument(
"--limit",
default=None,
default=100,
help="Limit the number of examples per task.",
type=int,
)
@@ -431,27 +353,6 @@ def main():
"otherwise `False`.",
default=None,
)
parser.add_argument(
"--chat-template-args",
type=json.loads,
help="""A JSON formatted string of arguments for the tokenizer's
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)
@@ -462,26 +363,12 @@ 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,
batch_size=args.batch_size,
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)
results = lm_eval.simple_evaluate(
model=lm,
tasks=args.tasks,
@@ -493,17 +380,14 @@ 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")]
if args.num_shots is not None:
file_keys += [f"{args.num_shots:02d}"]
file_keys += args.tasks
filename = "_".join(file_keys)
if world.rank() == 0:
output_path = output_dir / filename
output_path.write_text(json.dumps(results["results"], indent=4))
print("Results:")
for result in results["results"].values():
print(json.dumps(result, indent=4))
model_name = args.model.replace("/", "_")
task_names = "_".join(args.tasks)
ver = version("lm_eval")
filename = f"eval_{model_name}_{task_names}_{args.num_shots:02d}_v_{ver}.json"
output_path = output_dir / filename
output_path.write_text(json.dumps(results["results"], indent=4))
print("Results:")
for result in results["results"].values():
print(json.dumps(result, indent=4))
@@ -1,51 +0,0 @@
# 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])
+3 -9
View File
@@ -1,5 +1,5 @@
# The path to the local model directory or Hugging Face repo.
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
model: "mlx_model"
# Whether or not to train (boolean)
train: true
@@ -17,7 +17,7 @@ optimizer: adamw
# bias_correction: true
# Directory with {train, valid, test}.jsonl files
data: "mlx-community/WikiSQL"
data: "/path/to/training/data"
# The PRNG seed
seed: 0
@@ -37,19 +37,12 @@ val_batches: 25
# Adam learning rate.
learning_rate: 1e-5
# 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
# 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
@@ -93,3 +86,4 @@ lora_parameters:
# valid_split: "train[-100:]"
# prompt_feature: "text"
# completion_feature: "summary"
-65
View File
@@ -1,65 +0,0 @@
# Copyright © 2025 Apple Inc.
"""
This is an example of tool use with mlx_lm and the OpenAI client.
To run, first start the server:
>>> mlx_lm.server
Then run this script.
"""
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
model = "mlx-community/qwen3-4b-4bit-DWQ"
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
def get_current_weather(**kwargs):
return "51 Farenheit, clear skies"
functions = {"get_current_weather": get_current_weather}
# The first query generates a tool call
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
# Call the function
function = response.choices[0].message.tool_calls[0].function
tool_result = functions[function.name](**json.loads(function.arguments))
# Put the result of the function in the messages and generate the final
# response:
messages.append({"role": "tool", "name": function.name, "content": tool_result})
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
print(response.choices[0].message.content)
+58 -5
View File
@@ -5,7 +5,8 @@ Run with:
```
mlx.launch \
--hostfile /path/to/hosts.json \
--hostfile /path/to/hosts.txt \
--backend mpi \
/path/to/pipeline_generate.py \
--prompt "hello world"
```
@@ -17,11 +18,63 @@ https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
"""
import argparse
import json
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
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, _ = load_model(model_path, lazy=True, strict=False)
group = mx.distributed.init(backend="mpi")
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)
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")
@@ -45,14 +98,14 @@ if __name__ == "__main__":
)
args = parser.parse_args()
group = mx.distributed.init()
group = mx.distributed.init(backend="mpi")
rank = group.rank()
def rprint(*args, **kwargs):
if rank == 0:
print(*args, **kwargs)
model, tokenizer = pipeline_load(args.model)
model, tokenizer = shard_and_load(args.model)
messages = [{"role": "user", "content": args.prompt}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
+49 -29
View File
@@ -1,13 +1,19 @@
import argparse
import glob
import shutil
from pathlib import Path
from mlx.utils import tree_flatten, tree_unflatten
from .gguf import convert_to_gguf
from .tuner.dora import DoRAEmbedding, DoRALinear
from .tuner.lora import LoRAEmbedding, LoRALinear, LoRASwitchLinear
from .tuner.utils import dequantize, load_adapters
from .utils import (
dequantize_model,
load,
save,
fetch_from_hub,
get_model_path,
save_config,
save_weights,
upload_to_hub,
)
@@ -32,6 +38,12 @@ def parse_arguments() -> argparse.Namespace:
default="adapters",
help="Path to the trained adapter weights and config.",
)
parser.add_argument(
"--hf-path",
type=str,
default=None,
help="Path to the original Hugging Face model. Required for upload if --model is a local directory.",
)
parser.add_argument(
"--upload-repo",
help="The Hugging Face repo to upload the model to.",
@@ -39,8 +51,8 @@ def parse_arguments() -> argparse.Namespace:
default=None,
)
parser.add_argument(
"--dequantize",
help="Generate a dequantized model.",
"--de-quantize",
help="Generate a de-quantized model.",
action="store_true",
)
parser.add_argument(
@@ -61,33 +73,39 @@ def main() -> None:
print("Loading pretrained model")
args = parse_arguments()
model, tokenizer, config = load(
args.model, adapter_path=args.adapter_path, return_config=True
)
model_path = get_model_path(args.model)
model, config, tokenizer = fetch_from_hub(model_path)
model.freeze()
model = load_adapters(model, args.adapter_path)
fused_linears = [
(n, m.fuse(dequantize=args.dequantize))
for n, m in model.named_modules()
if hasattr(m, "fuse")
(n, m.fuse()) for n, m in model.named_modules() if hasattr(m, "fuse")
]
if fused_linears:
model.update_modules(tree_unflatten(fused_linears))
if args.dequantize:
print("Dequantizing model")
if args.de_quantize:
print("De-quantizing model")
model = dequantize(model)
config.pop("quantization", None)
weights = dict(tree_flatten(model.parameters()))
save_path = Path(args.save_path)
save(
save_path,
args.model,
model,
tokenizer,
config,
donate_model=False,
)
save_weights(save_path, weights)
py_files = glob.glob(str(model_path / "*.py"))
for file in py_files:
shutil.copy(file, save_path)
tokenizer.save_pretrained(save_path)
if args.de_quantize:
config.pop("quantization", None)
save_config(config, config_path=save_path / "config.json")
if args.export_gguf:
model_type = config["model_type"]
@@ -95,16 +113,18 @@ def main() -> None:
raise ValueError(
f"Model type {model_type} not supported for GGUF conversion."
)
weights = dict(tree_flatten(model.parameters()))
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
if args.upload_repo is not None:
upload_to_hub(args.save_path, args.upload_repo)
hf_path = args.hf_path or (
args.model if not Path(args.model).exists() else 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, hf_path)
if __name__ == "__main__":
print(
"Calling `python -m mlx_lm.fuse...` directly is deprecated."
" Use `mlx_lm.fuse...` or `python -m mlx_lm fuse ...` instead."
)
main()
+69 -621
View File
@@ -7,7 +7,6 @@ import json
import sys
import time
from dataclasses import dataclass
from functools import partial
from typing import (
Any,
Callable,
@@ -25,27 +24,20 @@ from transformers import PreTrainedTokenizer
from .models import cache
from .models.cache import (
ArraysCache,
BatchKVCache,
BatchRotatingKVCache,
CacheList,
KVCache,
QuantizedKVCache,
RotatingKVCache,
load_prompt_cache,
make_prompt_cache,
trim_prompt_cache,
)
from .sample_utils import make_sampler
from .tokenizer_utils import TokenizerWrapper
from .utils import does_model_support_input_embeddings, load
from .utils import load
DEFAULT_PROMPT = "hello"
DEFAULT_MAX_TOKENS = 100
DEFAULT_TEMP = 0.0
DEFAULT_TOP_P = 1.0
DEFAULT_MIN_P = 0.0
DEFAULT_TOP_K = 0
DEFAULT_XTC_PROBABILITY = 0.0
DEFAULT_XTC_THRESHOLD = 0.0
DEFAULT_MIN_TOKENS_TO_KEEP = 1
DEFAULT_SEED = None
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
@@ -68,11 +60,6 @@ def setup_arg_parser():
),
default=None,
)
parser.add_argument(
"--trust-remote-code",
action="store_true",
help="Enable trusting remote code for tokenizer",
)
parser.add_argument(
"--adapter-path",
type=str,
@@ -117,21 +104,6 @@ def setup_arg_parser():
parser.add_argument(
"--min-p", type=float, default=DEFAULT_MIN_P, help="Sampling min-p"
)
parser.add_argument(
"--top-k", type=int, default=DEFAULT_TOP_K, help="Sampling top-k"
)
parser.add_argument(
"--xtc-probability",
type=float,
default=DEFAULT_XTC_PROBABILITY,
help="Probability of XTC sampling to happen each next token",
)
parser.add_argument(
"--xtc-threshold",
type=float,
default=0.0,
help="Thresold the probs of each next token candidate to be sampled by XTC",
)
parser.add_argument(
"--min-tokens-to-keep",
type=int,
@@ -226,35 +198,29 @@ def wired_limit(model: nn.Module, streams: Optional[List[mx.Stream]] = None):
async eval could be running pass in the streams to synchronize with prior
to exiting the context manager.
"""
if not mx.metal.is_available():
try:
yield
finally:
pass
else:
model_bytes = tree_reduce(
lambda acc, x: acc + x.nbytes if isinstance(x, mx.array) else acc, model, 0
model_bytes = tree_reduce(
lambda acc, x: acc + x.nbytes if isinstance(x, mx.array) else acc, model, 0
)
max_rec_size = mx.metal.device_info()["max_recommended_working_set_size"]
if model_bytes > 0.9 * max_rec_size:
model_mb = model_bytes // 2**20
max_rec_mb = max_rec_size // 2**20
print(
f"[WARNING] Generating with a model that requires {model_mb} MB "
f"which is close to the maximum recommended size of {max_rec_mb} "
"MB. This can be slow. See the documentation for possible work-arounds: "
"https://github.com/ml-explore/mlx-lm/tree/main#large-models"
)
max_rec_size = mx.metal.device_info()["max_recommended_working_set_size"]
if model_bytes > 0.9 * max_rec_size:
model_mb = model_bytes // 2**20
max_rec_mb = max_rec_size // 2**20
print(
f"[WARNING] Generating with a model that requires {model_mb} MB "
f"which is close to the maximum recommended size of {max_rec_mb} "
"MB. This can be slow. See the documentation for possible work-arounds: "
"https://github.com/ml-explore/mlx-lm/tree/main#large-models"
)
old_limit = mx.set_wired_limit(max_rec_size)
try:
yield
finally:
if streams is not None:
for s in streams:
mx.synchronize(s)
else:
mx.synchronize()
mx.set_wired_limit(old_limit)
old_limit = mx.set_wired_limit(max_rec_size)
try:
yield None
finally:
if streams is not None:
for s in streams:
mx.synchronize(s)
else:
mx.synchronize()
mx.set_wired_limit(old_limit)
@dataclass
@@ -288,11 +254,16 @@ class GenerationResponse:
def maybe_quantize_kv_cache(prompt_cache, quantized_kv_start, kv_group_size, 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)
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
)
def generate_step(
@@ -300,7 +271,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,
@@ -308,8 +279,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]] = None,
input_embeddings: Optional[mx.array] = None,
prompt_progress_callback: Optional[Callable[int, int]] = None,
) -> Generator[Tuple[mx.array, mx.array], None, None]:
"""
A generator producing token ids based on the given prompt from the model.
@@ -334,28 +304,14 @@ 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], None]): A call-back which takes the
prompt_prorgress_callback (Callable[int, int]): 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``.
Yields:
Tuple[mx.array, mx.array]: One token and a vector of log probabilities.
"""
if input_embeddings is not None:
if not does_model_support_input_embeddings(model):
raise ValueError("Model does not support input embeddings.")
elif len(prompt) > 0 and len(prompt) != len(input_embeddings):
raise ValueError(
f"When providing input_embeddings, their sequence length ({len(input_embeddings)}) "
f"must match the sequence length of the prompt ({len(prompt)}), or the "
"prompt must be empty."
)
elif len(prompt) == 0:
raise ValueError(
"Either input_embeddings or prompt (or both) must be provided."
)
y = prompt
tokens = None
# Create the KV cache for generation
@@ -364,6 +320,8 @@ def generate_step(
model,
max_kv_size=max_kv_size,
)
elif len(prompt_cache) != len(model.layers):
raise ValueError("Wrong number of layers in the prompt cache.")
prompt_progress_callback = prompt_progress_callback or (lambda *_: None)
@@ -376,72 +334,37 @@ def generate_step(
sampler = sampler or (lambda x: mx.argmax(x, axis=-1))
def _model_call(input_tokens: mx.array, input_embeddings: Optional[mx.array]):
if input_embeddings is not None:
return model(
input_tokens, cache=prompt_cache, input_embeddings=input_embeddings
)
else:
return model(input_tokens, cache=prompt_cache)
def _step(input_tokens: mx.array, input_embeddings: Optional[mx.array] = None):
nonlocal tokens
def _step(y):
with mx.stream(generation_stream):
logits = _model_call(
input_tokens=input_tokens[None],
input_embeddings=(
input_embeddings[None] if input_embeddings is not None else None
),
)
logits = model(y[None], cache=prompt_cache)
logits = logits[:, -1, :]
if logits_processors and len(input_tokens) > 0:
tokens = (
mx.concat([tokens, input_tokens])
if tokens is not None
else input_tokens
)
if logits_processors:
nonlocal tokens
tokens = mx.concat([tokens, y]) if tokens is not None else y
for processor in logits_processors:
logits = processor(tokens, logits)
quantize_cache_fn(prompt_cache)
logprobs = logits - mx.logsumexp(logits, keepdims=True)
sampled = sampler(logprobs)
return sampled, logprobs.squeeze(0)
y = sampler(logprobs)
return y, logprobs.squeeze(0)
with mx.stream(generation_stream):
total_prompt_tokens = (
len(input_embeddings) if input_embeddings is not None else len(prompt)
)
total_prompt_tokens = y.size
prompt_processed_tokens = 0
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[:n_to_process][None],
input_embeddings=(
input_embeddings[:n_to_process][None]
if input_embeddings is not None
else None
),
)
while y.size > prefill_step_size:
model(y[:prefill_step_size][None], cache=prompt_cache)
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 = prompt[n_to_process:]
input_embeddings = (
input_embeddings[n_to_process:]
if input_embeddings is not None
else input_embeddings
)
prompt_processed_tokens += prefill_step_size
y = y[prefill_step_size:]
mx.clear_cache()
y, logprobs = _step(input_tokens=prompt, input_embeddings=input_embeddings)
y, logprobs = _step(y)
mx.async_eval(y, logprobs)
n = 0
@@ -466,9 +389,9 @@ def speculative_generate_step(
model: nn.Module,
draft_model: nn.Module,
*,
num_draft_tokens: int = 2,
num_draft_tokens=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,
@@ -487,7 +410,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
@@ -513,6 +436,8 @@ def speculative_generate_step(
if prompt_cache is None:
model_cache = cache.make_prompt_cache(model)
draft_cache = cache.make_prompt_cache(draft_model)
elif len(prompt_cache) != (len(model.layers) + len(draft_model.layers)):
raise ValueError("Wrong number of layers in the prompt cache.")
else:
model_cache = prompt_cache[: len(model.layers)]
draft_cache = prompt_cache[len(model.layers) :]
@@ -640,7 +565,6 @@ 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]:
@@ -652,8 +576,6 @@ 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``.
@@ -678,8 +600,6 @@ 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)
@@ -689,11 +609,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:
@@ -704,8 +624,6 @@ def stream_generate(
break
detokenizer.add_token(token)
if (n + 1) == max_tokens:
break
yield GenerationResponse(
text=detokenizer.last_segment,
@@ -740,6 +658,7 @@ def generate(
tokenizer: Union[PreTrainedTokenizer, TokenizerWrapper],
prompt: Union[str, List[int]],
verbose: bool = False,
formatter: Optional[Callable] = None,
**kwargs,
) -> str:
"""
@@ -754,6 +673,11 @@ 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)
@@ -781,473 +705,6 @@ 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()
@@ -1276,7 +733,7 @@ def main():
tokenizer_config = (
{} if not using_cache else json.loads(metadata["tokenizer_config"])
)
tokenizer_config["trust_remote_code"] = True if args.trust_remote_code else None
tokenizer_config["trust_remote_code"] = True
model_path = args.model
if using_cache:
@@ -1349,16 +806,7 @@ def main():
raise ValueError("Draft model tokenizer does not match model tokenizer.")
else:
draft_model = None
sampler = make_sampler(
args.temp,
args.top_p,
args.min_p,
args.min_tokens_to_keep,
top_k=args.top_k,
xtc_probability=args.xtc_probability,
xtc_threshold=args.xtc_threshold,
xtc_special_tokens=tokenizer.encode("\n") + list(tokenizer.eos_token_ids),
)
sampler = make_sampler(args.temp, args.top_p, args.min_p, args.min_tokens_to_keep)
response = generate(
model,
tokenizer,
+27 -57
View File
@@ -1,9 +1,10 @@
# Copyright © 2024 Apple Inc.
import argparse
import math
import os
import re
import types
import warnings
from pathlib import Path
import mlx.core as mx
@@ -12,8 +13,8 @@ import mlx.optimizers as optim
import numpy as np
import yaml
from .tuner.callbacks import get_reporting_callbacks
from .tuner.datasets import CacheDataset, load_dataset
from .tokenizer_utils import TokenizerWrapper
from .tuner.datasets import load_dataset
from .tuner.trainer import TrainingArgs, TrainingCallback, evaluate, train
from .tuner.utils import (
build_schedule,
@@ -40,18 +41,15 @@ yaml_loader.add_implicit_resolver(
)
CONFIG_DEFAULTS = {
"model": "Qwen/Qwen3-0.6b",
"model": "mlx_model",
"train": False,
"fine_tune_type": "lora",
"optimizer": "adam",
"optimizer_config": {
"adam": {},
"adamw": {},
"muon": {},
"sgd": {},
"adafactor": {},
},
"data": "mlx-community/WikiSQL",
"data": "data/",
"seed": 0,
"num_layers": 16,
"batch_size": 4,
@@ -68,12 +66,9 @@ 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},
"lora_parameters": {"rank": 8, "alpha": 16, "dropout": 0.0, "scale": 10.0},
"mask_prompt": False,
"report_to": None,
"project_name": None,
}
@@ -109,9 +104,9 @@ def build_parser():
parser.add_argument(
"--optimizer",
type=str,
choices=["adam", "adamw", "muon", "sgd", "adafactor"],
choices=["adam", "adamw"],
default=None,
help="Optimizer to use for training: adam, adamw, sgd, or adafactor.",
help="Optimizer to use for training: adam or adamw",
)
parser.add_argument(
"--mask-prompt",
@@ -142,11 +137,6 @@ 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,
@@ -178,6 +168,12 @@ def build_parser():
type=int,
help="Maximum sequence length.",
)
parser.add_argument(
"--seq-step-size",
type=int,
default=None,
help="",
)
parser.add_argument(
"-c",
"--config",
@@ -190,18 +186,6 @@ def build_parser():
help="Use gradient checkpointing to reduce memory use.",
default=None,
)
parser.add_argument(
"--report-to",
type=str,
default=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
@@ -209,6 +193,7 @@ def build_parser():
def train_model(
args,
model: nn.Module,
tokenizer: TokenizerWrapper,
train_set,
valid_set,
training_callback: TrainingCallback = None,
@@ -224,8 +209,6 @@ def train_model(
if args.fine_tune_type == "full":
for l in model.layers[-max(args.num_layers, 0) :]:
l.unfreeze()
args.lora_parameters = None
elif args.fine_tune_type in ["lora", "dora"]:
# Convert linear layers to lora/dora layers and unfreeze in the process
linear_to_lora_layers(
@@ -261,7 +244,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,
seq_step_size=args.seq_step_size,
)
# Initialize the selected optimizer
@@ -269,16 +252,11 @@ def train_model(
optimizer_name = args.optimizer.lower()
optimizer_config = args.optimizer_config.get(optimizer_name, {})
if optimizer_name == "adam":
opt_class = optim.Adam
elif optimizer_name == "adamw":
opt_class = optim.AdamW
elif optimizer_name == "muon":
opt_class = optim.Muon
elif optimizer_name == "sgd":
opt_class = optim.SGD
elif optimizer_name == "adafactor":
opt_class = optim.Adafactor
else:
raise ValueError(f"Unsupported optimizer: {optimizer_name}")
@@ -287,18 +265,20 @@ def train_model(
# Train model
train(
model=model,
tokenizer=tokenizer,
args=training_args,
optimizer=opt,
train_dataset=CacheDataset(train_set),
val_dataset=CacheDataset(valid_set),
train_dataset=train_set,
val_dataset=valid_set,
training_callback=training_callback,
)
def evaluate_model(args, model: nn.Module, test_set):
def evaluate_model(args, model: nn.Module, tokenizer: TokenizerWrapper, test_set):
test_loss = evaluate(
model=model,
dataset=CacheDataset(test_set),
dataset=test_set,
tokenizer=tokenizer,
batch_size=args.batch_size,
num_batches=args.test_batches,
max_seq_length=args.max_seq_length,
@@ -311,15 +291,9 @@ def evaluate_model(args, model: nn.Module, test_set):
def run(args, training_callback: TrainingCallback = None):
np.random.seed(args.seed)
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, tokenizer_config={"trust_remote_code": True})
model, tokenizer = load(args.model)
print("Loading datasets")
train_set, valid_set, test_set = load_dataset(args, tokenizer)
@@ -331,13 +305,13 @@ def run(args, training_callback: TrainingCallback = None):
elif args.train:
print("Training")
train_model(args, model, train_set, valid_set, training_callback)
train_model(args, model, tokenizer, train_set, valid_set, training_callback)
else:
raise ValueError("Must provide at least one of --train or --test")
if args.test:
print("Testing")
evaluate_model(args, model, test_set)
evaluate_model(args, model, tokenizer, test_set)
def main():
@@ -363,8 +337,4 @@ def main():
if __name__ == "__main__":
print(
"Calling `python -m mlx_lm.lora...` directly is deprecated."
" Use `mlx_lm.lora...` or `python -m mlx_lm lora ...` instead."
)
main()
-4
View File
@@ -136,8 +136,4 @@ def main():
if __name__ == "__main__":
print(
"Calling `python -m mlx_lm.manage...` directly is deprecated."
" Use `mlx_lm.manage...` or `python -m mlx_lm manage ...` instead."
)
main()
+172
View File
@@ -0,0 +1,172 @@
# Copyright © 2023-2024 Apple Inc.
import argparse
import glob
import shutil
from pathlib import Path
from typing import Optional
import mlx.core as mx
import mlx.nn as nn
import numpy as np
import yaml
from mlx.utils import tree_flatten, tree_map
from .utils import (
fetch_from_hub,
get_model_path,
save_config,
save_weights,
upload_to_hub,
)
def configure_parser() -> argparse.ArgumentParser:
"""
Configures and returns the argument parser for the script.
Returns:
argparse.ArgumentParser: Configured argument parser.
"""
parser = argparse.ArgumentParser(description="Merge multiple models.")
parser.add_argument("--config", type=str, help="Path to the YAML config.")
parser.add_argument(
"--mlx-path",
type=str,
default="mlx_merged_model",
help="Path to save the MLX model.",
)
parser.add_argument(
"--upload-repo",
help="The Hugging Face repo to upload the model to.",
type=str,
default=None,
)
return parser
def slerp(t, w1, w2, eps=1e-5):
"""
Spherical linear interpolation
Args:
t (float): Interpolation weight in [0.0, 1.0]
w1 (mx.array): First input
w2 (mx.array): Second input
eps (float): Constant for numerical stability
Returns:
mx.array: Interpolated result
"""
t = float(t)
if t == 0:
return w1
elif t == 1:
return w2
# Normalize
v1 = w1 / mx.linalg.norm(w1)
v2 = w2 / mx.linalg.norm(w2)
# Angle
dot = mx.clip((v1 * v2).sum(), 0.0, 1.0)
theta = mx.arccos(dot)
sin_theta = mx.sin(theta + eps)
s1 = mx.sin(theta * (1 - t)) / sin_theta
s2 = mx.sin(theta * t) / sin_theta
return s1 * w1 + s2 * w2
def merge_models(base_model: nn.Module, model: nn.Module, config: dict):
method = config.get("method", None)
if method != "slerp":
raise ValueError(f"Merge method {method} not supported")
num_layers = len(model.layers)
def unpack_values(vals):
if isinstance(vals, (int, float)):
return np.full(num_layers, vals)
bins = len(vals) - 1
sizes = [num_layers // bins] * bins
sizes[-1] = num_layers - sum(sizes[:-1])
return np.concatenate(
[np.linspace(v1, v2, s) for v1, v2, s in zip(vals[:-1], vals[1:], sizes)]
)
param_list = config["parameters"]["t"]
params = {}
filter_keys = set()
for pl in param_list[:-1]:
params[pl["filter"]] = unpack_values(pl["value"])
filter_keys.add(pl["filter"])
default = unpack_values(param_list[-1]["value"])
for e in range(num_layers):
bl = base_model.layers[e]
l = model.layers[e]
base_weights = bl.parameters()
weights = l.parameters()
for k, w1 in base_weights.items():
w2 = weights[k]
t = params.get(k, default)[e]
base_weights[k] = tree_map(lambda x, y: slerp(t, x, y), w1, w2)
base_model.update(base_weights)
def merge(
config: str,
mlx_path: str = "mlx_model",
upload_repo: Optional[str] = None,
):
with open(config, "r") as fid:
merge_conf = yaml.safe_load(fid)
print("[INFO] Loading")
model_paths = merge_conf.get("models", [])
if len(model_paths) < 2:
raise ValueError(f"Expected at least 2 models, got {len(model_paths)}.")
# Load all models
base_hf_path = model_paths[0]
base_path = get_model_path(base_hf_path)
base_model, base_config, tokenizer = fetch_from_hub(base_path, lazy=True)
models = []
for mp in model_paths[1:]:
model, model_config, _ = fetch_from_hub(get_model_path(mp), lazy=True)
base_type = base_config["model_type"]
model_type = model_config["model_type"]
if base_type != model_type:
raise ValueError(
f"Can only merge models of the same type,"
f" but got {base_type} and {model_type}."
)
models.append(model)
# Merge models into base model
for m in models:
merge_models(base_model, m, merge_conf)
# Save base model
mlx_path = Path(mlx_path)
weights = dict(tree_flatten(base_model.parameters()))
del models, base_model
save_weights(mlx_path, weights, donate_weights=True)
py_files = glob.glob(str(base_path / "*.py"))
for file in py_files:
shutil.copy(file, mlx_path)
tokenizer.save_pretrained(mlx_path)
save_config(config, config_path=mlx_path / "config.json")
if upload_repo is not None:
upload_to_hub(mlx_path, upload_repo, base_hf_path)
def main():
parser = configure_parser()
args = parser.parse_args()
merge(**vars(args))
if __name__ == "__main__":
main()
-262
View File
@@ -1,262 +0,0 @@
# 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
-394
View File
@@ -1,394 +0,0 @@
# Copyright © 2025 Apple Inc.
import math
from dataclasses import dataclass
from functools import partial
from itertools import accumulate
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 .cache import ConcatenateKVCache, KVCache
from .rope_utils import initialize_rope
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
vocab_size: int
hidden_dim: int
num_layers: int
num_kv_reuse_layers: int
num_heads: int
num_kv_heads: int
hidden_dim_scale_factor: float = 3.25
rope_theta: float = 50000
rms_norm_eps: float = 1e-5
class FusedLoRALinear(nn.Module):
def __init__(
self,
input_dims: int,
output_dims: list[int],
r: int = 8,
dropout: float = 0.0,
scale: float = 20.0,
):
super().__init__()
self.linear = FusedLinear(input_dims, output_dims)
self.dropout = nn.Dropout(p=dropout)
self.scale = scale
scale = 1 / math.sqrt(input_dims)
self.lora_a = [
mx.random.uniform(low=-scale, high=scale, shape=(input_dims, r))
for _ in output_dims
]
self.lora_b = [mx.zeros((r, od)) for od in output_dims]
def fuse(self, dequantize: bool = False):
linear = self.linear
weight = linear.weight
is_quantized = isinstance(linear, FusedQuantizedLinear)
# Use the same type as the linear weight if not quantized
dtype = weight.dtype
if is_quantized:
dtype = linear.scales.dtype
weight = mx.dequantize(
weight,
linear.scales,
linear.biases,
linear.group_size,
linear.bits,
)
input_dims = weight.shape[-1]
output_dims = linear.output_dims
fused_linear = FusedLinear(input_dims, output_dims)
fused_linear.weight = weight
deltas = [
((self.scale * b.T) @ a.T).astype(dtype)
for a, b in zip(self.lora_a, self.lora_b)
]
delta = mx.concatenate(deltas, axis=0)
fused_linear.weight = weight + delta
if is_quantized and not dequantize:
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
return fused_linear
def __call__(self, x):
dt = x.dtype
y = self.linear(x)
x = self.dropout(x)
z = [(x @ a) @ b for a, b in zip(self.lora_a, self.lora_b)]
return tuple(yi + (self.scale * zi).astype(dt) for yi, zi in zip(y, z))
class FusedQuantizedLinear(nn.QuantizedLinear):
def __init__(self, input_dims, output_dims, group_size: int = 64, bits: int = 4):
*indices, output_dims = accumulate(output_dims)
self.indices = indices
super().__init__(
input_dims, output_dims, bias=False, group_size=group_size, bits=bits
)
@property
def input_dims(self):
return self.scales.shape[-1] * self.group_size
@property
def output_dims(self):
indices = [0] + self.indices + [self.weight.shape[0]]
return [indices[i] - indices[i - 1] for i in range(1, len(indices))]
def __call__(self, x):
x = super().__call__(x)
return x.split(self.indices, axis=-1)
def to_lora(self, r: int = 8, dropout: float = 0.0, scale: float = 20.0):
lora_lin = FusedLoRALinear(self.input_dims, self.output_dims, r, dropout, scale)
lora_lin.linear = self
return lora_lin
class FusedLinear(nn.Linear):
def __init__(self, input_dims, output_dims):
*indices, output_dims = accumulate(output_dims)
self.indices = indices
super().__init__(input_dims, output_dims, bias=False)
@property
def input_dims(self):
return self.weight.shape[-1]
@property
def output_dims(self):
indices = [0] + self.indices + [self.weight.shape[0]]
return [indices[i] - indices[i - 1] for i in range(1, len(indices))]
def __call__(self, x):
x = super().__call__(x)
return x.split(self.indices, axis=-1)
def to_quantized(self, group_size: int = 64, bits: int = 4):
input_dims = self.input_dims
output_dims = self.output_dims
ql = FusedQuantizedLinear(input_dims, output_dims, group_size, bits)
ql.weight, ql.scales, ql.biases = mx.quantize(self.weight, group_size, bits)
return ql
def to_lora(self, r: int = 8, dropout: float = 0.0, scale: float = 20.0):
lora_lin = FusedLoRALinear(self.input_dims, self.output_dims, r, dropout, scale)
lora_lin.linear = self
return lora_lin
@partial(mx.compile, shapeless=True)
def fake_8bit_quant(x, scale):
dt = x.dtype
x = x.astype(mx.float32)
x = (x / scale).round()
x = mx.clip(x, -128, 127)
return (x * scale).astype(dt)
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_dim
self.n_heads = n_heads = args.num_heads
self.n_kv_heads = n_kv_heads = args.num_kv_heads
self.head_dim = head_dim = args.hidden_dim // n_heads
self.scale = head_dim**-0.5
qkv_dim = (n_heads + 2 * n_kv_heads) * head_dim
self.qkv_proj = FusedLinear(
dim, [n_heads * head_dim] + 2 * [n_kv_heads * head_dim]
)
self.out_proj = nn.Linear(dim, dim, bias=False)
self.rope = initialize_rope(
self.head_dim,
args.rope_theta,
True,
)
self.q_norm = nn.RMSNorm(head_dim)
self.k_norm = nn.RMSNorm(head_dim)
self.quant_key_scale = mx.array(1.0)
self.quant_value_scale = mx.array(1.0)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
# Get the queries, keys and values
queries, keys, values = self.qkv_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)
if cache is not None:
queries = self.q_norm(self.rope(queries, offset=cache.offset))
keys = self.k_norm(self.rope(keys, offset=cache.offset))
keys = fake_8bit_quant(keys, self.quant_key_scale)
values = fake_8bit_quant(values, self.quant_value_scale)
keys, values = cache.update_and_fetch(keys, values)
else:
queries = self.q_norm(self.rope(queries))
keys = self.k_norm(self.rope(keys))
keys = fake_8bit_quant(keys, self.quant_key_scale)
values = fake_8bit_quant(values, self.quant_value_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.out_proj(output)
class KVReuseAttention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_dim
self.n_heads = n_heads = args.num_heads
self.head_dim = head_dim = args.hidden_dim // n_heads
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, dim, bias=False)
self.out_proj = nn.Linear(dim, dim, bias=False)
self.rope = initialize_rope(
self.head_dim,
args.rope_theta,
True,
)
self.q_norm = nn.RMSNorm(head_dim)
def __call__(
self,
x: mx.array,
keys: mx.array,
values: mx.array,
mask: Optional[mx.array] = None,
) -> mx.array:
B, L, D = x.shape
_, _, S, _ = keys.shape
queries = self.q_proj(x)
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
queries = self.q_norm(self.rope(queries, offset=S - L))
output = scaled_dot_product_attention(
queries, keys, values, cache=None, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.out_proj(output)
@partial(mx.compile, shapeless=True)
def _swiglu(g, x):
return nn.silu(g) * x
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_dim
hidden_dim = int(dim * args.hidden_dim_scale_factor)
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:
g = self.gate_proj(x)
x = self.up_proj(x)
return self.down_proj(_swiglu(g, x))
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.self_attn = Attention(args)
self.mlp = MLP(args)
self.input_layernorm = nn.RMSNorm(args.hidden_dim, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_dim, 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 KVReuseTransformerBlock(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.self_attn = KVReuseAttention(args)
self.mlp = MLP(args)
self.input_layernorm = nn.RMSNorm(args.hidden_dim, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_dim, eps=args.rms_norm_eps
)
def __call__(
self,
x: mx.array,
keys: mx.array,
values: mx.array,
mask: Optional[mx.array] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), keys, values, mask)
h = x + r
r = self.mlp(self.post_attention_layernorm(h))
out = h + r
return out
class AFMModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.vocab_size = args.vocab_size
self.embedding = nn.Embedding(args.vocab_size, args.hidden_dim)
self.layers = [
TransformerBlock(args)
for _ in range(args.num_layers - args.num_kv_reuse_layers)
]
self.kv_reuse_layers = [
KVReuseTransformerBlock(args) for _ in range(args.num_kv_reuse_layers)
]
self.output_norm = nn.RMSNorm(args.hidden_dim, eps=args.rms_norm_eps)
def __call__(
self,
inputs: mx.array,
cache=None,
):
h = self.embedding(inputs)
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)
keys, values = cache[-1].state
for layer in self.kv_reuse_layers:
h = layer(h, keys, values, mask)
return self.output_norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = AFMModel(args)
def __call__(
self,
inputs: mx.array,
cache=None,
):
out = self.model(inputs, cache)
out = self.model.embedding.as_linear(out)
return out
def make_cache(self):
return [KVCache() for _ in range(len(self.model.layers))]
@property
def layers(self):
return self.model.layers + self.model.kv_reuse_layers
-404
View File
@@ -1,404 +0,0 @@
# 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
-219
View File
@@ -1,219 +0,0 @@
# 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
-250
View File
@@ -1,250 +0,0 @@
# 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 .cache import CacheList, KVCache, MambaCache, RotatingKVCache
@dataclass
class ModelArgs(BaseModelArgs):
vocab_size: int
hidden_size: int
intermediate_size: int
num_hidden_layers: int
num_attention_heads: int
num_key_value_heads: int
rope_theta: float
sliding_window: int
sliding_window_layers: List[int]
conv_window: int
rms_norm_eps: float
model_type: str = "baichuan_m1"
num_swa_attention_heads: Optional[int] = None
num_swa_key_value_heads: Optional[int] = None
tie_word_embeddings: bool = False
class Attention(nn.Module):
def __init__(self, config: ModelArgs, layer_idx: Optional[int] = None):
super().__init__()
self.config = config
self.layer_idx = layer_idx
if layer_idx is None:
raise ValueError("Layer index must be provided to Attention module.")
self.is_swa = layer_idx in config.sliding_window_layers
self.num_heads = (
config.num_swa_attention_heads
if self.is_swa and config.num_swa_attention_heads
else config.num_attention_heads
)
self.num_kv_heads = (
config.num_swa_key_value_heads
if self.is_swa and config.num_swa_key_value_heads
else config.num_key_value_heads
)
self.hidden_size = config.hidden_size
self.head_dim = self.hidden_size // self.num_heads
assert self.head_dim * self.num_heads == self.hidden_size
self.scale = self.head_dim**-0.5
self.W_pack = nn.Linear(
config.hidden_size,
self.hidden_size + 2 * self.num_kv_heads * self.head_dim,
bias=False,
)
self.o_proj = nn.Linear(
self.num_heads * self.head_dim, config.hidden_size, bias=False
)
self.rope = nn.RoPE(self.head_dim, traditional=False, base=config.rope_theta)
self.conv_window = config.conv_window
assert self.conv_window == 2
self.conv_k = mx.zeros((1, 1, self.num_kv_heads, 1, self.conv_window))
self.conv_v = mx.zeros((1, 1, self.num_kv_heads, 1, self.conv_window))
def _custom_convolution(self, u, weights, state=None):
B, H, L, D = u.shape
weights = weights.reshape((1, H, self.conv_window, 1, 1))
w0 = weights[:, :, 0]
w1 = weights[:, :, 1]
if state is None:
state = mx.zeros((B, H, 1, D), u.dtype)
if L > 1:
u_prev = mx.concatenate([state, u[:, :, :-1]], axis=2)
else:
u_prev = state
return u_prev * w0 + u * w1
def __call__(
self, x: mx.array, mask: mx.array = None, cache: Any = None
) -> mx.array:
B, L, D = x.shape
proj = self.W_pack(x)
q, k, v = mx.split(proj, (D, D + self.num_kv_heads * self.head_dim), axis=-1)
q = q.reshape(B, L, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
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 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:
offset = 0
last_k, last_v = None, None
k_init = k
v_init = v
k = self._custom_convolution(k, self.conv_k, state=last_k)
v = self._custom_convolution(v, self.conv_v, state=last_v)
q = self.rope(q, offset=offset)
k = self.rope(k, offset=offset)
if cache[0] is not None:
k, v = cache[1].update_and_fetch(k, v)
if L > 0:
cache[0][0] = k_init[:, :, -1:, :]
cache[0][1] = v_init[:, :, -1:, :]
out = scaled_dot_product_attention(
q, k, v, cache=cache[1], scale=self.scale, mask=mask
)
out = out.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(out)
class MLP(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.gate_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=False
)
self.up_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=False
)
self.down_proj = nn.Linear(
config.intermediate_size, config.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 DecoderLayer(nn.Module):
def __init__(self, config: ModelArgs, layer_idx: int):
super().__init__()
self.self_attn = Attention(config, layer_idx)
self.mlp = 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: mx.array = None, cache: Any = None
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
x = x + r
r = self.mlp(self.post_attention_layernorm(x))
return x + r
class BaichuanModel(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.args = config
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
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)
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 cache is None:
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)
class Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.config = config
self.model_type = config.model_type
self.model = BaichuanModel(config)
self.tie_word_embeddings = config.tie_word_embeddings
if not config.tie_word_embeddings:
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
def make_cache(self) -> List[Any]:
caches = []
for i, layer in enumerate(self.model.layers):
is_swa = i in self.config.sliding_window_layers
conv_cache = MambaCache()
if is_swa:
kv_cache = RotatingKVCache(max_size=self.config.sliding_window)
else:
kv_cache = KVCache()
caches.append(CacheList(conv_cache, kv_cache))
return caches
def sanitize(self, weights: dict) -> dict:
is_quantized = "lm_head.scales" in weights
if not is_quantized and "lm_head.weight" in weights:
w = weights["lm_head.weight"]
dtype = w.dtype
w = w.astype(mx.float32)
norm = mx.linalg.norm(w, axis=-1, keepdims=True)
w = (w / (norm + 1e-7)).astype(dtype)
weights["lm_head.weight"] = w
return weights
def __call__(self, inputs: mx.array, cache: Any = None) -> mx.array:
outputs = self.model(inputs, cache)
return self.lm_head(outputs)
@property
def layers(self) -> List[nn.Module]:
return self.model.layers
-405
View File
@@ -1,405 +0,0 @@
# 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
-594
View File
@@ -1,594 +0,0 @@
# 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
+29 -41
View File
@@ -7,6 +7,8 @@ from typing import Any, Optional
import mlx.core as mx
from mlx.utils import tree_map
from .cache import QuantizedKVCache
@dataclass
class BaseModelArgs:
@@ -25,8 +27,7 @@ def create_causal_mask(
N: int,
offset: int = 0,
window_size: Optional[int] = None,
right_padding: Optional[mx.array] = None,
left_padding: Optional[mx.array] = None,
lengths: Optional[mx.array] = None,
):
rinds = mx.arange(offset + N)
linds = mx.arange(offset, offset + N) if offset else rinds
@@ -34,31 +35,34 @@ def create_causal_mask(
rinds = rinds[None]
mask = linds >= rinds
if window_size is not None:
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)
mask = mask & (linds <= rinds + window_size)
if lengths is not None:
lengths = lengths[:, None, None, None]
mask = mask & (rinds < lengths)
return mask
def create_attention_mask(
h, cache=None, window_size: Optional[int] = None, return_array: bool = False
h: mx.array, cache: Optional[Any] = None, return_array: bool = False
):
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
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
def quantized_scaled_dot_product_attention(
@@ -85,15 +89,7 @@ def quantized_scaled_dot_product_attention(
queries, *q_keys, transpose=True, group_size=group_size, bits=bits
)
if mask is not None:
if isinstance(mask, str):
qL, kL = scores.shape[-2:]
q_indices = mx.arange(kL - qL, kL)
k_indices = mx.arange(kL)
mask = q_indices[:, None] >= k_indices[None]
if mask.dtype == mx.bool_:
scores = mx.where(mask, scores, mx.finfo(scores.dtype).min)
else:
scores += mask
scores += mask
scores = mx.softmax(scores, axis=-1, precise=True)
out = mx.quantized_matmul(
scores, *q_values, transpose=False, group_size=group_size, bits=bits
@@ -112,11 +108,8 @@ def scaled_dot_product_attention(
cache,
scale: float,
mask: Optional[mx.array],
sinks: Optional[mx.array] = None,
) -> mx.array:
if hasattr(cache, "bits"):
if sinks is not None:
raise ValueError("Quantized SDPA does not support attention sinks.")
if isinstance(cache, QuantizedKVCache):
return quantized_scaled_dot_product_attention(
queries,
keys,
@@ -128,10 +121,5 @@ def scaled_dot_product_attention(
)
else:
return mx.fast.scaled_dot_product_attention(
queries,
keys,
values,
scale=scale,
mask=mask,
sinks=sinks,
queries, keys, values, scale=scale, mask=mask
)
-158
View File
@@ -1,158 +0,0 @@
# Copyright © 2025 Apple Inc.
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.quantized import QuantizedLinear
from mlx.utils import tree_flatten, tree_unflatten
def bitnet_quantize(model, quantization_config: dict):
quantize_layers = []
modules_to_not_convert = quantization_config.get("modules_to_not_convert", [])
invert_weight_scales = (
quantization_config.get("linear_class", "") != "autobitlinear"
)
for name, module in tree_flatten(model.leaf_modules(), is_leaf=nn.Module.is_module):
# Replace nn.Linear layers, but skip any layer from the `modules_to_not_convert` list
if name not in modules_to_not_convert and isinstance(module, nn.Linear):
old_weight = module.weight
out_features, in_features = old_weight.shape
bias = "bias" in module
new_layer = BitLinear(
in_features,
out_features,
bias=bias,
invert_weight_scales=invert_weight_scales,
)
quantize_layers.append((name, new_layer))
if len(quantize_layers) > 0:
model.update_modules(tree_unflatten(quantize_layers))
return model
def make_bitlinear_kernel():
"""
Custom Metal kernel that performs matrix multiplication directly on
packed weights and scales the output. This eliminates the need to
store unpacked weights in memory.
"""
source = """
constexpr int M = 4;
constexpr int BLOCK = 32;
uint tid = thread_position_in_grid.y;
uint in_offset = thread_position_in_grid.x;
uint batch_idx = tid / (out_features / 4);
uint row_idx = tid % (out_features / 4);
float sum[4] = {0.0};
for (uint i = in_offset * M; i < in_features; i += BLOCK * M) {
float v[M];
for (int j=0; j<M; j++) {
v[j] = x[batch_idx * in_features + i + j];
}
for (int j=0; j<M; j++) {
uint8_t w = packed_weights[row_idx * in_features + i + j];
sum[0] += v[j] * ((w & 3) - 1);
sum[1] += v[j] * (((w >> 2) & 3) - 1);
sum[2] += v[j] * (((w >> 4) & 3) - 1);
sum[3] += v[j] * (((w >> 6) & 3) - 1);
}
}
for (int j=0; j<4; j++) {
sum[j] = simd_sum(sum[j]);
}
// Apply weight scaling by diving them or multiplying them
if (in_offset == 0) {
float scale = invert_weight_scales ? 1 / weight_scale[0] : weight_scale[0];
for (int i=0; i<4; i++) {
out[batch_idx * out_features + row_idx + i * (out_features/4)] = static_cast<T>(sum[i] * scale);
}
}
"""
return mx.fast.metal_kernel(
name="bitlinear_matmul",
input_names=["x", "packed_weights", "weight_scale"],
output_names=["out"],
source=source,
)
_bitlinear_kernel = make_bitlinear_kernel()
class BitLinear(nn.Module):
"""
BitLinear module with memory-efficient weight handling.
"""
def __init__(
self,
in_features,
out_features,
bias=True,
invert_weight_scales=False,
):
super().__init__()
self.in_features = in_features
self.out_features = out_features
# Calculate packed dimensions - the first dimension gets packed 4:1
# The weights are ternary so can be represented with 2 bits, and they
# are packed in uint8 tensors, hence the number of values per item is 4
packed_out_features = (out_features + 3) // 4
self.weight = mx.zeros((packed_out_features, in_features), dtype=mx.uint8)
self.invert_weight_scales = invert_weight_scales
self.weight_scale = mx.array([1.0])
if bias:
self.bias = mx.zeros((out_features,))
else:
self.bias = None
def execute_matmul_kernel(self, x, packed_weights):
original_shape = x.shape
if len(original_shape) > 2:
x = x.reshape(-1, original_shape[-1])
total_batch_elements, in_features = x.shape
out_features = self.out_features
dtype = self.weight_scale.dtype
assert x.dtype == dtype, "Wrong type for input."
out = _bitlinear_kernel(
inputs=[
x,
packed_weights,
self.weight_scale,
],
template=[
("T", dtype),
("invert_weight_scales", self.invert_weight_scales),
("in_features", in_features),
("out_features", out_features),
],
grid=(32, total_batch_elements * out_features // 4, 1),
threadgroup=(32, 1, 1), # SIMD width is 32 threads
output_shapes=[(total_batch_elements, out_features)],
output_dtypes=[dtype],
)[0]
if len(original_shape) > 2:
out = out.reshape(*original_shape[:-1], out_features)
return out
def __call__(self, x):
y = self.execute_matmul_kernel(x, self.weight)
if self.bias is not None:
y = mx.add(y, self.bias)
return y
-208
View File
@@ -1,208 +0,0 @@
# Copyright © 2023-2024 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 .bitlinear_layers import BitLinear
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
num_key_value_heads: int
rms_norm_eps: float
vocab_size: int
head_dim: Optional[int] = None
max_position_embeddings: Optional[int] = None
attention_bias: bool = False
mlp_bias: bool = False
rope_theta: float = 10000
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = True
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
attention_bias = args.attention_bias
self.q_proj = BitLinear(dim, n_heads * head_dim, bias=attention_bias)
self.k_proj = BitLinear(dim, n_kv_heads * head_dim, bias=attention_bias)
self.v_proj = BitLinear(dim, n_kv_heads * head_dim, bias=attention_bias)
self.o_proj = BitLinear(n_heads * head_dim, dim, bias=attention_bias)
self.rope = initialize_rope(
self.head_dim,
args.rope_theta,
args.rope_traditional,
args.rope_scaling,
args.max_position_embeddings,
)
self.attn_sub_norm = 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:
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)
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)
output = self.attn_sub_norm(output)
output = self.o_proj(output)
return output
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_size
hidden_dim = args.intermediate_size
if hasattr(args, "mlp_bias"):
mlp_bias = args.mlp_bias
else:
mlp_bias = False
self.gate_proj = BitLinear(dim, hidden_dim, bias=mlp_bias)
self.down_proj = BitLinear(hidden_dim, dim, bias=mlp_bias)
self.up_proj = BitLinear(dim, hidden_dim, bias=mlp_bias)
self.ffn_sub_norm = nn.RMSNorm(args.intermediate_size, eps=args.rms_norm_eps)
def __call__(self, x) -> mx.array:
x = nn.relu2(self.gate_proj(x)) * self.up_proj(x)
x = self.ffn_sub_norm(x)
x = self.down_proj(x)
return 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
)
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 LlamaModel(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=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, 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 = LlamaModel(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 "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
+20 -780
View File
@@ -1,21 +1,18 @@
# 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,
max_kv_size: Optional[int] = None,
) -> List[Any]:
"""
Construct the model's cache for use in generation.
Construct the model's cache for use when cgeneration.
This function will defer the cache construction to the model if it has a
``make_cache`` method, otherwise it will make a default KV cache.
@@ -74,10 +71,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].from_state(state, meta_state)
for c, state, meta_state in zip(classes, arrays, info)
]
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
if return_metadata:
return cache, metadata
return cache
@@ -109,21 +106,6 @@ 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):
@@ -146,85 +128,13 @@ 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.
Can be used as a mock KV cache or when large blocks are being processed at
a time in which case KVCache isn't necessarily faster. Consider using the
KVCache with a larger step size before using this cache.
"""
def __init__(self):
self.keys = None
self.values = None
self.offset = 0
def update_and_fetch(self, keys, values):
if self.keys is None:
self.keys = keys
self.values = values
else:
self.keys = mx.concatenate([self.keys, keys], axis=-2)
self.values = mx.concatenate([self.values, values], axis=-2)
self.offset = self.keys.shape[-2]
return self.keys, self.values
@property
def state(self):
return self.keys, self.values
@state.setter
def state(self, v):
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
@@ -286,11 +196,11 @@ class QuantizedKVCache(_BaseCache):
@property
def meta_state(self):
return tuple(map(str, (self.offset, self.group_size, self.bits)))
return tuple(map(str, (self.step, self.offset, self.group_size, self.bits)))
@meta_state.setter
def meta_state(self, v):
self.offset, self.group_size, self.bits = map(int, v)
self.step, self.offset, self.group_size, self.bits = map(int, v)
def is_trimmable(self):
return True
@@ -300,17 +210,13 @@ 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
@@ -336,9 +242,6 @@ 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]:
@@ -372,19 +275,16 @@ 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, keep=0):
def __init__(self, max_size=None, keep=0, step=256):
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):
@@ -424,11 +324,10 @@ 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 - 1 to ensure
# The largest size is self.max_size + S to ensure
# every token gets at least self.max_size context
trim_size = self._idx - self.max_size + 1
trim_size = self._idx - self.max_size
self.keys = self._trim(trim_size, self.keys, keys)
self.values = self._trim(trim_size, self.values, values)
self.offset += keys.shape[2]
@@ -483,9 +382,6 @@ 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]:
@@ -499,11 +395,13 @@ class RotatingKVCache(_BaseCache):
@property
def meta_state(self):
return tuple(map(str, (self.keep, self.max_size, self.offset, self._idx)))
return tuple(
map(str, (self.keep, self.max_size, self.step, self.offset, self._idx))
)
@meta_state.setter
def meta_state(self, v):
self.keep, self.max_size, self.offset, self._idx = map(
self.keep, self.max_size, self.step, self.offset, self._idx = map(
int,
v,
)
@@ -520,37 +418,10 @@ 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, left_padding: Optional[List[int]] = None):
self.cache = [None] * size
self.left_padding = mx.array(left_padding) if left_padding else None
class MambaCache(_BaseCache):
def __init__(self):
self.cache = [None, None]
def __setitem__(self, idx, value):
self.cache[idx] = value
@@ -565,634 +436,3 @@ class ArraysCache(_BaseCache):
@state.setter
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, left_padding: Optional[List[int]] = None):
super().__init__(size=2, left_padding=left_padding)
class ChunkedKVCache(KVCache):
def __init__(self, chunk_size):
super().__init__()
self.chunk_size = chunk_size
self.start_position = 0
def maybe_trim_front(self):
# Maintain the cache below the chunk size
if self.keys is not None and self.keys.shape[2] >= self.chunk_size:
self.start_position += self.keys.shape[2] - self.chunk_size
self.keys = self.keys[..., -self.chunk_size :, :]
self.values = self.values[..., -self.chunk_size :, :]
def update_and_fetch(self, keys, values):
prev = self.offset - self.start_position
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]
end = self.offset - self.start_position
self.keys[..., prev:end, :] = keys
self.values[..., prev:end, :] = values
return self.keys[..., :end, :], self.values[..., :end, :]
def trim(self, n):
n = min(self.offset - self.start_position, n)
self.offset -= n
return n
@property
def meta_state(self):
return tuple(map(str, (self.chunk_size, self.start_position)))
@meta_state.setter
def meta_state(self, v):
self.chunk_size, self.start_position = map(int, v)
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]
@state.setter
def state(self, v):
state_lens = [len(c.state) for c in self.caches]
start = 0
for c in self.caches:
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
+7 -4
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -155,15 +155,17 @@ 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)
@@ -180,9 +182,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
out = self.model.embed_tokens.as_linear(out)
out = out * self.model.args.logit_scale
return out
+18 -7
View File
@@ -83,6 +83,11 @@ 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
@@ -143,7 +148,6 @@ 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)
@@ -156,6 +160,7 @@ class CohereModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
@@ -163,9 +168,10 @@ class CohereModel(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
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)
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)
for i, (layer, c) in enumerate(zip(self.layers, cache)):
is_global = (
@@ -173,9 +179,13 @@ class CohereModel(nn.Module):
== self.args.sliding_window_pattern - 1
)
mask = full_mask if is_global else swa_mask
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, mask, c)
h = layer(h, local_mask, c)
return self.norm(h)
@@ -190,9 +200,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
out = self.model.embed_tokens.as_linear(out)
out = out * self.model.args.logit_scale
return out
+9 -5
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -105,9 +105,10 @@ class MLP(nn.Module):
self.v1 = nn.Linear(d_model, ffn_dim, bias=False)
self.w1 = nn.Linear(d_model, ffn_dim, bias=False)
self.w2 = nn.Linear(ffn_dim, d_model, bias=False)
self.act_fn = nn.silu
def __call__(self, x: mx.array) -> mx.array:
current_hidden_states = nn.silu(self.w1(x)) * self.v1(x)
current_hidden_states = self.act_fn(self.w1(x)) * self.v1(x)
current_hidden_states = self.w2(current_hidden_states)
return current_hidden_states
@@ -196,15 +197,17 @@ 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)
@@ -222,9 +225,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.transformer(inputs, cache)
out = self.transformer(inputs, mask, cache)
return self.lm_head(out)
@property
+7 -4
View File
@@ -118,9 +118,10 @@ class DeepseekMLP(nn.Module):
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)
self.act_fn = nn.silu
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
class MoEGate(nn.Module):
@@ -210,14 +211,15 @@ 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)
@@ -236,8 +238,9 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
):
out = self.model(inputs, cache)
out = self.model(inputs, cache, mask)
return self.lm_head(out)
def sanitize(self, weights):
+44 -16
View File
@@ -2,13 +2,12 @@
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional
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 .pipeline import PipelineMixin
from .switch_layers import SwitchGLU
@@ -149,7 +148,7 @@ class DeepseekV2Attention(nn.Module):
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_a_layernorm = nn.RMSNorm(self.q_lora_rank)
self.q_b_proj = nn.Linear(
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
)
@@ -159,7 +158,7 @@ class DeepseekV2Attention(nn.Module):
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_a_layernorm = nn.RMSNorm(self.kv_lora_rank)
self.kv_b_proj = nn.Linear(
self.kv_lora_rank,
self.num_heads
@@ -356,7 +355,7 @@ class DeepseekV2DecoderLayer(nn.Module):
return out
class DeepseekV2Model(PipelineMixin, nn.Module):
class DeepseekV2Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -365,37 +364,65 @@ class DeepseekV2Model(PipelineMixin, 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
# Hack to avoid time-outs during prompt-processing
dist_stream = mx.cpu if h.shape[1] > 1 else mx.gpu
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.pipeline_layers)
mask = create_attention_mask(h, cache[0])
cache = [None] * self.num_layers
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
h = mx.distributed.recv_like(h, (pipeline_rank + 1), stream=dist_stream)
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
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)
h = mx.distributed.send(
h, (pipeline_rank - 1) % pipeline_size, stream=dist_stream
)
# Broadcast h while keeping it in the graph
h = mx.distributed.all_gather(h)[: h.shape[0]]
h = mx.distributed.all_gather(h, stream=dist_stream)[: h.shape[0]]
return self.norm(h)
@@ -412,8 +439,9 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
):
out = self.model(inputs, cache)
out = self.model(inputs, cache, mask)
return self.lm_head(out)
def sanitize(self, weights):
@@ -431,4 +459,4 @@ class Model(nn.Module):
@property
def layers(self):
return self.model.pipeline_layers
return self.model.layers[self.model.start_idx : self.model.end_idx]
+173 -69
View File
@@ -3,14 +3,12 @@
import math
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, Optional
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 .pipeline import PipelineMixin
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@@ -35,9 +33,9 @@ class ModelArgs(BaseModelArgs):
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
n_group: Optional[int] = None
topk_group: Optional[int] = None
num_experts_per_tok: Optional[int] = None
moe_layer_freq: int = 1
first_k_dense_replace: int = 0
max_position_embeddings: int = 2048
@@ -47,6 +45,93 @@ 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 * base ** (
mx.arange(0, dim, 2, dtype=mx.float32) / dim
)
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 DeepseekV3Attention(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
@@ -72,7 +157,7 @@ class DeepseekV3Attention(nn.Module):
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_a_layernorm = nn.RMSNorm(self.q_lora_rank)
self.q_b_proj = nn.Linear(
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
)
@@ -82,7 +167,7 @@ class DeepseekV3Attention(nn.Module):
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_a_layernorm = nn.RMSNorm(self.kv_lora_rank)
self.kv_b_proj = nn.Linear(
self.kv_lora_rank,
self.num_heads
@@ -98,19 +183,35 @@ 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:
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
mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
self.scale = self.scale * mscale * mscale
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,
)
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,
)
def __call__(
self,
@@ -188,24 +289,21 @@ 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
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)
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, 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)
scores = mx.take_along_axis(scores, inds, axis=-1)
if top_k > 1 and norm_topk_prob:
denominator = scores.sum(axis=-1, keepdims=True)
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
scores = scores / denominator
scores = scores * routed_scaling_factor
@@ -247,6 +345,7 @@ class DeepseekV3MoE(nn.Module):
config.hidden_size,
config.moe_intermediate_size,
config.n_routed_experts,
activation=clipped_silu,
)
self.gate = MoEGate(config)
@@ -296,7 +395,7 @@ class DeepseekV3DecoderLayer(nn.Module):
return h + r
class DeepseekV3Model(PipelineMixin, nn.Module):
class DeepseekV3Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -305,37 +404,63 @@ class DeepseekV3Model(PipelineMixin, 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
# Hack to avoid time-outs during prompt-processing
dist_stream = mx.cpu if h.shape[1] > 1 else mx.gpu
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.pipeline_layers)
mask = create_attention_mask(h, cache[0])
cache = [None] * self.num_layers
# 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)
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1), stream=dist_stream)
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)
h = mx.distributed.send(
h, (pipeline_rank - 1) % pipeline_size, stream=dist_stream
)
# Broadcast h while keeping it in the graph
h = mx.distributed.all_gather(h)[: h.shape[0]]
h = mx.distributed.all_gather(h, stream=dist_stream)[: h.shape[0]]
return self.norm(h)
@@ -352,13 +477,13 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
):
out = self.model(inputs, cache)
out = self.model(inputs, cache, mask)
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
@@ -367,27 +492,13 @@ class Model(nn.Module):
weight = weight.reshape(
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
)
scale_inv = scale_inv.astype(weight.dtype)
weight = (weight * scale_inv[:, None, :, None]).reshape(
m + pad_bottom, n + pad_side
)
return weight[:m, :n].astype(dtype)
return weight[:m, :n]
# 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
# Dequantize
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
@@ -421,11 +532,4 @@ class Model(nn.Module):
@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
return self.model.layers[self.model.start_idx : self.model.end_idx]
-515
View File
@@ -1,515 +0,0 @@
# 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]
-315
View File
@@ -1,315 +0,0 @@
# Copyright © 2023-2024 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
num_hidden_layers: int
intermediate_size: int
num_attention_heads: int
rms_norm_eps: float
vocab_size: int
max_position_embeddings: Optional[int]
num_key_value_heads: int
first_k_dense_replace: int
moe_intermediate_size: int
n_routed_experts: int
n_shared_experts: int
norm_topk_prob: bool
num_experts_per_tok: int
rope_theta: float
routed_scaling_factor: float
head_dim: Optional[int] = None
scoring_func: str = ("noaux_tc",)
n_group: Optional[int] = 1
topk_group: Optional[int] = 1
attention_bias: bool = False
mlp_bias: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = False
class Dots1Attention(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 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.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 = initialize_rope(
head_dim,
base=args.rope_theta,
traditional=False,
scaling_config=args.rope_scaling,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
0, 2, 1, 3
)
keys = self.k_norm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
0, 2, 1, 3
)
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
if cache is not None:
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)
@mx.compile
def group_expert_select(
gates,
e_score_correction_bias,
top_k,
n_group,
topk_group,
routed_scaling_factor,
norm_topk_prob,
):
k = top_k
scores = mx.sigmoid(gates.astype(mx.float32))
orig_scores = scores
scores = scores + e_score_correction_bias
k = n_group - topk_group
if k != 0:
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
scores = mx.put_along_axis(scores, 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 Dots1TopkRouter(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.top_k = args.num_experts_per_tok
self.norm_topk_prob = args.norm_topk_prob
self.n_routed_experts = args.n_routed_experts
self.routed_scaling_factor = args.routed_scaling_factor
self.n_group = args.n_group
self.topk_group = args.topk_group
self.weight = mx.zeros((self.n_routed_experts, args.hidden_size))
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
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 Dots1MLP(nn.Module):
def __init__(
self, args: ModelArgs, hidden_size: int = None, intermediate_size: int = None
):
super().__init__()
self.hidden_size = args.hidden_size if hidden_size is None else hidden_size
self.intermediate_size = (
args.intermediate_size if intermediate_size is None else intermediate_size
)
self.gate_proj = nn.Linear(
self.hidden_size, self.intermediate_size, bias=args.mlp_bias
)
self.up_proj = nn.Linear(
self.hidden_size, self.intermediate_size, bias=args.mlp_bias
)
self.down_proj = nn.Linear(
self.intermediate_size, self.hidden_size, bias=args.mlp_bias
)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Dots1MoE(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_experts_per_tok = args.num_experts_per_tok
self.n_shared_experts = args.n_shared_experts
self.experts = SwitchGLU(
args.hidden_size,
args.moe_intermediate_size,
args.n_routed_experts,
)
self.gate = Dots1TopkRouter(args)
self.shared_experts = Dots1MLP(
args=args,
intermediate_size=args.moe_intermediate_size * args.n_shared_experts,
)
def __call__(self, x):
inds, scores = self.gate(x)
y = self.experts(x, inds)
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
if self.n_shared_experts is not None:
y = y + self.shared_experts(x)
return y
class Dots1DecoderLayer(nn.Module):
def __init__(self, args: ModelArgs, layer_idx: int):
super().__init__()
self.self_attn = Dots1Attention(args)
if layer_idx >= args.first_k_dense_replace:
self.mlp = Dots1MoE(args)
else:
self.mlp = Dots1MLP(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))
return h + r
class Dots1Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
Dots1DecoderLayer(args, layer_idx)
for layer_idx 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,
) -> 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 = Dots1Model(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):
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
for l in range(self.args.num_hidden_layers):
prefix = f"model.layers.{l}"
if l >= self.args.first_k_dense_replace:
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.experts.{m}.{k}"] = mx.stack(to_join)
return {k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k}
@property
def layers(self):
return self.model.layers
-164
View File
@@ -1,164 +0,0 @@
# Copyright © 2023-2024 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
from .rope_utils import initialize_rope
@dataclass
class ModelArgs(BaseModelArgs):
hidden_size: int
intermediate_size: int
model_type: str
max_position_embeddings: int
num_attention_heads: int
num_key_value_heads: int
head_dim: Optional[int]
num_hidden_layers: int
rms_norm_eps: float
vocab_size: int
rope_theta: float
use_bias: bool
tie_word_embeddings: bool
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 dim // n_heads
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.use_bias)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.use_bias)
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.use_bias)
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=args.use_bias)
self.rope = initialize_rope(
head_dim,
base=args.rope_theta,
traditional=True,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = 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 MLP(nn.Module):
def __init__(self, dim, hidden_dim, use_bias=False):
super().__init__()
self.gate_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
self.down_proj = nn.Linear(hidden_dim, dim, bias=use_bias)
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class DecoderLayer(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.self_attn = Attention(args)
self.mlp = MLP(args.hidden_size, args.intermediate_size, args.use_bias)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
h = x + r
r = self.mlp(self.post_attention_layernorm(h))
return h + r
class Ernie45Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [DecoderLayer(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 = Ernie45Model(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
@property
def layers(self):
return self.model.layers
-288
View File
@@ -1,288 +0,0 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass, field
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 .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@dataclass
class ModelArgs(BaseModelArgs):
hidden_size: int
intermediate_size: int
model_type: str
max_position_embeddings: int
num_attention_heads: int
num_key_value_heads: int
num_hidden_layers: int
rms_norm_eps: float
vocab_size: int
rope_theta: float
use_bias: bool
tie_word_embeddings: bool
moe_num_experts: int
moe_layer_start_index: int = 0
moe_intermediate_size: int = 0
moe_capacity: list[int] = field(default_factory=list)
moe_k: int = 1
moe_layer_interval: int = 1
moe_use_aux_free: bool = False
moe_num_shared_experts: int = 0
moe_layer_end_index: Optional[int] = None
head_dim: Optional[int] = None
moe_gate_act: str = "softmax"
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 dim // n_heads
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.use_bias)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.use_bias)
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.use_bias)
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=args.use_bias)
self.rope = initialize_rope(
head_dim,
base=args.rope_theta,
traditional=True,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = 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 Ernie4_5_MLP(nn.Module):
def __init__(self, dim, hidden_dim, use_bias=False):
super().__init__()
self.gate_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
self.down_proj = nn.Linear(hidden_dim, dim, bias=use_bias)
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Ernie4_5_MoeMLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.k = args.moe_k
self.moe_intermediate_size = (
args.moe_intermediate_size
if args.moe_intermediate_size
else args.intermediate_size
)
self.gate = nn.Linear(args.hidden_size, args.moe_num_experts, bias=False)
self.switch_mlp = SwitchGLU(
args.hidden_size,
self.moe_intermediate_size,
args.moe_num_experts,
bias=args.use_bias,
)
if getattr(args, "moe_num_shared_experts", 0) > 0:
shared_intermediate_size = (
args.moe_intermediate_size * args.moe_num_shared_experts
if getattr(args, "moe_intermediate_size", None)
else args.intermediate_size * args.moe_num_shared_experts
)
self.shared_experts = Ernie4_5_MLP(
args.hidden_size, shared_intermediate_size, args.use_bias
)
else:
self.shared_experts = None
if args.moe_gate_act == "softmax":
self.gate_act = nn.Softmax()
elif args.moe_gate_act == "sigmoid":
self.gate_act = nn.Sigmoid()
else:
raise ValueError(f"{args.moe_gate_act} is not supported.")
def __call__(self, x: mx.array) -> mx.array:
gates = self.gate(x)
gates = self.gate_act(gates.astype(mx.float32))
k = self.k
inds = mx.stop_gradient(mx.argpartition(-gates, kth=k - 1, axis=-1)[..., :k])
scores = mx.take_along_axis(gates, inds, axis=-1)
scores = scores / mx.maximum(scores.sum(axis=-1, keepdims=True), 1e-12)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
if self.shared_experts is not None:
y = y + self.shared_experts(x)
return y
class Ernie4_5_DecoderLayer(nn.Module):
def __init__(self, args: ModelArgs, layer_idx: int):
super().__init__()
self.self_attn = Attention(args)
moe_layer_start_index = (
min(args.moe_layer_start_index)
if isinstance(args.moe_layer_start_index, (tuple, list))
else args.moe_layer_start_index
)
if args.moe_layer_end_index is None:
moe_layer_end_index = args.num_hidden_layers - 1
else:
moe_layer_end_index = (
max(args.moe_layer_end_index)
if isinstance(args.moe_layer_end_index, (tuple, list))
else args.moe_layer_end_index
)
if (
((layer_idx + 1) % args.moe_layer_interval == 0)
and layer_idx >= moe_layer_start_index
and layer_idx <= moe_layer_end_index
):
self.mlp = Ernie4_5_MoeMLP(args)
else:
self.mlp = Ernie4_5_MLP(
args.hidden_size, args.intermediate_size, args.use_bias
)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
h = x + r
r = self.mlp(self.post_attention_layernorm(h))
return h + r
class Ernie45Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
Ernie4_5_DecoderLayer(args, 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=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 = Ernie45Model(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
@property
def layers(self):
return self.model.layers
def sanitize(self, weights):
remove_patterns = [
"mtp_block.",
"mtp_linear_proj.",
"mtp_hidden_norm.",
"mtp_emb_norm.",
"e_score_correction_bias",
]
weights = {
key: value
for key, value in weights.items()
if not any(pattern in key for pattern in remove_patterns)
}
# Stack experts
for l in range(self.args.num_hidden_layers):
prefix = f"model.layers.{l}"
for m in ["gate_proj", "down_proj", "up_proj"]:
if f"{prefix}.mlp.experts.0.{m}.weight" in weights:
to_join = [
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.weight")
for e in range(self.args.moe_num_experts)
]
weights[f"{prefix}.mlp.switch_mlp.{m}.weight"] = mx.stack(to_join)
return weights
+5 -3
View File
@@ -123,15 +123,16 @@ 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)
@@ -150,9 +151,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.transformer(inputs, cache)
out = self.transformer(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.transformer.wte.as_linear(out)
else:
-219
View File
@@ -1,219 +0,0 @@
# 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 .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
num_key_value_heads: int
max_position_embeddings: int
rope_theta: float
head_dim: int
tie_word_embeddings: bool
rope_scaling: Dict[str, Union[float, str]]
sliding_window: Optional[int]
sliding_window_pattern: Optional[str]
class Attention(nn.Module):
def __init__(self, args: ModelArgs, is_local: Optional[bool]):
super().__init__()
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
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.q_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
self.k_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
self.is_local = is_local or False
self.use_rope = is_local is None or is_local
if self.use_rope:
self.rope = initialize_rope(
head_dim,
base=args.rope_theta,
traditional=False,
scaling_config=args.rope_scaling,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
0, 2, 1, 3
)
keys = self.k_norm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
0, 2, 1, 3
)
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
if cache is not None:
if self.use_rope:
queries = self.rope(queries, offset=cache.offset)
keys = self.rope(keys, offset=cache.offset)
keys, values = cache.update_and_fetch(keys, values)
elif self.use_rope:
queries = self.rope(queries)
keys = self.rope(keys)
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class MLP(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 TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, is_local: bool):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.hidden_size = args.hidden_size
self.self_attn = Attention(args, is_local)
self.mlp = MLP(args.hidden_size, args.intermediate_size)
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.self_attn(x, mask, cache)
h = x + self.post_attention_layernorm(r)
r = self.mlp(h)
out = h + self.post_feedforward_layernorm(r)
return out
class ExaoneModel(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
assert self.vocab_size > 0
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
pattern = args.sliding_window_pattern
self.layers = [
TransformerBlock(
args=args,
is_local=pattern[i % len(pattern)] == "L" if pattern else None,
)
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,
cache=None,
):
h = self.embed_tokens(inputs)
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)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = ExaoneModel(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 make_cache(self):
return [
(
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
if l.self_attn.is_local
else KVCache()
)
for l in self.layers
]
@property
def layers(self):
return self.model.layers
-479
View File
@@ -1,479 +0,0 @@
# 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
-284
View File
@@ -1,284 +0,0 @@
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)
+7 -4
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -138,16 +138,18 @@ 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)
@@ -164,9 +166,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
out = self.model.embed_tokens.as_linear(out)
return out
+7 -4
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -165,16 +165,18 @@ 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)
@@ -192,9 +194,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
out = self.model.embed_tokens.as_linear(out)
out = mx.tanh(out / self.final_logit_softcapping)
out = out * self.final_logit_softcapping
+2 -4
View File
@@ -40,11 +40,9 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
):
return self.language_model(
inputs, cache=cache, input_embeddings=input_embeddings
)
return self.language_model(inputs, cache=cache, mask=mask)
def sanitize(self, weights):
weights = tree_unflatten(list(weights.items()))
+44 -56
View File
@@ -1,15 +1,13 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, Optional
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 .base import BaseModelArgs, create_attention_mask
from .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@dataclass
@@ -23,13 +21,12 @@ class ModelArgs(BaseModelArgs):
rms_norm_eps: float = 1.0e-6
vocab_size: int = 262144
num_key_value_heads: int = 1
rope_theta: float = 1_000_000.0
rope_global_base_freq: 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):
@@ -54,12 +51,14 @@ 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 = 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,
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
),
)
def __call__(
@@ -87,8 +86,11 @@ class Attention(nn.Module):
keys = self.rope(keys)
# Sliding window
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
if isinstance(mask, mx.array) and mask.shape[-1] != keys.shape[-2]:
mask = mask[..., -keys.shape[-2] :]
output = mx.fast.scaled_dot_product_attention(
queries, keys, values, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
@@ -115,16 +117,6 @@ class MLP(nn.Module):
return self.down_proj(nn.gelu_approx(self.gate_proj(x)) * self.up_proj(x))
@partial(mx.compile, shapeless=True)
def clip_residual(x, y):
if x.dtype != mx.float16:
return x + y
bound = mx.finfo(mx.float16).max
return mx.clip(x.astype(mx.float32) + y.astype(mx.float32), -bound, bound).astype(
mx.float16
)
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, layer_idx: int):
super().__init__()
@@ -148,9 +140,9 @@ class TransformerBlock(nn.Module):
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
h = clip_residual(x, self.post_attention_layernorm(r))
h = x + self.post_attention_layernorm(r)
r = self.mlp(self.pre_feedforward_layernorm(h))
out = clip_residual(h, self.post_feedforward_layernorm(r))
out = h + self.post_feedforward_layernorm(r)
return out
@@ -158,8 +150,6 @@ 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,34 +163,34 @@ class Gemma3Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
if input_embeddings is not None:
h = input_embeddings
else:
h = self.embed_tokens(inputs)
h = self.embed_tokens(inputs)
h *= mx.array(self.args.hidden_size**0.5, mx.bfloat16).astype(h.dtype)
if cache is None:
cache = [None] * len(self.layers)
global_mask = create_attention_mask(h, cache[self.sliding_window_pattern - 1])
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)
if self.sliding_window_pattern > 1:
sliding_window_mask = create_attention_mask(
h,
cache[0],
window_size=self.window_size,
)
else:
sliding_window_mask = None
for i, (layer, c) in enumerate(zip(self.layers, cache)):
is_global = (
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
i % self.args.sliding_window_pattern
== self.args.sliding_window_pattern - 1
)
mask = global_mask if is_global else sliding_window_mask
h = layer(h, mask, c)
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)
return self.norm(h)
@@ -212,25 +202,21 @@ 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,
input_embeddings: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
):
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)
out = self.model(inputs, mask, cache)
out = self.lm_head(out)
return out
def sanitize(self, weights):
weights = dict(weights)
if "lm_head.weight" not in weights:
self.tie_word_embeddings = True
self.pop("lm_head")
weights["lm_head.weight"] = weights["model.embed_tokens.weight"]
return weights
@property
@@ -246,5 +232,7 @@ class Model(nn.Module):
):
caches.append(KVCache())
else:
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
caches.append(
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
)
return caches
-613
View File
@@ -1,613 +0,0 @@
# Copyright © 2025 Apple Inc.
import math
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, List, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten, tree_unflatten
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
@dataclass
class TextConfig(BaseModelArgs):
model_type: str
hidden_size: int
num_hidden_layers: int
intermediate_size: int
num_attention_heads: int
head_dim: int
rms_norm_eps: float
vocab_size: int
num_key_value_heads: int
num_kv_shared_layers: int
vocab_size_per_layer_input: int
sliding_window: int
max_position_embeddings: int
rope_local_base_freq: float
rope_theta: float
final_logit_softcapping: float
layer_types: List[str]
activation_sparsity_pattern: List[float]
hidden_size_per_layer_input: int
altup_num_inputs: int
altup_coef_clip: float
altup_correct_scale: bool
altup_active_idx: int
laurel_rank: int
rope_scaling: Optional[Dict] = None
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
text_config: dict
class RMSNoScale(nn.Module):
def __init__(self, eps: float = 1e-5):
super().__init__()
self.eps = eps
def __call__(self, x):
return mx.fast.rms_norm(x, None, self.eps)
class Gemma3nLaurelBlock(nn.Module):
"""Learned Augmented Residual Layer"""
def __init__(self, config: TextConfig):
super().__init__()
self.config = config
self.linear_left = nn.Linear(
self.config.hidden_size, self.config.laurel_rank, bias=False
)
self.linear_right = nn.Linear(
self.config.laurel_rank, self.config.hidden_size, bias=False
)
self.post_laurel_norm = nn.RMSNorm(
dims=self.config.hidden_size,
eps=self.config.rms_norm_eps,
)
def __call__(self, x: mx.array) -> mx.array:
laurel_x = self.linear_left(x)
laurel_x = self.linear_right(laurel_x)
normed_laurel_x = self.post_laurel_norm(laurel_x)
return x + normed_laurel_x
class Gemma3nAttention(nn.Module):
def __init__(self, config: TextConfig, layer_idx: int, is_kv_shared_layer: bool):
super().__init__()
self.is_sliding = config.layer_types[layer_idx] == "sliding_attention"
dim = config.hidden_size
self.n_heads = n_heads = config.num_attention_heads
self.n_kv_heads = n_kv_heads = config.num_key_value_heads
self.repeats = n_heads // n_kv_heads
self.head_dim = head_dim = config.head_dim
self.layer_idx = layer_idx
self.scale = 1.0
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.q_norm = nn.RMSNorm(dims=config.head_dim, eps=config.rms_norm_eps)
self.k_norm = nn.RMSNorm(dims=config.head_dim, eps=config.rms_norm_eps)
self.v_norm = RMSNoScale(eps=config.rms_norm_eps)
self.is_kv_shared_layer = is_kv_shared_layer
self.rope = nn.RoPE(
head_dim,
traditional=False,
base=(
config.rope_local_base_freq if self.is_sliding else config.rope_theta
),
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, _ = x.shape
queries = self.q_proj(x)
queries = queries.reshape(B, L, -1, self.head_dim)
queries = self.q_norm(queries)
offset = 0
if self.is_kv_shared_layer and cache is not None:
# For shared layers, retrieve KV from the designated cache layer
keys, values = cache.state
offset = cache.offset
else:
if cache is not None:
offset = cache.offset
keys = self.k_proj(x).reshape(B, L, -1, self.head_dim)
keys = self.k_norm(keys)
keys = keys.transpose(0, 2, 1, 3)
keys = self.rope(keys, offset=offset)
values = self.v_proj(x).reshape(B, L, -1, self.head_dim)
values = self.v_norm(values)
values = values.transpose(0, 2, 1, 3)
if cache is not None:
keys, values = cache.update_and_fetch(keys, values)
queries = queries.transpose(0, 2, 1, 3)
queries = self.rope(queries, offset=offset)
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)
@partial(mx.compile, shapeless=True)
def gelu_topk(inputs, std_multiplier):
inputs_mean = mx.mean(inputs, axis=-1, keepdims=True)
inputs_std = mx.std(inputs, axis=-1, keepdims=True)
cutoff_x = inputs_mean + inputs_std * std_multiplier.astype(inputs_std.dtype)
return nn.gelu_approx(mx.maximum(0, inputs - cutoff_x))
class MLP(nn.Module):
def __init__(self, config: TextConfig, layer_idx: int = 0):
super().__init__()
self.config = config
self.hidden_size = config.hidden_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)
if config.activation_sparsity_pattern is not None:
self.activation_sparsity = config.activation_sparsity_pattern[layer_idx]
else:
self.activation_sparsity = 0.0
if self.activation_sparsity > 0:
self._std_multiplier = math.sqrt(2.0) * mx.erfinv(
2 * self.activation_sparsity - 1
)
def __call__(self, x: mx.array):
gate_proj = self.gate_proj(x)
if self.activation_sparsity > 0.0:
activations = gelu_topk(gate_proj, self._std_multiplier)
else:
activations = nn.gelu_approx(gate_proj)
up_proj = self.up_proj(x)
down_proj = self.down_proj(activations * up_proj)
return down_proj
class Gemma3nAltUp(nn.Module):
"""Alternating Updates (AltUp)"""
def __init__(self, config: TextConfig):
super().__init__()
self.config = config
self.correct_output_scale = mx.zeros((self.config.hidden_size,))
self.correction_coefs = nn.Linear(
self.config.altup_num_inputs, self.config.altup_num_inputs, bias=False
)
self.prediction_coefs = nn.Linear(
self.config.altup_num_inputs, self.config.altup_num_inputs**2, bias=False
)
self.modality_router = nn.Linear(
self.config.hidden_size, self.config.altup_num_inputs, bias=False
)
self.router_norm = nn.RMSNorm(
dims=self.config.hidden_size,
eps=self.config.rms_norm_eps,
)
def compute_router_modalities(self, x: mx.array) -> mx.array:
router_inputs = self.router_norm(x) * (self.config.hidden_size**-1.0)
routed = self.modality_router(router_inputs).astype(mx.float32)
return mx.tanh(routed)
def predict(self, x: mx.array) -> mx.array:
modalities = self.compute_router_modalities(x[self.config.altup_active_idx])
self.prediction_coefs.weight = self.prediction_coefs.weight.astype(mx.float32)
if self.config.altup_coef_clip is not None:
self.prediction_coefs.weight = mx.clip(
self.prediction_coefs.weight,
-self.config.altup_coef_clip,
self.config.altup_coef_clip,
)
all_coefs = (
self.prediction_coefs(modalities)
.reshape(
*modalities.shape[:-1],
self.config.altup_num_inputs,
self.config.altup_num_inputs,
)
.transpose(0, 1, 3, 2)
)
x_up = x.astype(mx.float32)
x_permuted = x_up.transpose(1, 2, 3, 0)
predictions = mx.matmul(x_permuted, all_coefs)
predictions = predictions.transpose(3, 0, 1, 2)
predictions += x_up
return predictions.astype(x.dtype)
def correct(self, predictions: mx.array, activated: mx.array):
modalities = self.compute_router_modalities(activated)
self.correction_coefs.weight = self.correction_coefs.weight.astype(mx.float32)
if self.config.altup_coef_clip is not None:
self.correction_coefs.weight = mx.clip(
self.correction_coefs.weight,
-self.config.altup_coef_clip,
self.config.altup_coef_clip,
)
all_coefs = self.correction_coefs(modalities) + 1.0
active_x = predictions[self.config.altup_active_idx]
innovation = activated - active_x
all_coefs = all_coefs.moveaxis(2, 0)
corrected = innovation[None] * all_coefs[..., None]
corrected += predictions
return corrected.astype(activated.dtype)
class Gemma3nDecoderLayer(nn.Module):
def __init__(self, config: TextConfig, layer_idx: int, is_kv_shared_layer: bool):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.layer_idx = layer_idx
self.self_attn = Gemma3nAttention(config, layer_idx, is_kv_shared_layer)
self.mlp = MLP(config, layer_idx=layer_idx)
self.input_layernorm = nn.RMSNorm(
self.hidden_size,
eps=config.rms_norm_eps,
)
self.post_attention_layernorm = nn.RMSNorm(
self.hidden_size,
eps=config.rms_norm_eps,
)
self.pre_feedforward_layernorm = nn.RMSNorm(
self.hidden_size,
eps=config.rms_norm_eps,
)
self.post_feedforward_layernorm = nn.RMSNorm(
self.hidden_size,
eps=config.rms_norm_eps,
)
self.is_sliding = self.self_attn.is_sliding
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
self.altup = Gemma3nAltUp(config)
self.laurel = Gemma3nLaurelBlock(config)
self.per_layer_input_gate = nn.Linear(
self.hidden_size, self.hidden_size_per_layer_input, bias=False
)
self.per_layer_projection = nn.Linear(
self.hidden_size_per_layer_input, self.hidden_size, bias=False
)
self.post_per_layer_input_norm = nn.RMSNorm(
self.hidden_size,
eps=config.rms_norm_eps,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
per_layer_input: Optional[mx.array] = None,
):
predictions = self.altup.predict(x)
active_prediction = predictions[self.config.altup_active_idx]
active_prediction_normed = self.input_layernorm(active_prediction)
laurel_output = self.laurel(active_prediction_normed)
attn = self.self_attn(
active_prediction_normed,
mask,
cache,
)
attn = self.post_attention_layernorm(attn)
attn_gated = active_prediction + attn
attn_laurel = (attn_gated + laurel_output) * (2.0**-0.5)
attn_norm = self.pre_feedforward_layernorm(attn_laurel)
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]
if self.config.altup_correct_scale:
first_prediction = first_prediction * self.altup.correct_output_scale
first_prediction = self.per_layer_input_gate(first_prediction)
first_prediction = nn.gelu_approx(first_prediction)
first_prediction = mx.multiply(first_prediction, per_layer_input)
first_prediction = self.per_layer_projection(first_prediction)
first_prediction = self.post_per_layer_input_norm(first_prediction)
corrected_predictions[1:] = corrected_predictions[1:] + first_prediction
return corrected_predictions
@partial(mx.compile, shapeless=True)
def logit_softcap(softcap, x):
out = mx.tanh(x / softcap)
out = out * softcap
return out
class LanguageModel(nn.Module):
def __init__(self, config: TextConfig):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
self.vocab_size = config.vocab_size
self.vocab_size_per_layer_input = config.vocab_size_per_layer_input
self.num_hidden_layers = config.num_hidden_layers
self.final_logit_softcapping = config.final_logit_softcapping
self.first_kv_shared_layer_idx = (
config.num_hidden_layers - config.num_kv_shared_layers
)
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
self.layers = [
Gemma3nDecoderLayer(
config=config,
layer_idx=layer_idx,
is_kv_shared_layer=layer_idx >= self.first_kv_shared_layer_idx,
)
for layer_idx in range(config.num_hidden_layers)
]
self.embed_tokens_per_layer = nn.Embedding(
config.vocab_size_per_layer_input,
config.num_hidden_layers * config.hidden_size_per_layer_input,
)
self.per_layer_model_projection = nn.Linear(
config.hidden_size,
config.num_hidden_layers * config.hidden_size_per_layer_input,
bias=False,
)
self.per_layer_projection_norm = nn.RMSNorm(
dims=config.hidden_size_per_layer_input,
eps=config.rms_norm_eps,
)
self.altup_projections = [
nn.Linear(config.hidden_size, config.hidden_size, bias=False)
for _ in range(1, self.config.altup_num_inputs)
]
self.altup_unembed_projections = [
nn.Linear(config.hidden_size, config.hidden_size, bias=False)
for _ in range(1, self.config.altup_num_inputs)
]
self.norm = nn.RMSNorm(
config.hidden_size,
eps=config.rms_norm_eps,
)
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 = config.layer_types[: self.first_kv_shared_layer_idx]
shared_full_idx = (
len(concrete_layers) - 1 - concrete_layers[::-1].index("full_attention")
)
shared_sliding_idx = (
len(concrete_layers) - 1 - concrete_layers[::-1].index("sliding_attention")
)
self.layer_idx_to_cache_idx = []
for i, layer_type in enumerate(self.config.layer_types):
if i < self.first_kv_shared_layer_idx:
self.layer_idx_to_cache_idx.append(i)
else:
if layer_type == "full_attention":
self.layer_idx_to_cache_idx.append(shared_full_idx)
elif layer_type == "sliding_attention":
self.layer_idx_to_cache_idx.append(shared_sliding_idx)
else:
raise NotImplementedError(f"Unknown layer type: {layer_type}")
def __call__(
self,
inputs: mx.array = None,
cache=None,
input_embeddings: mx.array = None,
):
if input_embeddings is None:
h = self.embed_tokens(inputs) * (self.hidden_size**0.5)
else:
h = input_embeddings
per_layer_inputs = self.get_per_layer_inputs(inputs)
per_layer_inputs = self.project_per_layer_inputs(h, per_layer_inputs)
if cache is None:
cache = [None] * len(self.layers)
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
target_magnitude = mx.mean(h0**2, axis=-1, keepdims=True) ** 0.5
h_list = [h0]
h_list.extend([proj(h0) for proj in self.altup_projections])
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"
if is_global:
mask = global_mask
else:
mask = sliding_window_mask
h = layer(
h,
mask,
cache[self.layer_idx_to_cache_idx[i]],
per_layer_input,
)
# Per-layer inputs to single output
target_magnitude = mx.mean(h[0] ** 2, axis=-1, keepdims=True) ** 0.5
for i, proj in enumerate(self.altup_unembed_projections):
h[i + 1] = proj(h[i + 1])
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))
h = mx.mean(h, axis=0)
out = self.norm(h)
out = self.embed_tokens.as_linear(out)
if self.final_logit_softcapping is not None:
out = logit_softcap(self.final_logit_softcapping, out)
return out
def get_per_layer_inputs(self, input_ids: mx.array) -> mx.array:
per_layer_inputs_mask = input_ids < self.vocab_size_per_layer_input
tokens = mx.where(per_layer_inputs_mask, input_ids, mx.zeros_like(input_ids))
result = self.embed_tokens_per_layer(tokens) * (
self.hidden_size_per_layer_input**0.5
)
return result.reshape(
*input_ids.shape,
self.num_hidden_layers,
self.hidden_size_per_layer_input,
)
def project_per_layer_inputs(
self,
inputs_embeds: mx.array,
per_layer_inputs: mx.array,
) -> mx.array:
per_layer_projection = self.per_layer_model_projection(inputs_embeds) * (
self.hidden_size**-0.5
)
per_layer_projection = per_layer_projection.reshape(
*inputs_embeds.shape[:-1],
self.config.num_hidden_layers,
self.config.hidden_size_per_layer_input,
)
per_layer_projection = self.per_layer_projection_norm(per_layer_projection)
return (per_layer_projection + per_layer_inputs) * (2.0**-0.5)
def make_cache(self):
caches = []
for layer_type in self.config.layer_types[: self.first_kv_shared_layer_idx]:
if layer_type == "full_attention":
caches.append(KVCache())
elif layer_type == "sliding_attention":
caches.append(
RotatingKVCache(max_size=self.config.sliding_window, keep=0)
)
else:
raise NotImplementedError(f"Unknown layer type: {layer_type}")
return caches
class Gemma3n(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.language_model = LanguageModel(TextConfig.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 make_cache(self):
return self.language_model.make_cache()
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model = Gemma3n(args)
self.model_type = args.model_type
def __call__(
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
return self.model(inputs, cache=cache, input_embeddings=input_embeddings)
def sanitize(self, weights):
weights = tree_unflatten(list(weights.items()))
for k in ["vision_tower", "audio_tower", "embed_audio", "embed_vision"]:
weights["model"].pop(k, None)
return dict(tree_flatten(weights))
@property
def layers(self):
return self.model.language_model.layers
def make_cache(self):
return self.model.make_cache()
-187
View File
@@ -1,187 +0,0 @@
# 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
-180
View File
@@ -1,180 +0,0 @@
# 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_hidden_layers: int
intermediate_size: int
num_attention_heads: int
attention_bias: bool
head_dim: int
rms_norm_eps: float
vocab_size: int
num_key_value_heads: int
partial_rotary_factor: float
rope_theta: float
rope_traditional: bool = True
max_position_embeddings: int = 32768
class Glm4MLP(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, up_states = mx.split(x, 2, axis=-1)
return self.down_proj(nn.silu(gate) * up_states)
class Glm4Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.head_dim = getattr(
args, "head_dim", args.hidden_size // args.num_attention_heads
)
self.n_heads = args.num_attention_heads
self.n_kv_heads = args.num_key_value_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=False
)
self.rope = nn.RoPE(
dims=int(self.head_dim * args.partial_rotary_factor),
base=args.rope_theta,
traditional=args.rope_traditional,
)
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 Glm4DecoderLayer(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.self_attn = Glm4Attention(args=args)
self.mlp = Glm4MLP(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.post_self_attn_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:
x = x + self.post_self_attn_layernorm(
self.self_attn(self.input_layernorm(x), mask, cache)
)
residual = x
x = (
self.post_mlp_layernorm(self.mlp(self.post_attention_layernorm(x)))
+ residual
)
return x
class Glm4Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
Glm4DecoderLayer(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,
):
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, 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 = Glm4Model(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
-341
View File
@@ -1,341 +0,0 @@
# 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
+15 -14
View File
@@ -1,10 +1,11 @@
# Copyright © 2023 - 2024 Apple Inc.
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
import numpy as np
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -125,26 +126,25 @@ class GPT2Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
_, L = inputs.shape
hidden_states = self.wte(inputs)
mask = None
if hidden_states.shape[1] > 1:
position_ids = mx.array(np.arange(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,9 +161,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
out = self.model.wte.as_linear(out)
return out
+8 -4
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
@@ -137,20 +137,23 @@ 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):
@@ -171,9 +174,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.transformer(inputs, cache)
out = self.transformer(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.transformer.wte.as_linear(out)
else:
+14 -20
View File
@@ -1,10 +1,11 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
import numpy as np
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -23,7 +24,6 @@ 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):
@@ -108,7 +108,6 @@ 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,
@@ -123,20 +122,12 @@ class TransformerBlock(nn.Module):
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
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
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
class GPTNeoXModel(nn.Module):
@@ -155,17 +146,19 @@ 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)
@@ -185,9 +178,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
return out
def sanitize(self, weights):
-291
View File
@@ -1,291 +0,0 @@
# 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
+6 -3
View File
@@ -150,15 +150,17 @@ 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)
@@ -178,9 +180,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
-235
View File
@@ -1,235 +0,0 @@
# 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
-541
View File
@@ -1,541 +0,0 @@
# 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
+7 -4
View File
@@ -1,7 +1,7 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -136,15 +136,17 @@ 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)
@@ -168,9 +170,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
) -> mx.array:
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+14 -26
View File
@@ -1,5 +1,6 @@
# Copyright © 2023-2024 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple, Union
@@ -29,7 +30,6 @@ class ModelArgs(BaseModelArgs):
rope_theta: float
use_cla: bool
cla_share_factor: 2
moe_intermediate_size: Optional[Union[int, list]] = None
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = False
@@ -41,12 +41,6 @@ class ModelArgs(BaseModelArgs):
raise ValueError(f"rope_scaling must contain keys {required_keys}")
def _int_or_list(arg, idx):
if isinstance(arg, list):
return arg[idx]
return arg
class DynamicNTKAlphaRoPE(nn.Module):
def __init__(
self,
@@ -161,29 +155,20 @@ class Gate(nn.Module):
class MoeBlock(nn.Module):
def __init__(self, args: ModelArgs, layer_idx: int = 0):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_size
intermediate_size = args.intermediate_size
self.use_shared_mlp = args.use_mixed_mlp_moe
if args.use_mixed_mlp_moe:
num_shared = _int_or_list(args.num_shared_expert, layer_idx)
self.shared_mlp = MLP(dim, int(intermediate_size * num_shared))
self.shared_mlp = MLP(dim, intermediate_size * args.num_shared_expert)
self.num_experts = num_experts = args.num_experts
self.top_k = _int_or_list(args.moe_topk, layer_idx)
self.top_k = args.moe_topk
self.gate = Gate(dim, num_experts)
# Use moe_intermediate_size if available, otherwise use intermediate_size
expert_intermediate_size = intermediate_size
if args.moe_intermediate_size is not None:
expert_intermediate_size = _int_or_list(
args.moe_intermediate_size, layer_idx
)
self.switch_mlp = SwitchGLU(dim, expert_intermediate_size, num_experts)
self.switch_mlp = SwitchGLU(dim, intermediate_size, num_experts)
def __call__(
self,
@@ -197,7 +182,7 @@ class MoeBlock(nn.Module):
scores = mx.take_along_axis(gates, inds, axis=-1)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None].astype(mx.float32)).sum(axis=-2).astype(y.dtype)
y = (y * scores[..., None]).sum(axis=-2)
if self.use_shared_mlp:
shared_expert_output = self.shared_mlp(x)
@@ -207,14 +192,14 @@ class MoeBlock(nn.Module):
class DecoderLayer(nn.Module):
def __init__(self, args: ModelArgs, kv_proj: bool, layer_idx: int = 0):
def __init__(self, args: ModelArgs, kv_proj: bool):
super().__init__()
self.hidden_size = args.hidden_size
self.self_attn = Attention(kv_proj, args)
if args.num_experts == 1:
self.mlp = MLP(args.hidden_size, args.intermediate_size)
else:
self.mlp = MoeBlock(args, layer_idx)
self.mlp = MoeBlock(args)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
@@ -250,7 +235,6 @@ class HunYuanModel(nn.Module):
DecoderLayer(
args=args,
kv_proj=(not args.use_cla) or (i % args.cla_share_factor) == 0,
layer_idx=i,
)
for i in range(args.num_hidden_layers)
]
@@ -259,14 +243,17 @@ 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
@@ -285,9 +272,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
return self.model.embed_tokens.as_linear(out)
def sanitize(self, weights):
-230
View File
@@ -1,230 +0,0 @@
# 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
+7 -3
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
@@ -193,14 +193,17 @@ 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)
@@ -219,9 +222,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.tok_embeddings.as_linear(out)
else:
+7 -3
View File
@@ -1,7 +1,7 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
@@ -193,14 +193,17 @@ 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)
@@ -219,9 +222,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
-384
View File
@@ -1,384 +0,0 @@
# 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
-575
View File
@@ -1,575 +0,0 @@
# 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
-116
View File
@@ -1,116 +0,0 @@
# Copyright © 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
from .deepseek_v3 import DeepseekV3Model
@dataclass
class TextArgs(BaseModelArgs):
vocab_size: int = 102400
hidden_size: int = 4096
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
@dataclass
class ModelArgs(BaseModelArgs):
text_config: Union[TextArgs, dict]
model_type: str
def __post_init__(self):
self.text_config = TextArgs.from_dict(self.text_config)
class LanguageModel(nn.Module):
def __init__(self, config: TextArgs):
super().__init__()
self.args = config
self.model = DeepseekV3Model(config)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
):
out = self.model(inputs, cache)
return self.lm_head(out)
class Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.args = config
self.model_type = config.model_type
self.language_model = LanguageModel(config.text_config)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
):
return self.language_model(inputs, cache)
def sanitize(self, weights):
def keep(key):
return (
"vision_tower" not in key
and "rotary_emb" not in key
and "multi_modal_projector" not in key
)
weights = {k: v for k, v in weights.items() if keep(k)}
# Stack experts
for l in range(self.args.text_config.num_hidden_layers):
prefix = f"language_model.model.layers.{l}"
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.text_config.n_routed_experts)
]
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
return weights
@property
def layers(self):
return self.language_model.model.layers
@property
def cast_predicate(self):
def predicate(k):
return "e_score_correction_bias" not in k
return predicate
-51
View File
@@ -1,51 +0,0 @@
# 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()
-301
View File
@@ -1,301 +0,0 @@
# 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
@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
max_position_embeddings: int
norm_eps: float
conv_bias: bool
conv_L_cache: int
block_dim: int
block_ff_dim: int
block_multiple_of: int
block_ffn_dim_multiplier: float
block_auto_adjust_ff_dim: bool
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):
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,
dim: int,
ff_dim: int,
multiple_of: int,
auto_adjust_ff_dim: bool,
ffn_dim_multiplier: Optional[float],
):
super().__init__()
if auto_adjust_ff_dim:
ff_dim = int(2 * ff_dim / 3)
if ffn_dim_multiplier is not None:
ff_dim = int(ffn_dim_multiplier * ff_dim)
ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
self.w1 = nn.Linear(dim, ff_dim, bias=False)
self.w3 = nn.Linear(dim, ff_dim, bias=False)
self.w2 = nn.Linear(ff_dim, dim, bias=False)
def __call__(self, x) -> mx.array:
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
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(
dim=args.block_dim,
ff_dim=args.block_ff_dim,
multiple_of=args.block_multiple_of,
auto_adjust_ff_dim=args.block_auto_adjust_ff_dim,
ffn_dim_multiplier=args.block_ffn_dim_multiplier,
)
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)
sanitized_weights[name] = param
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
]
-372
View File
@@ -1,372 +0,0 @@
# 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
-154
View File
@@ -1,154 +0,0 @@
# 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}
+12 -45
View File
@@ -1,13 +1,12 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Union
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 .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@@ -29,16 +28,11 @@ 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):
@@ -120,11 +114,10 @@ class MLP(nn.Module):
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, use_sliding: bool = False):
def __init__(self, args: ModelArgs):
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)
@@ -152,45 +145,29 @@ 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, use_sliding=layer_type == "sliding_attention")
for layer_type in self.layer_types
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
]
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,
):
if input_embeddings is not None:
h = input_embeddings
else:
h = self.embed_tokens(inputs)
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
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)
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
return self.norm(h)
@@ -207,10 +184,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, cache, input_embeddings)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
@@ -229,13 +206,3 @@ 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
]
-324
View File
@@ -1,324 +0,0 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, 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 ChunkedKVCache, KVCache
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@dataclass
class TextArgs(BaseModelArgs):
attention_bias: bool
attention_chunk_size: int
head_dim: int
hidden_size: int
interleave_moe_layer_step: int
intermediate_size: int
intermediate_size_mlp: int
max_position_embeddings: int
model_type: str
num_attention_heads: int
num_experts_per_tok: int
num_hidden_layers: int
num_key_value_heads: int
num_local_experts: int
rms_norm_eps: float
rope_scaling: Any
rope_theta: float
use_qk_norm: bool
vocab_size: int
attn_temperature_tuning: int = 4
floor_scale: int = 8192
attn_scale: float = 0.1
@dataclass
class ModelArgs(BaseModelArgs):
text_config: Union[TextArgs, dict]
model_type: str
def __post_init__(self):
self.text_config = TextArgs.from_dict(self.text_config)
class Attention(nn.Module):
def __init__(self, args: TextArgs, layer_idx: int):
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.use_rope = int((layer_idx + 1) % 4 != 0) # rope unused for dense layers
self.attn_temperature_tuning = args.attn_temperature_tuning
self.floor_scale = args.floor_scale
self.attn_scale = args.attn_scale
self.head_dim = head_dim = args.head_dim or args.hidden_size // n_heads
self.scale = head_dim**-0.5
if hasattr(args, "attention_bias"):
attention_bias = args.attention_bias
else:
attention_bias = False
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.use_qk_norm = args.use_qk_norm and self.use_rope
if self.use_rope:
self.rope = initialize_rope(
head_dim,
args.rope_theta,
traditional=True,
scaling_config=args.rope_scaling,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = 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:
offset = cache.offset
else:
offset = 0
if self.use_rope:
queries = self.rope(queries, offset=offset)
keys = self.rope(keys, offset=offset)
if self.use_qk_norm:
queries = mx.fast.rms_norm(queries, weight=None, eps=1e-6)
keys = mx.fast.rms_norm(keys, weight=None, eps=1e-6)
if self.attn_temperature_tuning and not self.use_rope:
attn_scales = (
mx.log(
mx.floor(mx.arange(offset + 1, offset + L + 1) / self.floor_scale)
+ 1.0
)
* self.attn_scale
+ 1.0
)
attn_scales = attn_scales[:, None]
queries = (queries * attn_scales).astype(queries.dtype)
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, intermediate_size: int = None):
super().__init__()
dim = args.hidden_size
hidden_dim = intermediate_size or 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 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
)
self.router = nn.Linear(args.hidden_size, args.num_local_experts, bias=False)
self.shared_expert = MLP(args)
def __call__(self, x) -> mx.array:
logits = self.router(x)
k = self.top_k
indices = mx.argpartition(-logits, kth=k - 1, axis=-1)[..., :k]
scores = mx.take_along_axis(logits, indices, axis=-1)
scores = mx.sigmoid(scores.astype(mx.float32)).astype(x.dtype)
out = self.experts(x * scores, indices).squeeze(2)
return out + self.shared_expert(x)
class TransformerBlock(nn.Module):
def __init__(self, args: TextArgs, layer_idx: int):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.hidden_size = args.hidden_size
self.self_attn = Attention(args, layer_idx)
self.is_moe_layer = (layer_idx % args.interleave_moe_layer_step) == (
args.interleave_moe_layer_step - 1
)
if self.is_moe_layer:
self.feed_forward = MoE(args)
else:
self.feed_forward = MLP(args, 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
)
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.feed_forward(self.post_attention_layernorm(h))
out = h + r
return out
class LlamaModel(nn.Module):
def __init__(self, args: TextArgs):
super().__init__()
self.args = args
self.vocab_size = args.vocab_size
self.num_hidden_layers = args.num_hidden_layers
assert self.vocab_size > 0
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [TransformerBlock(args, i) for i in range(args.num_hidden_layers)]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.attention_chunk_size = args.attention_chunk_size
def __call__(
self,
inputs: mx.array,
cache=None,
):
h = self.embed_tokens(inputs)
if cache is not None:
for idx, c in enumerate(cache):
if (idx + 1) % 4 != 0:
c.maybe_trim_front()
start = cache[0].start_position
offset = cache[0].offset
else:
start = 0
offset = 0
end = offset + h.shape[1]
linds = mx.arange(start, end)
rinds = mx.arange(offset, end)[:, None]
block_pos = mx.abs(
(linds // self.attention_chunk_size) - (rinds // self.attention_chunk_size)
)
token_pos = linds <= rinds
chunk_mask = (block_pos == 0) & token_pos
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
mask = chunk_mask if use_chunked_attention else global_mask
h = layer(h, mask, cache=c)
return self.norm(h)
class LanguageModel(nn.Module):
def __init__(self, args: TextArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = LlamaModel(self.args)
self.lm_head = nn.Linear(
self.args.hidden_size, self.args.vocab_size, bias=False
)
def __call__(
self,
inputs: mx.array,
cache=None,
):
out = self.model(inputs, cache)
return self.lm_head(out)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.language_model = LanguageModel(args.text_config)
def __call__(
self,
inputs: mx.array,
cache=None,
):
return self.language_model(inputs, cache)
def sanitize(self, weights):
def to_remove(k):
return "vision_model" in k or "multi_modal_projector" in k
# Remove vision weights
weights = {k: v for k, v in weights.items() if not to_remove(k)}
# Rename expert weights for SwitchGLU
for l in range(self.args.text_config.num_hidden_layers):
prefix = f"language_model.model.layers.{l}.feed_forward.experts"
if f"{prefix}.gate_up_proj" in weights:
v = weights.pop(f"{prefix}.gate_up_proj")
gate_k = f"{prefix}.gate_proj.weight"
up_k = f"{prefix}.up_proj.weight"
gate_proj, up_proj = mx.split(v, 2, axis=-1)
weights[gate_k] = mx.swapaxes(gate_proj, 1, 2)
weights[up_k] = mx.swapaxes(up_proj, 1, 2)
if f"{prefix}.down_proj" in weights:
down_proj = weights.pop(f"{prefix}.down_proj")
weights[f"{prefix}.down_proj.weight"] = mx.swapaxes(down_proj, 1, 2)
return weights
@property
def layers(self):
return self.language_model.model.layers
def make_cache(self):
chunk_size = self.args.text_config.attention_chunk_size
caches = []
for i in range(len(self.layers)):
if (i + 1) % 4 != 0:
caches.append(ChunkedKVCache(chunk_size))
else:
caches.append(KVCache())
return caches
-181
View File
@@ -1,181 +0,0 @@
# 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
-381
View File
@@ -1,381 +0,0 @@
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]
+40 -18
View File
@@ -50,6 +50,32 @@ 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__()
@@ -71,13 +97,11 @@ class MambaBlock(nn.Module):
self.hidden_size, self.intermediate_size * 2, bias=args.use_bias
)
self.conv1d = nn.Conv1d(
in_channels=self.intermediate_size,
out_channels=self.intermediate_size,
self.conv1d = DepthWiseConv1d(
channels=self.intermediate_size,
kernel_size=self.conv_kernel_size,
groups=self.intermediate_size,
bias=self.use_conv_bias,
padding=0,
padding=self.conv_kernel_size - 1,
)
self.x_proj = nn.Linear(
@@ -124,15 +148,13 @@ class MambaBlock(nn.Module):
B, T, D = x.shape
xz = self.in_proj(x)
x, z = xz.split(indices_or_sections=2, axis=-1)
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) :, :]
conv_out, new_conv_cache = self.conv1d(x, conv_cache)
x = nn.silu(conv_out)
A = -mx.exp(self.A_log)
outputs = []
current_state = state_cache
y = []
for t in range(T):
@@ -206,15 +228,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
-245
View File
@@ -1,245 +0,0 @@
# 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
-193
View File
@@ -1,193 +0,0 @@
# 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
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
num_key_value_heads: int
max_position_embeddings: int = 32768
rope_theta: float = 10000.0
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = False
num_nextn_predict_layers: int = 2
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
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.hidden_size // n_heads
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=True)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=True)
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=True)
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
self.rope = initialize_rope(
head_dim,
base=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
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 MLP(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 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.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
)
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 MiMoModel(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.num_nextn_predict_layers = args.num_nextn_predict_layers
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)
]
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)
h = self.norm(h)
return h
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = MiMoModel(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):
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
return {
k: v
for k, v in weights.items()
if "self_attn.rotary_emb.inv_freq" not in k
and not k.startswith("model.mtp_layers.")
}
@property
def layers(self):
return self.model.layers
+17 -11
View File
@@ -7,7 +7,6 @@ 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
@@ -23,7 +22,6 @@ class ModelArgs(BaseModelArgs):
num_key_value_heads: int
scale_depth: float
scale_emb: float
max_position_embeddings: Optional[int] = None
rope_theta: float = 1000000.0
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[str, float]]] = None
@@ -69,12 +67,17 @@ class Attention(nn.Module):
self.num_heads * self.head_dim, self.hidden_size, bias=False
)
self.rope = initialize_rope(
self.head_dim,
args.rope_theta,
args.rope_traditional,
args.rope_scaling,
args.max_position_embeddings,
rope_scale = (
1 / args.rope_scaling["factor"]
if args.rope_scaling is not None and args.rope_scaling["type"] == "linear"
else 1
)
self.rope = nn.RoPE(
dims=self.head_dim,
traditional=args.rope_traditional,
base=self.rope_theta,
scale=rope_scale,
)
def __call__(
@@ -154,15 +157,17 @@ 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)
@@ -182,9 +187,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if not self.args.tie_word_embeddings:
out = self.lm_head(out / (self.args.hidden_size / self.args.dim_model_base))
+2 -2
View File
@@ -7,7 +7,7 @@ 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 SuScaledRoPE
from .su_rope import SuScaledRotaryEmbedding
@dataclass
@@ -82,7 +82,7 @@ class Attention(nn.Module):
bias=self.attention_bias,
)
self.rope = SuScaledRoPE(
self.rope = SuScaledRotaryEmbedding(
dims=args.qk_rope_head_dim,
base=args.rope_theta,
max_position_embeddings=args.max_position_embeddings,
-287
View File
@@ -1,287 +0,0 @@
# 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
-264
View File
@@ -1,264 +0,0 @@
# 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
]
-58
View File
@@ -1,58 +0,0 @@
# 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 llama, ministral3
from .base import BaseModelArgs
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
text_config: dict
def __post_init__(self):
if "tie_word_embeddings" not in self.text_config:
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
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,
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)
lm_weights = dict(tree_flatten(weights["language_model"]))
weights["language_model"] = self.language_model.sanitize(lm_weights)
return dict(tree_flatten(weights))
@property
def layers(self):
return self.language_model.model.layers
+8 -4
View File
@@ -1,7 +1,8 @@
# Copyright © 2023-2024 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
@@ -161,15 +162,17 @@ 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)
@@ -187,9 +190,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
return self.lm_head(out)
def sanitize(self, weights):
-233
View File
@@ -1,233 +0,0 @@
# 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
-382
View File
@@ -1,382 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass, field
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 .rope_utils import initialize_rope
@dataclass(frozen=True)
class AttentionConfig:
no_op: bool = False
replace_with_linear: bool = False
sparsify: Optional[list[str]] = None
n_heads_in_group: Optional[int] = None # GQA group size
window_length: Optional[int] = None # Not directly used here, placeholder
num_sink_tokens: Optional[int] = None # Not directly used here, placeholder
use_prefill_window_in_sink_attention: bool = (
False # Not directly used here, placeholder
)
unshifted_sink: bool = False # Not directly used here, placeholder
def __post_init__(self):
# Ensure consistency: If no-op or linear, other attn params are irrelevant
if self.no_op or self.replace_with_linear:
# Use object.__setattr__ because the dataclass is frozen
object.__setattr__(self, "n_heads_in_group", None)
object.__setattr__(self, "window_length", None)
object.__setattr__(self, "num_sink_tokens", None)
# If it's a standard attention block, n_heads_in_group must be provided
elif not self.no_op:
if self.n_heads_in_group is None:
raise ValueError(
"n_heads_in_group must be specified for active attention blocks"
)
if self.n_heads_in_group <= 0:
raise ValueError(
f"n_heads_in_group must be positive, got {self.n_heads_in_group}"
)
@dataclass(frozen=True)
class FFNConfig:
no_op: bool = False
replace_with_linear: bool = False
sparsify: Optional[list[str]] = None
ffn_mult: Optional[float] = None
def __post_init__(self):
# Ensure consistency: If no-op or linear, ffn_mult is irrelevant
if self.no_op or self.replace_with_linear:
object.__setattr__(self, "ffn_mult", None)
# If it's a standard FFN block, ffn_mult must be provided
elif not self.no_op:
if self.ffn_mult is None:
raise ValueError("ffn_mult must be specified for active FFN blocks")
# Round to prevent potential floating point inconsistencies if needed
object.__setattr__(self, "ffn_mult", round(self.ffn_mult, 6))
@dataclass(frozen=True)
class BlockConfig:
attention: AttentionConfig
ffn: FFNConfig
@classmethod
def from_dict(cls, data: dict):
# Helper to create BlockConfig from a dictionary (e.g., loaded from JSON)
attn_conf = AttentionConfig(**data.get("attention", {}))
ffn_conf = FFNConfig(**data.get("ffn", {}))
return cls(attention=attn_conf, ffn=ffn_conf)
def _find_multiple(n: int, k: int) -> int:
"""Finds the smallest multiple of k greater than or equal to n."""
if n % k == 0:
return n
return n + k - (n % k)
def _ffn_mult_to_intermediate_size(ffn_mult: float, n_embd: int) -> int:
"""Calculates intermediate size based on multiplier, rounding up to multiple of 256."""
intermediate_size = int(2 * ffn_mult * n_embd / 3)
return _find_multiple(intermediate_size, 256)
# Activation function mapping
_ACT2FN = {
"silu": nn.silu,
"relu": nn.relu,
"gelu": nn.gelu,
"gelu_new": nn.gelu_approx,
"gelu_fast": nn.gelu_approx,
}
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str = "nemotron-nas"
hidden_size: int = 8192
num_hidden_layers: int = 80
num_attention_heads: int = 64
rms_norm_eps: float = 1e-5
vocab_size: int = 128256
block_configs: list = field(default_factory=list) # List of BlockConfig or dicts
hidden_act: str = "silu"
attention_bias: bool = False
mlp_bias: bool = False
rope_theta: float = 500000.0
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
max_position_embeddings: int = 131072
tie_word_embeddings: bool = False
def __post_init__(self):
# Automatically parse block_configs if they are loaded as dicts
if self.block_configs and isinstance(self.block_configs[0], dict):
self.block_configs = [
BlockConfig.from_dict(conf) for conf in self.block_configs
]
if len(self.block_configs) != self.num_hidden_layers:
raise ValueError(
f"Number of block_configs ({len(self.block_configs)}) must match "
f"num_hidden_layers ({self.num_hidden_layers})"
)
# Basic validation for RoPE scaling if provided
if self.rope_scaling:
if "factor" not in self.rope_scaling:
raise ValueError("rope_scaling must contain 'factor'")
rope_type = self.rope_scaling.get("rope_type")
if rope_type is None:
raise ValueError("rope_scaling must contain 'rope_type'")
# Validate individual block configs (post_init in dataclasses already does some)
for i, block_conf in enumerate(self.block_configs):
attn_conf = block_conf.attention
if not attn_conf.no_op and not attn_conf.replace_with_linear:
if self.num_attention_heads % attn_conf.n_heads_in_group != 0:
raise ValueError(
f"Layer {i}: num_attention_heads ({self.num_attention_heads}) "
f"must be divisible by n_heads_in_group ({attn_conf.n_heads_in_group})"
)
class Attention(nn.Module):
"""Standard GQA Attention mechanism for layers that use it."""
def __init__(self, args: ModelArgs, attention_config: AttentionConfig):
super().__init__()
dim = args.hidden_size
self.n_heads = n_heads = args.num_attention_heads
self.n_kv_heads = n_kv_heads = n_heads // attention_config.n_heads_in_group
self.head_dim = head_dim = args.hidden_size // n_heads
if (self.head_dim * n_heads) != dim:
raise ValueError(
f"hidden_size ({dim}) must be divisible by num_attention_heads ({n_heads})"
)
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)
# Initialize RoPE based on global config
self.rope = initialize_rope(
self.head_dim,
args.rope_theta,
False, # Llama uses traditional=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, 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)
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):
"""Standard Feed-Forward Network for layers that use it."""
def __init__(self, args: ModelArgs, ffn_config: FFNConfig):
super().__init__()
dim = args.hidden_size
# Calculate intermediate dim based on layer's specific config
hidden_dim = _ffn_mult_to_intermediate_size(ffn_config.ffn_mult, dim)
self.gate_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
self.down_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
self.up_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
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:
act_fn = _ACT2FN[self.act_fn]
return self.down_proj(act_fn(self.gate_proj(x)) * self.up_proj(x))
class LinearSubblockReplacement(nn.Module):
"""A simple linear layer used to replace Attention or MLP blocks."""
def __init__(self, hidden_size: int, bias: bool):
super().__init__()
self.linear = nn.Linear(hidden_size, hidden_size, bias=bias)
def __call__(self, x: mx.array, *args, **kwargs) -> mx.array:
# Accepts potential extra args (like mask, cache) but ignores them
return self.linear(x)
class TransformerBlock(nn.Module):
"""A single transformer block, potentially heterogeneous based on config."""
def __init__(self, args: ModelArgs, layer_idx: int):
super().__init__()
self.hidden_size = args.hidden_size
# Get the specific configuration for this layer
block_config = args.block_configs[layer_idx]
self.attention_config = block_config.attention
self.ffn_config = block_config.ffn
# Conditionally initialize Input LayerNorm (needed unless Attention is no-op)
if not self.attention_config.no_op:
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
else:
self.input_layernorm = None
# Conditionally initialize Attention block
if self.attention_config.no_op:
self.self_attn = None
elif self.attention_config.replace_with_linear:
self.self_attn = LinearSubblockReplacement(
args.hidden_size, args.attention_bias
)
else:
# Standard attention for this layer
self.self_attn = Attention(args, self.attention_config)
# Conditionally initialize Post-Attention LayerNorm (needed unless FFN is no-op)
if not self.ffn_config.no_op:
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
else:
self.post_attention_layernorm = None
# Conditionally initialize MLP block
if self.ffn_config.no_op:
self.mlp = None
elif self.ffn_config.replace_with_linear:
self.mlp = LinearSubblockReplacement(args.hidden_size, args.mlp_bias)
else:
# Standard MLP for this layer
self.mlp = MLP(args, self.ffn_config)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
# Attention part (Input Norm -> Attention -> Residual)
if self.self_attn is not None:
residual = x
h = self.input_layernorm(x)
attn_out = self.self_attn(h, mask=mask, cache=cache)
x = residual + attn_out
# MLP part (Post-Attention Norm -> MLP -> Residual)
if self.mlp is not None:
residual = x
h = self.post_attention_layernorm(x)
mlp_out = self.mlp(h)
x = residual + mlp_out
return x
class NemotronNASModel(nn.Module):
"""The core Nemotron-NAS style transformer model."""
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=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[List[Any]] = 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, 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 = NemotronNASModel(args)
if not args.tie_word_embeddings:
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
else:
self.lm_head = None
def __call__(
self,
inputs: mx.array,
cache=None,
):
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
def sanitize(self, weights):
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
return weights
@property
def layers(self):
return self.model.layers
+6 -2
View File
@@ -176,14 +176,17 @@ 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)
@@ -202,9 +205,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, mask, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
-375
View File
@@ -1,375 +0,0 @@
# 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
+9 -5
View File
@@ -2,7 +2,7 @@
import sys
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
@@ -124,15 +124,17 @@ 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)
@@ -152,9 +154,10 @@ class OlmoModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
return self.transformer(inputs, cache)
return self.transformer(inputs, mask, cache)
class Model(nn.Module):
@@ -167,9 +170,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
return self.model(inputs, cache)
return self.model(inputs, mask, cache)
@property
def layers(self):
+6 -2
View File
@@ -163,12 +163,15 @@ 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)
@@ -189,8 +192,9 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache=None,
mask=None,
):
out = self.model(inputs, cache)
out = self.model(inputs, cache, mask)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:

Some files were not shown because too many files have changed in this diff Show More