Compare commits
534 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a716f040b7 | |||
| df1d3f3c9a | |||
| ed1fca4cef | |||
| 4f5cbd2a4f | |||
| 3cd9a52df2 | |||
| 2f1ab85aec | |||
| f3bb10c141 | |||
| e1c24b3237 | |||
| f39cb8e934 | |||
| a9856b485d | |||
| e92138cb01 | |||
| a401730941 | |||
| 6d114686e5 | |||
| aa4f880fb3 | |||
| 62f38aeb51 | |||
| d9c63fff67 | |||
| dcbf6e33d1 | |||
| f26fddfd3b | |||
| f56d99712c | |||
| c65c27b450 | |||
| 3257c3df17 | |||
| d4eb136d44 | |||
| 4469ad4647 | |||
| f79dba7832 | |||
| 3f9d179fd1 | |||
| 9dc023beed | |||
| 9dcefa5272 | |||
| bdeac59767 | |||
| 6ddfdda1ac | |||
| 4d3af3cebc | |||
| ed7884cb80 | |||
| f8019f7769 | |||
| 564281f793 | |||
| 73c8550478 | |||
| ed69f837e6 | |||
| cc393b2862 | |||
| 2146e4ed18 | |||
| 735a43b275 | |||
| 332d94ca6f | |||
| 480934402d | |||
| ab157c2d18 | |||
| 5a8ced697e | |||
| 760c5abcc8 | |||
| 43ee5455d3 | |||
| 23af85703e | |||
| 89c430a9c2 | |||
| 4a21ffdf4b | |||
| 852119b774 | |||
| 044474bc80 | |||
| 2105aaf9c3 | |||
| cff7273a55 | |||
| fc7d84448b | |||
| 47be7150a6 | |||
| 35fa620279 | |||
| 8162aaad56 | |||
| 834fac934c | |||
| 179da774b1 | |||
| 720f2369ba | |||
| 65725dcec2 | |||
| d4701ba513 | |||
| 321e764e0a | |||
| 83ff9c96d5 | |||
| 9c113f7019 | |||
| 7d6c5e4af7 | |||
| ad067ea627 | |||
| d7b91e80f0 | |||
| 1fd521c3c7 | |||
| 572ada278c | |||
| fb47f8fb99 | |||
| 7a720882a7 | |||
| 014ebc6a46 | |||
| c6d9d3c9f5 | |||
| bcf630614f | |||
| 1974376d70 | |||
| 7e67225e1d | |||
| 0fd3126496 | |||
| ca0d1c9630 | |||
| 82edd51a1e | |||
| aca4c149a1 | |||
| 8f1c56ec83 | |||
| 84ae19e675 | |||
| 645a326a2e | |||
| fd6959dca7 | |||
| f18526f8d6 | |||
| 25a4c8369e | |||
| e08ec15b72 | |||
| b77ec6b951 | |||
| ab050d1fac | |||
| 942b3ed4b6 | |||
| 11ebc98ada | |||
| 1630f9bf16 | |||
| b7cc3aa5e5 | |||
| 7afcfac51a | |||
| 1ecd27a31a | |||
| 1fe1b3c901 | |||
| 8a0f3781e9 | |||
| 56b8c0f383 | |||
| ad9434bef0 | |||
| 04fd7ccb97 | |||
| 7f1b7fe6bc | |||
| c645a55582 | |||
| 1bdc8afca3 | |||
| 96699e6dad | |||
| b012e1a1e9 | |||
| f53a9b0689 | |||
| beceb5c16d | |||
| 12073b1db4 | |||
| d9846d37cb | |||
| 1b76e3d580 | |||
| e5ddaff99b | |||
| 14b9faf1bc | |||
| 1423702019 | |||
| 5ed1e48a3c | |||
| 4100703f1a | |||
| 8c08a46da4 | |||
| bc891dca4c | |||
| 02228601cd | |||
| 8daabcc7c1 | |||
| cd7d9a536e | |||
| 25246632cf | |||
| 6651d2e0bf | |||
| 43dcf2f0c0 | |||
| 769069d66b | |||
| 5261ab85ee | |||
| c27c94a0ff | |||
| fd80ac89fb | |||
| edbf61dd8b | |||
| c2a716c871 | |||
| 63c9873617 | |||
| 7585c142a6 | |||
| 44d12e5d6f | |||
| 7a86c1289e | |||
| a20eefd7c2 | |||
| 3eb6ecf2b6 | |||
| 39a96ab18b | |||
| 43082feafa | |||
| 5cce1495e0 | |||
| 509f5aef89 | |||
| 0f76343ea4 | |||
| 298b67c755 | |||
| 94497d5255 | |||
| 4c80c68ea6 | |||
| ac8ae2c05a | |||
| 7a4d137df6 | |||
| 90db1e6266 | |||
| d3dc2e3f33 | |||
| 7423bf6752 | |||
| 5dec49a12f | |||
| 3727e01cd7 | |||
| 09579644ac | |||
| 0081085a91 | |||
| fed582eede | |||
| 7973b8cfe8 | |||
| 7096618d50 | |||
| 1e0c0f3985 | |||
| 68f18bae14 | |||
| f5ae09a807 | |||
| 08c8c0a5ea | |||
| a9311cca23 | |||
| 9fe5f43abf | |||
| 1b2d11b5c7 | |||
| 657a66c5c4 | |||
| 595fb4bdbf | |||
| 79a0721c9a | |||
| cc3264c22e | |||
| a227a9e9f3 | |||
| cd9ca9f068 | |||
| 7744d0f40b | |||
| f3ed856610 | |||
| ede65a1484 | |||
| 3d3e0751a3 | |||
| 085e36e6ab | |||
| eea2e5f5de | |||
| cb763947ee | |||
| b343a0556f | |||
| 82dfd39ef2 | |||
| 84996808a2 | |||
| 99f8fd6cc8 | |||
| 7d042c6124 | |||
| 0fbff353db | |||
| 0ad37e2bbf | |||
| 454bf9a22b | |||
| 133b5d3bd7 | |||
| abc52a0a48 | |||
| 6b42901468 | |||
| f353e0178b | |||
| f940cf3a95 | |||
| 34cbb8b51a | |||
| 4bc21cc17b | |||
| 9fd3e419ec | |||
| 743f4f7710 | |||
| 088e7ad7ca | |||
| 1d01257d2e | |||
| 2959af09fb | |||
| 8f1f88e5af | |||
| 606ff3ef06 | |||
| cd367819c7 | |||
| ba2cf3c0ee | |||
| 6c1a459314 | |||
| 3833c205c1 | |||
| 3356b0a017 | |||
| f6c94659d8 | |||
| d3bf847e6f | |||
| df6434185c | |||
| 974e17b43a | |||
| a82790a141 | |||
| 2aa31f95a7 | |||
| 4decc4d381 | |||
| 0d8272483b | |||
| 663b822de5 | |||
| f36977385f | |||
| 1e8fca4e0b | |||
| 61669b270f | |||
| f2b0262824 | |||
| 367d6d7686 | |||
| 5e6a7f6895 | |||
| edc656a85c | |||
| a4c6470390 | |||
| 1d114498f2 | |||
| b1fc49a9f2 | |||
| 4781244aaf | |||
| 49ba6dff08 | |||
| c3b4a15851 | |||
| a4e32ef5a5 | |||
| cceb45d6b6 | |||
| 0b8c1668d2 | |||
| f876b3f775 | |||
| 373c63c08f | |||
| 344755a1f6 | |||
| abb185cb66 | |||
| 49919e8563 | |||
| 7168556aaa | |||
| 1e71de53e3 | |||
| f318741784 | |||
| 0edd0a0cd1 | |||
| 44c74e1d04 | |||
| 81c3c193cf | |||
| b264da7602 | |||
| cf8cfd0a1b | |||
| f96344dfe6 | |||
| 9a4039a518 | |||
| 16ca0b3a17 | |||
| e1e66c0d64 | |||
| 380789d067 | |||
| a1d079e930 | |||
| 0c0b72221f | |||
| dcb4b9ba6d | |||
| 358b4d2ab5 | |||
| 1a4d24ed5f | |||
| 47e1710f23 | |||
| 50012d153d | |||
| eaf1748ea5 | |||
| ffc0ecc1ca | |||
| 4096aabdba | |||
| 36963eec80 | |||
| f22120ef83 | |||
| c991106dbf | |||
| a7f534c3f5 | |||
| 04d6d926f3 | |||
| 38dc092e1f | |||
| de47734510 | |||
| 9e89bb5c25 | |||
| b2564b5226 | |||
| 502554646d | |||
| 40dd25d7b7 | |||
| 55bb9471b8 | |||
| 9a11a81add | |||
| 4aaac2072d | |||
| 469461f463 | |||
| 714157be6d | |||
| 06a9fdc5ad | |||
| 420418ca4e | |||
| d6c45998f0 | |||
| 42828df17a | |||
| 80c217ff96 | |||
| cf8e59ef76 | |||
| 7b84eb6bd1 | |||
| 547b6b7e26 | |||
| 319c453828 | |||
| 64b3c51a58 | |||
| 1537efd29a | |||
| 4a085c7618 | |||
| 87961a743a | |||
| d79f3cd612 | |||
| 103877ea3e | |||
| 64574e19b8 | |||
| 1b08ef199b | |||
| 4ce11c6e50 | |||
| 0f268680c8 | |||
| 3ae6583393 | |||
| dccde59664 | |||
| 30c30a2a7e | |||
| 6dd9d48bdc | |||
| f5741cae33 | |||
| 3b9074f48a | |||
| e797abf49b | |||
| 60320dc234 | |||
| 1cd6045176 | |||
| d17184063a | |||
| 24fefe3d05 | |||
| da1309f5a7 | |||
| bdcac4b635 | |||
| 04a113fbdc | |||
| cd9884dab3 | |||
| 249b0a11d6 | |||
| 6fd60d3edb | |||
| 1b7b67bfcf | |||
| e7f241094c | |||
| a2acdd6ddd | |||
| c0d630a9b4 | |||
| ebc2eea042 | |||
| d9a3ece154 | |||
| b513585c2f | |||
| 5f71d8bd84 | |||
| 877cc38e6c | |||
| 5ff59f0389 | |||
| 6d74487ed6 | |||
| 90e33f5443 | |||
| f1a6359c92 | |||
| 6c876ca5d1 | |||
| cfa74add1e | |||
| e22bdaafe3 | |||
| d5bdab1a22 | |||
| 667a7116c3 | |||
| cbfba0a973 | |||
| fc800f1a0b | |||
| 06efe8db99 | |||
| 4d6d705140 | |||
| 7c987941f2 | |||
| e4470506ab | |||
| 72bc789b2b | |||
| e9b1649662 | |||
| b60cec88df | |||
| b26c608811 | |||
| 489e63376b | |||
| d23c79bf90 | |||
| a1e16ca845 | |||
| 48aead682c | |||
| 64763adeeb | |||
| 08e4dd2fc5 | |||
| d7573a85fb | |||
| 803781fa21 | |||
| 402820ac43 | |||
| 2929259a9f | |||
| e469d89f73 | |||
| 1ec6a9d383 | |||
| d84315dbe9 | |||
| fffcba5362 | |||
| 5808c1c752 | |||
| d1a18f6449 | |||
| fd9b190963 | |||
| 00e56a1761 | |||
| 53da6bda72 | |||
| f42eae84ef | |||
| 802dd862a7 | |||
| 93cd9e86a4 | |||
| 7f7c7b929a | |||
| 6b0a744449 | |||
| 9ee2b7358f | |||
| 1e1c790cdf | |||
| b1cfe43f49 | |||
| d8c4667ddb | |||
| 5cb7526fe8 | |||
| bfa03f0ea7 | |||
| 84bdda1f0c | |||
| 80481ad51d | |||
| 90230d31cc | |||
| 854b427fb9 | |||
| 5fa62eb5f5 | |||
| e8f8729854 | |||
| e8c2cfce6a | |||
| 5431546b1e | |||
| c2f7facb66 | |||
| 36d0d04ecd | |||
| e6dfe18344 | |||
| 74a47b1434 | |||
| d0ef4bcf17 | |||
| 7c13b0defc | |||
| d9bd78a4db | |||
| 19287dc922 | |||
| 4a3b2a978f | |||
| f009881e5c | |||
| 584780a05f | |||
| e673a97c80 | |||
| 3be51537a3 | |||
| 19153e1671 | |||
| 1db99d41a2 | |||
| d1d0771e3f | |||
| e8980c050b | |||
| 3cc61aa64d | |||
| 77edf17bc0 | |||
| 71e8e57c2e | |||
| 1b555aaa08 | |||
| 77898fd22d | |||
| f2aa9419d9 | |||
| 064c75d78e | |||
| 0824576a57 | |||
| 5960ee9c7a | |||
| 29f8e7765d | |||
| f93589cb7d | |||
| 864f5ce118 | |||
| 5101aebe05 | |||
| 1ca5474822 | |||
| 4401043b0c | |||
| 76c30edbd4 | |||
| 854c580f72 | |||
| 2973b75c8a | |||
| 4b484773cf | |||
| f1572d4586 | |||
| c592f76f6a | |||
| 02a0241581 | |||
| 4a176da038 | |||
| 66e9cc480b | |||
| 68f6e5be23 | |||
| f031c97dc5 | |||
| 13087ecf98 | |||
| 989b291159 | |||
| 68e33d9a13 | |||
| 60c93b8caa | |||
| d25eb2b3e5 | |||
| c48de5082c | |||
| b839de1716 | |||
| 5c2c18d6a3 | |||
| 36faf2edfd | |||
| d6a9e61572 | |||
| ce2358d297 | |||
| a9e1e82c61 | |||
| 9df9689fdc | |||
| dd24e000b6 | |||
| 5c04f07f3b | |||
| f816fdabd0 | |||
| 69195f8632 | |||
| c904be4ba3 | |||
| c3cbafb688 | |||
| 3b9b0ed052 | |||
| 1bfb7c2407 | |||
| cff94b7fbf | |||
| ba83487ed9 | |||
| c9f747bfaf | |||
| db368f25f3 | |||
| 23ba454f8c | |||
| a8e8f5755a | |||
| b4c88dc5e7 | |||
| ebf6124267 | |||
| 9a60e96249 | |||
| d79ba69021 | |||
| 3962ce6de9 | |||
| 59c2844cc2 | |||
| eb06c1621f | |||
| 0895f8f5cf | |||
| 81f9d8211d | |||
| 191d81d1a0 | |||
| 1958691940 | |||
| 2e96d0730a | |||
| c158143831 | |||
| d00af36bda | |||
| b92c8f3eda | |||
| 455cdac5df | |||
| a53225747f | |||
| 2d4c134ec2 | |||
| e2e62d9085 | |||
| fd175f11d5 | |||
| 465b107c2a | |||
| 93cc7d319f | |||
| d4275716f6 | |||
| 3b3df251d3 | |||
| c16c2984ba | |||
| 1dc5de4fce | |||
| ed8087f723 | |||
| 50f4cad769 | |||
| ee044da0a8 | |||
| 466544baff | |||
| 59d2005a8b | |||
| 2ec4db9dfc | |||
| 70d555b325 | |||
| a57288b877 | |||
| 60b6b18219 | |||
| 37691af2b1 | |||
| 61e64358a8 | |||
| 94cd2397f1 | |||
| 4d5200d638 | |||
| 38c0a14ea2 | |||
| c614cb4889 | |||
| 6a085265d5 | |||
| d348c96a57 | |||
| 5172d92ef9 | |||
| 56a2995e76 | |||
| c8749a6abc | |||
| 5846de61f4 | |||
| bd27c05310 | |||
| 051a892660 | |||
| 1fc6fc7978 | |||
| e00844b121 | |||
| b8bbbca6bf | |||
| 6b05bde124 | |||
| 35a4203ecb | |||
| 09c5785fb4 | |||
| f472850b1e | |||
| 91d0a054a7 | |||
| 761828523c | |||
| 97fe80467c | |||
| e893a9bcaf | |||
| 9e3e7b1e8b | |||
| 8c68587f00 | |||
| e05c6fb2f5 | |||
| 5f67c3a2ed | |||
| b1a47a7634 | |||
| c9ba9d2377 | |||
| 07e07deaee | |||
| 36a6734479 | |||
| 50af99c2ef | |||
| 7a393da1d6 | |||
| c8b0818ecc | |||
| c4c3d6faa7 | |||
| cae885eb1f | |||
| 485b30898c | |||
| 18673aad23 | |||
| 67c9ee5c1a | |||
| 932401344e | |||
| e9cc2307ac | |||
| 2922cb4f39 | |||
| 311c0b3016 | |||
| f7f3173c44 | |||
| 62a706bfe4 | |||
| 2d0e3f3ea6 | |||
| fd18f4524c | |||
| 9da2292db0 | |||
| d09376c52a | |||
| f1df7128ab | |||
| a3167a8dc2 | |||
| 4b45d778a7 | |||
| dfd2d3ec04 | |||
| eaddd969b5 | |||
| 3d028f88cb |
@@ -0,0 +1,16 @@
|
||||
name: 'Setup macOS Environment'
|
||||
description: 'Install dependencies for macOS'
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: 'Python version to use'
|
||||
required: false
|
||||
default: '3.10'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: "latest"
|
||||
python-version: ${{ inputs.python-version }}
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/head/main' }}
|
||||
|
||||
jobs:
|
||||
check_lint:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
|
||||
mac_build_and_test:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: [self-hosted, macos]
|
||||
needs: check_lint
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup-macos
|
||||
- name: Install test dependencies
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
pip install unittest-xml-reporting
|
||||
pip install -e ".[test]"
|
||||
- name: Run tests
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
curl -o test_data.zip -L https://github.com/ml-explore/mlx-lm/releases/download/test_data/test_data.zip
|
||||
unzip test_data.zip
|
||||
METAL_DEVICE_WRAPPER_TYPE=1 METAL_DEBUG_ERROR_MODE=0 HF_HOME="." python -m xmlrunner discover -v tests -o test-results/
|
||||
mlx.launch -n 2 tests/model_parallel_tests.py
|
||||
@@ -0,0 +1,41 @@
|
||||
name: PyPI Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build_release:
|
||||
if: github.repository == 'ml-explore/mlx-lm'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mlx-lm
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Build package
|
||||
shell: sh
|
||||
run: |
|
||||
pip install build
|
||||
python -m build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
overwrite: true
|
||||
name: mlx-lm
|
||||
path: dist/*
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://upload.pypi.org/legacy/
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# .DS_Store files
|
||||
.DS_Store
|
||||
@@ -0,0 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args:
|
||||
- --profile=black
|
||||
+24
-8
@@ -5,13 +5,29 @@ with a short description of your contribution(s) below. For example:
|
||||
|
||||
- Jane Smith: Added the `foo` example.
|
||||
|
||||
MLX Examples was developed with contributions from the following individuals:
|
||||
MLX LM was developed with contributions from the following individuals:
|
||||
|
||||
- Juarez Bochi: Added support for T5 models.
|
||||
- Sarthak Yadav: Added the `cifar` and `speechcommands` examples.
|
||||
- Shunta Saito: Added support for PLaMo models.
|
||||
- Gabrijel Boduljak: Implemented `CLIP`.
|
||||
- Markus Enzweiler: Added the `cvae` examples.
|
||||
- Prince Canuma: Helped add support for `Starcoder2` models.
|
||||
- Shiyu Li: Added the `Segment Anything Model`.
|
||||
- Gökdeniz Gülmez: Added support for `MiniCPM`, `Mamba` and support for `full-fine-tuning`.
|
||||
- Gökdeniz Gülmez: Added support for the following architectures:
|
||||
OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1` and
|
||||
`Mamba v2`, Z.ai & THUKEG's `GLM`, `GLM4`, `GLM5 (GLM MoE DSA)`, Rednote `dots.llm1`, Baidu's `Ernie4.5 MoE`,
|
||||
inclusionAI's `Bailing MoE e.g. Ling-family`, `Bailing MoE Linear e.g. Ling-Linear-family`,
|
||||
Klear team - Kuaishou Technology's `Klear`, AI21 Lab's `Jamba` IBM's `Granite MoE`,
|
||||
Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's `Apertus`, Nikity's `Lille130m`,
|
||||
Alibaba Qwen's `Qwen3Next`, Tele-AI's `TeleChat3`, and Allenai's `OLMoE` and `Olmo 3`;
|
||||
Helped add support for the following model architectures:
|
||||
Alibaba Qwen's `Qwen3 & Qwen3MoE)`; Added support for the following training algorithms:
|
||||
`Full Weight Fine-Tuning`, and the `Muon` optimizer;
|
||||
Added support for the following other features:
|
||||
`Multiple Optimizers to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
|
||||
- Prince Canuma: Helped add support for the following model architectures:
|
||||
HuggingFace's `Starcoder2`, Cohere's `Cohere (1 and 2)`, Alibaba Qwen's `Qwen (2, 3 and MoE)`,
|
||||
Microsoft's `Phi (3 and 3.5 MoE)`, `BitNet1.58`, Meta's `Llama (3 and 4)`, MinimaxAI's `MiniMax`,
|
||||
MoonshotAI's `Kimi-Linear`, LiquidAI's `LFM2` and `LFM2 MoE`,
|
||||
Google DeepMind's `Gemma 3`, TII's `Falcon H1` and InterLM's `InternLM 2.5`.
|
||||
- Ivan Fioravanti: Added support for the following architectures:
|
||||
ServiceNow-AI's `Apriel 1.5`, Tencent's `Hunyuan Dense V1` and `Hunyuan MoE V1`.
|
||||
- Tarjei Mandt: Added support for the following architectures: `Step 3.5 Flash`,
|
||||
MoonshotAI's `Kimi K2.5`, Upstage's `Solar Open`, LG AI Research's `K-Exaone MoE`,
|
||||
Meituan's `LongCat Flash Lite` Helped add support for the following model architectures:
|
||||
Z.ai & THUKEG's `GLM5 (GLM MoE DSA)`
|
||||
+51
-8
@@ -1,11 +1,54 @@
|
||||
# Contributing to MLX LM
|
||||
|
||||
We want to make contributing to this project as easy and transparent as
|
||||
possible.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
1. Fork and submit pull requests to the repo.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. Every PR should have passing tests and at least one review.
|
||||
4. For code formatting install `pre-commit` using something like `pip install pre-commit` and run `pre-commit install`.
|
||||
This should install hooks for running `black` and `clang-format` to ensure
|
||||
consistent style for C++ and python code.
|
||||
|
||||
You can also run the formatters manually as follows on individual files:
|
||||
|
||||
```bash
|
||||
clang-format -i file.cpp
|
||||
```
|
||||
|
||||
```bash
|
||||
black file.py
|
||||
```
|
||||
|
||||
or,
|
||||
|
||||
```bash
|
||||
# single file
|
||||
pre-commit run --files file1.py
|
||||
|
||||
# specific files
|
||||
pre-commit run --files file1.py file2.py
|
||||
```
|
||||
|
||||
or run `pre-commit run --all-files` to check all files in the repo.
|
||||
|
||||
## Issues
|
||||
|
||||
We use GitHub issues to track public bugs. Please ensure your description is
|
||||
clear and has sufficient instructions to be able to reproduce the issue.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to mlx-lm, you agree that your contributions will be licensed
|
||||
under the LICENSE file in the root directory of this source tree.
|
||||
|
||||
## Adding New Models
|
||||
|
||||
Below are some tips to port LLMs available on Hugging Face to MLX.
|
||||
|
||||
Before starting checkout the [general contribution
|
||||
guidelines](https://github.com/ml-explore/mlx-examples/blob/main/CONTRIBUTING.md).
|
||||
|
||||
Next, from this directory, do an editable install:
|
||||
From this directory, do an editable install:
|
||||
|
||||
```shell
|
||||
pip install -e .
|
||||
@@ -17,7 +60,7 @@ Then check if the model has weights in the
|
||||
convert it.
|
||||
|
||||
After that, add the model file to the
|
||||
[`mlx_lm/models`](https://github.com/ml-explore/mlx-examples/tree/main/llms/mlx_lm/models)
|
||||
[`mlx_lm/models`](https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/models)
|
||||
directory. You can see other examples there. We recommend starting from a model
|
||||
that is similar to the model you are porting.
|
||||
|
||||
@@ -35,12 +78,12 @@ To determine the model layer names, we suggest either:
|
||||
in the Hugging Face repo.
|
||||
|
||||
To add LoRA support edit
|
||||
[`mlx_lm/tuner/utils.py`](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/tuner/utils.py#L27-L60)
|
||||
[`mlx_lm/tuner/utils.py`](https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/tuner/utils.py#L27-L60)
|
||||
|
||||
Finally, add a test for the new modle type to the [model
|
||||
tests](https://github.com/ml-explore/mlx-examples/blob/main/llms/tests/test_models.py).
|
||||
tests](https://github.com/ml-explore/mlx-lm/blob/main/tests/test_models.py).
|
||||
|
||||
From the `llms/` directory, you can run the tests with:
|
||||
You can run the tests with:
|
||||
|
||||
```shell
|
||||
python -m unittest discover tests/
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
include mlx_lm/requirements.txt
|
||||
include requirements.txt
|
||||
recursive-include mlx_lm/ *.py
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
## Generate Text with LLMs and MLX
|
||||
## MLX LM
|
||||
|
||||
MLX LM is a Python package for generating text and fine-tuning large language
|
||||
models on Apple silicon with MLX.
|
||||
|
||||
Some key features include:
|
||||
|
||||
* Integration with the Hugging Face Hub to easily use thousands of LLMs with a
|
||||
single command.
|
||||
* Support for quantizing and uploading models to the Hugging Face Hub.
|
||||
* [Low-rank and full model
|
||||
fine-tuning](https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/LORA.md)
|
||||
with support for quantized models.
|
||||
* Distributed inference and fine-tuning with `mx.distributed`
|
||||
|
||||
The easiest way to get started is to install the `mlx-lm` package:
|
||||
|
||||
@@ -14,18 +27,12 @@ pip install mlx-lm
|
||||
conda install -c conda-forge mlx-lm
|
||||
```
|
||||
|
||||
The `mlx-lm` package also has:
|
||||
|
||||
- [LoRA, QLoRA, and full fine-tuning](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/LORA.md)
|
||||
- [Merging models](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/MERGE.md)
|
||||
- [HTTP model serving](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/SERVER.md)
|
||||
|
||||
### Quick Start
|
||||
|
||||
To generate text with an LLM use:
|
||||
|
||||
```bash
|
||||
mlx_lm.generate --prompt "Hi!"
|
||||
mlx_lm.generate --prompt "How tall is Mt Everest?"
|
||||
```
|
||||
|
||||
To chat with an LLM use:
|
||||
@@ -45,6 +52,12 @@ options for a command, e.g.:
|
||||
mlx_lm.generate -h
|
||||
```
|
||||
|
||||
The default model for generation and chat is
|
||||
`mlx-community/Llama-3.2-3B-Instruct-4bit`. You can specify any MLX-compatible
|
||||
model with the `--model` flag. Thousands are available in the
|
||||
[MLX Community](https://huggingface.co/mlx-community) Hugging Face
|
||||
organization.
|
||||
|
||||
### Python API
|
||||
|
||||
You can use `mlx-lm` as a module:
|
||||
@@ -58,7 +71,7 @@ prompt = "Write a story about Einstein"
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages, add_generation_prompt=True,
|
||||
)
|
||||
|
||||
text = generate(model, tokenizer, prompt=prompt, verbose=True)
|
||||
@@ -71,8 +84,10 @@ To see a description of all the arguments you can do:
|
||||
```
|
||||
|
||||
Check out the [generation
|
||||
example](https://github.com/ml-explore/mlx-examples/tree/main/llms/mlx_lm/examples/generate_response.py)
|
||||
to see how to use the API in more detail.
|
||||
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.
|
||||
|
||||
The `mlx-lm` package also comes with functionality to quantize and optionally
|
||||
upload models to the Hugging Face Hub.
|
||||
@@ -115,7 +130,7 @@ prompt = "Write a story about Einstein"
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages, add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for response in stream_generate(model, tokenizer, prompt, max_tokens=512):
|
||||
@@ -123,6 +138,18 @@ for response in stream_generate(model, tokenizer, prompt, max_tokens=512):
|
||||
print()
|
||||
```
|
||||
|
||||
#### Sampling
|
||||
|
||||
The `generate` and `stream_generate` functions accept `sampler` and
|
||||
`logits_processors` keyword arguments. A sampler is any callable which accepts
|
||||
a possibly batched logits array and returns an array of sampled tokens. The
|
||||
`logits_processors` must be a list of callables which take the token history
|
||||
and current logits as input and return the processed logits. The logits
|
||||
processors are applied in order.
|
||||
|
||||
Some standard sampling functions and logits processors are provided in
|
||||
`mlx_lm.sample_utils`.
|
||||
|
||||
### Command Line
|
||||
|
||||
You can also use `mlx-lm` from the command line with:
|
||||
@@ -143,7 +170,7 @@ mlx_lm.generate --help
|
||||
To quantize a model from the command line run:
|
||||
|
||||
```
|
||||
mlx_lm.convert --hf-path mistralai/Mistral-7B-Instruct-v0.3 -q
|
||||
mlx_lm.convert --model mistralai/Mistral-7B-Instruct-v0.3 -q
|
||||
```
|
||||
|
||||
For more options run:
|
||||
@@ -158,13 +185,13 @@ You can upload new models to Hugging Face by specifying `--upload-repo` to
|
||||
|
||||
```
|
||||
mlx_lm.convert \
|
||||
--hf-path mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
--model mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
-q \
|
||||
--upload-repo mlx-community/my-4bit-mistral
|
||||
```
|
||||
|
||||
Models can also be converted and quantized directly in the
|
||||
[mlx-my-repo]https://huggingface.co/spaces/mlx-community/mlx-my-repo) Hugging
|
||||
[mlx-my-repo](https://huggingface.co/spaces/mlx-community/mlx-my-repo) Hugging
|
||||
Face Space.
|
||||
|
||||
### Long Prompts and Generations
|
||||
@@ -201,53 +228,27 @@ The cached prompt is treated as a prefix to the supplied prompt. Also notice
|
||||
when using a cached prompt, the model to use is read from the cache and need
|
||||
not be supplied explicitly.
|
||||
|
||||
Prompt caching can also be used in the Python API in order to to avoid
|
||||
Prompt caching can also be used in the Python API in order to avoid
|
||||
recomputing the prompt. This is useful in multi-turn dialogues or across
|
||||
requests that use the same context. See the
|
||||
[example](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/examples/chat.py)
|
||||
[example](https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/examples/chat.py)
|
||||
for more usage details.
|
||||
|
||||
### Supported Models
|
||||
|
||||
`mlx-lm` supports thousands of Hugging Face format LLMs. If the model you want to
|
||||
run is not supported, file an
|
||||
[issue](https://github.com/ml-explore/mlx-examples/issues/new) or better yet,
|
||||
submit a pull request.
|
||||
`mlx-lm` supports thousands of LLMs available on the Hugging Face Hub. If the
|
||||
model you want to run is not supported, file an
|
||||
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet, submit
|
||||
a pull request. Many supported models are available in various quantization
|
||||
formats in the [MLX Community](https://huggingface.co/mlx-community) Hugging
|
||||
Face organization.
|
||||
|
||||
Here are a few examples of Hugging Face models that work with this example:
|
||||
For some models the tokenizer may require you to enable the `trust_remote_code`
|
||||
option. You can do this by passing `--trust-remote-code` in the command line.
|
||||
If you don't specify the flag explicitly, you will be prompted to trust remote
|
||||
code in the terminal when running the model.
|
||||
|
||||
- [mistralai/Mistral-7B-v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1)
|
||||
- [meta-llama/Llama-2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf)
|
||||
- [deepseek-ai/deepseek-coder-6.7b-instruct](https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-instruct)
|
||||
- [01-ai/Yi-6B-Chat](https://huggingface.co/01-ai/Yi-6B-Chat)
|
||||
- [microsoft/phi-2](https://huggingface.co/microsoft/phi-2)
|
||||
- [mistralai/Mixtral-8x7B-Instruct-v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1)
|
||||
- [Qwen/Qwen-7B](https://huggingface.co/Qwen/Qwen-7B)
|
||||
- [pfnet/plamo-13b](https://huggingface.co/pfnet/plamo-13b)
|
||||
- [pfnet/plamo-13b-instruct](https://huggingface.co/pfnet/plamo-13b-instruct)
|
||||
- [stabilityai/stablelm-2-zephyr-1_6b](https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b)
|
||||
- [internlm/internlm2-7b](https://huggingface.co/internlm/internlm2-7b)
|
||||
- [tiiuae/falcon-mamba-7b-instruct](https://huggingface.co/tiiuae/falcon-mamba-7b-instruct)
|
||||
|
||||
Most
|
||||
[Mistral](https://huggingface.co/models?library=transformers,safetensors&other=mistral&sort=trending),
|
||||
[Llama](https://huggingface.co/models?library=transformers,safetensors&other=llama&sort=trending),
|
||||
[Phi-2](https://huggingface.co/models?library=transformers,safetensors&other=phi&sort=trending),
|
||||
and
|
||||
[Mixtral](https://huggingface.co/models?library=transformers,safetensors&other=mixtral&sort=trending)
|
||||
style models should work out of the box.
|
||||
|
||||
For some models (such as `Qwen` and `plamo`) the tokenizer requires you to
|
||||
enable the `trust_remote_code` option. You can do this by passing
|
||||
`--trust-remote-code` in the command line. If you don't specify the flag
|
||||
explicitly, you will be prompted to trust remote code in the terminal when
|
||||
running the model.
|
||||
|
||||
For `Qwen` models you must also specify the `eos_token`. You can do this by
|
||||
passing `--eos-token "<|endoftext|>"` in the command
|
||||
line.
|
||||
|
||||
These options can also be set in the Python API. For example:
|
||||
Tokenizer options can also be set in the Python API. For example:
|
||||
|
||||
```python
|
||||
model, tokenizer = load(
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
Spin up the local server:
|
||||
|
||||
mlx_lm.server
|
||||
|
||||
Then run the benchmark:
|
||||
|
||||
python server_benchmark.py --concurrency 4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from itertools import cycle
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
# Default prompts if no file is provided
|
||||
DEFAULT_PROMPTS = [
|
||||
"Explain quantum computing in simple terms.",
|
||||
"What are the main differences between Python and JavaScript?",
|
||||
"Describe the process of photosynthesis in plants.",
|
||||
"How does a neural network learn from data?",
|
||||
"What is the significance of the Turing test in AI?",
|
||||
"Explain the concept of blockchain technology.",
|
||||
"What causes seasons on Earth?",
|
||||
"How do vaccines work in the human body?",
|
||||
"Describe the water cycle and its importance.",
|
||||
"What is the theory of relativity proposed by Einstein?",
|
||||
"How do electric cars help reduce carbon emissions?",
|
||||
"What are the key features of a market economy?",
|
||||
"Explain how DNA replication works in cells.",
|
||||
"What is machine learning and its real-world applications?",
|
||||
"Describe the structure and function of the human heart.",
|
||||
]
|
||||
|
||||
|
||||
def tokens_per_second(tokens):
|
||||
start = math.floor(tokens[0])
|
||||
stop = math.ceil(tokens[-1])
|
||||
n_bins = int(stop - start) * 10
|
||||
bins = [0] * n_bins
|
||||
for t in tokens:
|
||||
bins[int(n_bins * (t - start) / (stop - start))] += 1
|
||||
|
||||
result = []
|
||||
|
||||
ms = 0
|
||||
cnt = 0
|
||||
for i, b in enumerate(bins):
|
||||
ms += b
|
||||
if cnt == 10:
|
||||
ms -= bins[i - 10]
|
||||
else:
|
||||
cnt += 1
|
||||
|
||||
result.append(10 * ms / cnt)
|
||||
|
||||
times = [start]
|
||||
while times[-1] < stop:
|
||||
times.append(times[-1] + 0.1)
|
||||
|
||||
return times, result
|
||||
|
||||
|
||||
def plot_generation(times, tokens_per_sec, start=None, interval=1.0, width=50):
|
||||
c = "█"
|
||||
start = start or times[0]
|
||||
stop = times[-1]
|
||||
|
||||
bar_times = [start]
|
||||
while bar_times[-1] < stop:
|
||||
bar_times.append(bar_times[-1] + interval)
|
||||
|
||||
bar_values = [[] for _ in bar_times]
|
||||
bar_idx = 0
|
||||
|
||||
for t, v in zip(times, tokens_per_sec):
|
||||
while t > bar_times[bar_idx] + interval:
|
||||
bar_idx += 1
|
||||
bar_values[bar_idx].append(v)
|
||||
|
||||
bar_values = [sum(v) / len(v) if v else 0 for v in bar_values]
|
||||
m = max(bar_values)
|
||||
|
||||
for t, v in zip(bar_times, bar_values):
|
||||
t = t - start
|
||||
b = c * int(v * width / m)
|
||||
print(f"{t:3.2f} {b} ({v})")
|
||||
|
||||
|
||||
def percentile(data, percent):
|
||||
if not data:
|
||||
return 0
|
||||
data = sorted(data)
|
||||
k = (len(data) - 1) * percent / 100
|
||||
f = math.floor(k)
|
||||
c = math.ceil(k)
|
||||
return (
|
||||
data[int(f)]
|
||||
if f == c
|
||||
else data[int(f)] + (data[int(c)] - data[int(f)]) * (k - f)
|
||||
)
|
||||
|
||||
|
||||
def median(data):
|
||||
return percentile(data, 50)
|
||||
|
||||
|
||||
async def make_request(
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
max_tokens: int,
|
||||
) -> Tuple[bool, float, list]:
|
||||
"""
|
||||
Make a single streaming API request and return
|
||||
|
||||
- whether the request succeeded
|
||||
- the request start time
|
||||
- the time of every generated token
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"stream": True,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
start_time = time.perf_counter()
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_body = await response.text()
|
||||
print(f"Error {response.status}: {error_body}")
|
||||
return (False, 0, [])
|
||||
|
||||
# Process streaming response
|
||||
async for chunk in response.content:
|
||||
if chunk:
|
||||
chunk_str = chunk.decode("utf-8").strip()
|
||||
if chunk_str.startswith("data:"):
|
||||
data_str = chunk_str[5:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
if choices := data.get("choices", False):
|
||||
if choices[0].get("finish_reason") != "length":
|
||||
tokens.append(time.perf_counter())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return (bool(tokens), start_time, tokens)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {str(e)}")
|
||||
return (False, 0, [])
|
||||
|
||||
|
||||
async def run_benchmark(
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
concurrency: int,
|
||||
total_requests: int,
|
||||
prompts: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
prompt_cycle = cycle(prompts)
|
||||
semaphore = asyncio.Semaphore(concurrency)
|
||||
results = []
|
||||
request_times = []
|
||||
bar = tqdm(total=total_requests)
|
||||
|
||||
async def worker():
|
||||
async with semaphore:
|
||||
prompt = next(prompt_cycle)
|
||||
result = await make_request(
|
||||
session, url, api_key, model, prompt, max_tokens
|
||||
)
|
||||
bar.update(1)
|
||||
return result
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for _ in range(total_requests):
|
||||
task = asyncio.create_task(worker())
|
||||
tasks.append(task)
|
||||
await asyncio.sleep(0.01) # Stagger requests slightly
|
||||
|
||||
for task in tasks:
|
||||
result = await task
|
||||
results.append(result)
|
||||
bar.close()
|
||||
|
||||
successful_requests = [r for r in results if r[0]]
|
||||
total_tokens = sum(len(r[2]) for r in successful_requests)
|
||||
|
||||
# Gather all the tokens generated with their corresponding timestamps
|
||||
all_tokens = []
|
||||
for r in successful_requests:
|
||||
all_tokens.extend(r[2])
|
||||
all_tokens.sort()
|
||||
full_generation = tokens_per_second(all_tokens)
|
||||
start = min(r[1] for r in successful_requests)
|
||||
|
||||
# Aggregate metrics
|
||||
metrics = {
|
||||
"total_requests": total_requests,
|
||||
"successful_requests": len(successful_requests),
|
||||
"failed_requests": total_requests - len(successful_requests),
|
||||
"total_tokens": total_tokens,
|
||||
"total_time": all_tokens[-1] - start,
|
||||
"aggregate_tokens_per_sec": median(full_generation[1]),
|
||||
"per_request": [],
|
||||
"start": start,
|
||||
"full_generation": full_generation,
|
||||
}
|
||||
|
||||
# Per-request metrics
|
||||
for i, (_, start, tokens) in enumerate(successful_requests):
|
||||
metrics["per_request"].append(
|
||||
{
|
||||
"request_id": i + 1,
|
||||
"time_to_first_token": tokens[0] - start,
|
||||
"total_time": tokens[-1] - start,
|
||||
"tokens_received": len(tokens),
|
||||
"tokens_per_sec": median(tokens_per_second(tokens)[1]),
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate percentiles
|
||||
ttft_values = [m["time_to_first_token"] for m in metrics["per_request"]]
|
||||
tps_values = [m["tokens_per_sec"] for m in metrics["per_request"]]
|
||||
|
||||
metrics["aggregate_metrics"] = {
|
||||
"time_to_first_token": {
|
||||
"min": min(ttft_values) if ttft_values else 0,
|
||||
"max": max(ttft_values) if ttft_values else 0,
|
||||
"avg": sum(ttft_values) / len(ttft_values) if ttft_values else 0,
|
||||
"p95": percentile(ttft_values, 95) if ttft_values else 0,
|
||||
},
|
||||
"tokens_per_sec": {
|
||||
"min": min(tps_values) if tps_values else 0,
|
||||
"max": max(tps_values) if tps_values else 0,
|
||||
"avg": sum(tps_values) / len(tps_values) if tps_values else 0,
|
||||
"p95": percentile(tps_values, 95) if tps_values else 0,
|
||||
},
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LLM API Benchmark Tool")
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
default="http://localhost:8080/v1/chat/completions",
|
||||
help="Chat completions API endpoint URL",
|
||||
)
|
||||
parser.add_argument("--api-key", default="none", help="API key")
|
||||
parser.add_argument("--model", default="default_model", help="Model name")
|
||||
parser.add_argument(
|
||||
"--max-tokens", type=int, default=100, help="Max tokens to generate"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--concurrency", type=int, default=1, help="Number of concurrent requests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-requests", type=int, default=10, help="Total requests to make"
|
||||
)
|
||||
parser.add_argument("--prompt-file", help="File containing prompts (one per line)")
|
||||
parser.add_argument("--output", help="Output file for results (JSON format)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load prompts
|
||||
if args.prompt_file:
|
||||
with open(args.prompt_file, "r") as f:
|
||||
prompts = [line.strip() for line in f if line.strip()]
|
||||
else:
|
||||
prompts = DEFAULT_PROMPTS
|
||||
|
||||
print(
|
||||
f"Starting benchmark with {args.concurrency} concurrency and {args.total_requests} total requests..."
|
||||
)
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Run benchmark
|
||||
results = asyncio.run(
|
||||
run_benchmark(
|
||||
url=args.url,
|
||||
api_key=args.api_key,
|
||||
model=args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
concurrency=args.concurrency,
|
||||
total_requests=args.total_requests,
|
||||
prompts=prompts,
|
||||
)
|
||||
)
|
||||
|
||||
duration = time.perf_counter() - start_time
|
||||
print(f"\nBenchmark completed in {duration:.2f} seconds")
|
||||
print(
|
||||
f"Successful requests: {results['successful_requests']}/{args.total_requests}"
|
||||
)
|
||||
print(f"Total tokens generated: {results['total_tokens']}")
|
||||
print(f"Aggregate tokens/sec: {results['aggregate_tokens_per_sec']:.2f}")
|
||||
|
||||
# Print summary
|
||||
if results["successful_requests"] > 0:
|
||||
ttft = results["aggregate_metrics"]["time_to_first_token"]
|
||||
tps = results["aggregate_metrics"]["tokens_per_sec"]
|
||||
|
||||
print("\nTime to First Token (seconds):")
|
||||
print(
|
||||
f" Min: {ttft['min']:.4f} | Max: {ttft['max']:.4f} | Avg: {ttft['avg']:.4f} | P95: {ttft['p95']:.4f}"
|
||||
)
|
||||
|
||||
print("\nTokens per Second (per request):")
|
||||
print(
|
||||
f" Min: {tps['min']:.2f} | Max: {tps['max']:.2f} | Avg: {tps['avg']:.2f} | P95: {tps['p95']:.2f}"
|
||||
)
|
||||
|
||||
print()
|
||||
plot_generation(*results["full_generation"], results["start"])
|
||||
|
||||
# Save results
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"\nResults saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# Benchmarks
|
||||
|
||||
## Commands
|
||||
|
||||
The command for evaluating on MMLU Pro:
|
||||
|
||||
```
|
||||
mlx_lm.evaluate --model model/repo --task mmlu_pro
|
||||
```
|
||||
|
||||
The command for efficiency benchmarks:
|
||||
|
||||
```
|
||||
mlx_lm.benchmark --model model/repo -p 2048 -g 128
|
||||
```
|
||||
|
||||
To get the package versions run:
|
||||
|
||||
```
|
||||
python -m mlx --version && python -m mlx_lm --version
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
<details>
|
||||
|
||||
<summary> Qwen/Qwen3-4B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 64.05 | 1780.63 | 52.47 | 9.02 | Qwen/Qwen3-4B-Instruct-2507
|
||||
q8 | 63.85 | 1606.573| 86.907 | 5.254 | mlx-community/Qwen3-4B-Instruct-2507-8bit
|
||||
q6 | 63.53 | 1576.73 | 104.68 | 4.25 | mlx-community/Qwen3-4B-Instruct-2507-6bit
|
||||
q5 g32 | 63.16 | 1570.80 | 110.29 | 4.00 | mlx-community/Qwen3-4B-Instruct-2507-5bit-g32
|
||||
q5 | 62.38 | 1584.33 | 116.39 | 3.86 | mlx-community/Qwen3-4B-Instruct-2507-5bit
|
||||
q4 g32 | 61.46 | 1610.03 | 126.00 | 3.603 | mlx-community/Qwen3-4B-Instruct-2507-4bit-g32
|
||||
q4 | 60.72 | 1622.27 | 134.52 | 3.35 | mlx-community/Qwen3-4B-Instruct-2507-4bit
|
||||
|
||||
- Performance benchmark on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> Qwen/Qwen3-30B-A3B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 72.62 | :skull: | :skull: | :skull: | Qwen/Qwen3-30B-A3B-Instruct-2507
|
||||
q8 | 72.46 | 1719.47 | 83.16 | 33.46 | mlx-community/Qwen3-30B-A3B-Instruct-2507-8bit
|
||||
q6 | 72.41 | 1667.45 | 94.14 | 25.82 | mlx-community/Qwen3-30B-A3B-Instruct-2507-6bit
|
||||
q5 | 71.97 | 1664.24 | 101.00 |22.01 | mlx-community/Qwen3-30B-A3B-Instruct-2507-5bit
|
||||
q4 | 70.71 | 1753.90 | 113.33 |18.20 | mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit
|
||||
|
||||
|
||||
- Performance benchmarks on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,170 @@
|
||||
# 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.
|
||||
+70
-13
@@ -26,6 +26,12 @@ 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
|
||||
@@ -60,9 +66,10 @@ mlx_lm.lora \
|
||||
To fine-tune the full model weights, add the `--fine-tune-type full` flag.
|
||||
Currently supported fine-tuning types are `lora` (default), `dora`, and `full`.
|
||||
|
||||
The `--data` argument must specify a path to a `train.jsonl`, `valid.jsonl`
|
||||
when using `--train` and a path to a `test.jsonl` when using `--test`. For more
|
||||
details on the data format see the section on [Data](#Data).
|
||||
The `--data` argument must specify a path to a `train.jsonl` when using
|
||||
`--train` and a path to a `test.jsonl` when using `--test`. A `valid.jsonl` is
|
||||
optional; if provided, validation loss will be reported during training. For
|
||||
more details on the data format see the section on [Data](#Data).
|
||||
|
||||
For example, to fine-tune a Mistral 7B you can use `--model
|
||||
mistralai/Mistral-7B-v0.1`.
|
||||
@@ -76,6 +83,25 @@ 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
|
||||
ignore the prompt and compute loss for just the completion by passing
|
||||
`--mask-prompt`. Note this is only supported for `chat` and `completion`
|
||||
datasets. For `chat` datasets the final message in the message list is
|
||||
considered the completion. See the [dataset section](#Data) for more details.
|
||||
|
||||
### Evaluate
|
||||
|
||||
To compute test set perplexity use:
|
||||
@@ -159,9 +185,10 @@ Face.
|
||||
|
||||
### Local Datasets
|
||||
|
||||
For fine-tuning (`--train`), the data loader expects a `train.jsonl` and a
|
||||
`valid.jsonl` to be in the data directory. For evaluation (`--test`), the data
|
||||
loader expects a `test.jsonl` in the data directory.
|
||||
For fine-tuning (`--train`), the data loader expects a `train.jsonl` to be in
|
||||
the data directory. A `valid.jsonl` is optional; if present, validation loss
|
||||
will be reported periodically during training. For evaluation (`--test`), the
|
||||
data loader expects a `test.jsonl` in the data directory.
|
||||
|
||||
Currently, `*.jsonl` files support `chat`, `tools`, `completions`, and `text`
|
||||
data formats. Here are examples of these formats:
|
||||
@@ -241,14 +268,25 @@ Refer to the documentation for the model you are fine-tuning for more details.
|
||||
{"prompt": "What is the capital of France?", "completion": "Paris."}
|
||||
```
|
||||
|
||||
For the `completions` data format, a different key can be used for the prompt
|
||||
and completion by specifying the following in the YAML config:
|
||||
|
||||
```yaml
|
||||
prompt_feature: "input"
|
||||
completion_feature: "output"
|
||||
```
|
||||
|
||||
Here, `"input"` is the expected key instead of the default `"prompt"`, and
|
||||
`"output"` is the expected key instead of `"completion"`.
|
||||
|
||||
`text`:
|
||||
|
||||
```jsonl
|
||||
{"text": "This is an example for the model."}
|
||||
```
|
||||
|
||||
Note, the format is automatically determined by the dataset. Note also, keys in
|
||||
each line not expected by the loader will be ignored.
|
||||
Note, the format is automatically determined by the dataset. Note also, keys
|
||||
in each line not expected by the loader will be ignored.
|
||||
|
||||
> [!NOTE]
|
||||
> Each example in the datasets must be on a single line. Do not put more than
|
||||
@@ -270,20 +308,36 @@ Otherwise, provide a mapping of keys in the dataset to the features MLX LM
|
||||
expects. Use a YAML config to specify the Hugging Face dataset arguments. For
|
||||
example:
|
||||
|
||||
```
|
||||
```yaml
|
||||
hf_dataset:
|
||||
name: "billsum"
|
||||
path: "billsum"
|
||||
prompt_feature: "text"
|
||||
completion_feature: "summary"
|
||||
```
|
||||
|
||||
- Use `prompt_feature` and `completion_feature` to specify keys for a
|
||||
`completions` dataset. Use `text_feature` to specify the key for a `text`
|
||||
dataset.
|
||||
dataset. Use `chat_feature` to specify the key for a chat dataset.
|
||||
|
||||
- To specify the train, valid, or test splits, set the corresponding
|
||||
`{train,valid,test}_split` argument.
|
||||
|
||||
You can specify a list of Hugging Face datasets with a list of records each
|
||||
with the same structure as above. For example:
|
||||
|
||||
```yaml
|
||||
hf_dataset:
|
||||
- path: "Open-Orca/OpenOrca"
|
||||
train_split: "train[:90%]"
|
||||
valid_split: "train[-10%:]"
|
||||
prompt_feature: "question"
|
||||
completion_feature: "response"
|
||||
- path: "trl-lib/ultrafeedback_binarized"
|
||||
train_split: "train[:90%]"
|
||||
valid_split: "train[-10%:]"
|
||||
chat_feature: "chosen"
|
||||
```
|
||||
|
||||
- Arguments specified in `config` will be passed as keyword arguments to
|
||||
[`datasets.load_dataset`](https://huggingface.co/docs/datasets/v2.20.0/en/package_reference/loading_methods#datasets.load_dataset).
|
||||
|
||||
@@ -319,7 +373,10 @@ of memory. Here are some tips to reduce memory use should you need to do so:
|
||||
|
||||
2. Try using a smaller batch size with `--batch-size`. The default is `4` so
|
||||
setting this to `2` or `1` will reduce memory consumption. This may slow
|
||||
things down a little, but will also reduce the memory use.
|
||||
things down a little, but will also reduce the memory use. You can increase
|
||||
the effective batch size without increasing the memory use by accumulating
|
||||
gradients using `--grad-accumulation-steps <N>` which will accumulate the
|
||||
gradient of `<N>` batches before updating the parameters.
|
||||
|
||||
3. Reduce the number of layers to fine-tune with `--num-layers`. The default
|
||||
is `16`, so you can try `8` or `4`. This reduces the amount of memory
|
||||
@@ -344,7 +401,7 @@ mlx_lm.lora \
|
||||
--train \
|
||||
--batch-size 1 \
|
||||
--num-layers 4 \
|
||||
--data wikisql
|
||||
--data mlx-community/wikisql
|
||||
```
|
||||
|
||||
The above command on an M1 Max with 32 GB runs at about 250
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# 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.
|
||||
+28
-4
@@ -54,24 +54,42 @@ 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 `100`.
|
||||
to generate. Defaults to `512`.
|
||||
|
||||
- `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 `1.0`.
|
||||
Defaults to `0.0`.
|
||||
|
||||
- `top_p`: (Optional) A float specifying the nucleus sampling parameter.
|
||||
Defaults to `1.0`.
|
||||
|
||||
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
|
||||
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 multiplicative penalty to repeated
|
||||
tokens. Defaults to `0.0` (disabled).
|
||||
|
||||
- `repetition_context_size`: (Optional) The size of the context window for
|
||||
applying repetition penalty. Defaults to `20`.
|
||||
|
||||
- `presence_penalty`: (Optional) Applies an additive penalty to tokens
|
||||
that appeared before. Defaults to `0.0` (disabled).
|
||||
|
||||
- `presence_context_size`: (Optional) The size of the context window for
|
||||
applying presence penalty. Defaults to `20`.
|
||||
|
||||
- `frequency_penalty`: (Optional) Applies an additive penalty proportional to
|
||||
how many times a token appeared previously. Defaults to `0.0` (disabled).
|
||||
|
||||
- `frequency_context_size`: (Optional) The size of the context window for
|
||||
applying frequency penalty. Defaults to `20`.
|
||||
|
||||
- `logit_bias`: (Optional) A dictionary mapping token IDs to their bias
|
||||
values. Defaults to `None`.
|
||||
|
||||
@@ -86,6 +104,12 @@ 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.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
### 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/*
|
||||
```
|
||||
+12
-1
@@ -6,4 +6,15 @@ from ._version import __version__
|
||||
|
||||
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
|
||||
|
||||
from .utils import convert, generate, load, stream_generate
|
||||
from .convert import convert
|
||||
from .generate import batch_generate, generate, stream_generate
|
||||
from .utils import load
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"convert",
|
||||
"batch_generate",
|
||||
"generate",
|
||||
"stream_generate",
|
||||
"load",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
if __name__ == "__main__":
|
||||
from . import cli
|
||||
|
||||
cli.main()
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.20.4"
|
||||
__version__ = "0.31.3"
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
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, sharded_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,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quantize-activations",
|
||||
"-qa",
|
||||
action="store_true",
|
||||
help="Quantize activations using the same quantization config as the corresponding layer.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefill-step-size",
|
||||
type=int,
|
||||
default=2048,
|
||||
help="Step size for prefill processing (default: 2048)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Delay between each test in seconds (default: 0)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
mx.random.seed(0)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
if group.size() > 1:
|
||||
model, tokenizer, config = sharded_load(
|
||||
model_path, pipeline_group, tensor_group, return_config=True
|
||||
)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
model_path,
|
||||
return_config=True,
|
||||
tokenizer_config={"trust_remote_code": True},
|
||||
model_config={"quantize_activations": args.quantize_activations},
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
tokenizer._eos_token_ids = {}
|
||||
|
||||
prompt_tokens = args.prompt_tokens
|
||||
generation_tokens = args.generation_tokens
|
||||
batch_size = args.batch_size
|
||||
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,
|
||||
prefill_step_size=args.prefill_step_size,
|
||||
):
|
||||
pass
|
||||
return response
|
||||
|
||||
def batch_bench():
|
||||
return batch_generate(
|
||||
model,
|
||||
tokenizer,
|
||||
prompts,
|
||||
max_tokens=generation_tokens,
|
||||
prefill_step_size=args.prefill_step_size,
|
||||
).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):
|
||||
if args.delay > 0:
|
||||
time.sleep(args.delay)
|
||||
tic = time.perf_counter()
|
||||
response = _bench()
|
||||
toc = time.perf_counter()
|
||||
responses.append(response)
|
||||
results = [(k, getattr(response, k)) for k in report_keys]
|
||||
results = [f"{k}={v:.3f}" for k, v in results]
|
||||
results.append(f"total_time={toc - tic:.3f}")
|
||||
rprint(f"Trial {i+1}: " + ", ".join(results))
|
||||
|
||||
def avg(k):
|
||||
vals = (getattr(response, k) for response in responses)
|
||||
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()
|
||||
+11
-19
@@ -7,8 +7,9 @@ import time
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from .generate import generate_step
|
||||
from .models.cache import make_prompt_cache, save_prompt_cache
|
||||
from .utils import generate_step, load
|
||||
from .utils import load
|
||||
|
||||
DEFAULT_QUANTIZED_KV_START = 5000
|
||||
|
||||
@@ -40,16 +41,6 @@ def setup_arg_parser():
|
||||
default=None,
|
||||
help="End of sequence token for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-chat-template",
|
||||
action="store_true",
|
||||
help="Use the raw prompt without the tokenizer's chat template.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-default-chat-template",
|
||||
action="store_true",
|
||||
help="Use the default chat template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-kv-size",
|
||||
type=int,
|
||||
@@ -106,14 +97,12 @@ def main():
|
||||
|
||||
args.prompt = sys.stdin.read() if args.prompt == "-" else args.prompt
|
||||
|
||||
if args.use_default_chat_template:
|
||||
if tokenizer.chat_template is None:
|
||||
tokenizer.chat_template = tokenizer.default_chat_template
|
||||
|
||||
if not args.ignore_chat_template and tokenizer.chat_template is not None:
|
||||
if tokenizer.has_chat_template:
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=False, continue_final_message=True
|
||||
messages,
|
||||
add_generation_prompt=False,
|
||||
continue_final_message=True,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -147,15 +136,18 @@ def main():
|
||||
pass
|
||||
|
||||
print()
|
||||
print(f"Peak memory: {mx.metal.get_peak_memory() / 1e9:.3f} GB")
|
||||
print(f"Peak memory: {mx.get_peak_memory() / 1e9:.3f} GB")
|
||||
|
||||
print("Saving...")
|
||||
metadata = {}
|
||||
metadata["model"] = args.model
|
||||
metadata["chat_template"] = tokenizer.chat_template
|
||||
metadata["tokenizer_config"] = json.dumps(tokenizer_config)
|
||||
save_prompt_cache(args.prompt_cache_file, cache, metadata)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
+96
-15
@@ -1,16 +1,18 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from .generate import stream_generate
|
||||
from .models.cache import make_prompt_cache
|
||||
from .sample_utils import make_sampler
|
||||
from .utils import load, stream_generate
|
||||
from .utils import load, sharded_load
|
||||
|
||||
DEFAULT_TEMP = 0.0
|
||||
DEFAULT_TOP_P = 1.0
|
||||
DEFAULT_XTC_PROBABILITY = 0.0
|
||||
DEFAULT_XTC_THRESHOLD = 0.0
|
||||
DEFAULT_SEED = 0
|
||||
DEFAULT_MAX_TOKENS = 256
|
||||
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
|
||||
@@ -25,6 +27,11 @@ def setup_arg_parser():
|
||||
help="The path to the local model directory or Hugging Face repo.",
|
||||
default=DEFAULT_MODEL,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
action="store_true",
|
||||
help="Enable trusting remote code for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--adapter-path",
|
||||
type=str,
|
||||
@@ -36,7 +43,24 @@ def setup_arg_parser():
|
||||
parser.add_argument(
|
||||
"--top-p", type=float, default=DEFAULT_TOP_P, help="Sampling top-p"
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=DEFAULT_SEED, help="PRNG seed")
|
||||
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,
|
||||
default=DEFAULT_SEED,
|
||||
help="PRNG seed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-kv-size",
|
||||
type=int,
|
||||
@@ -50,6 +74,16 @@ 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",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -57,33 +91,80 @@ def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={"trust_remote_code": True},
|
||||
)
|
||||
if group.size() > 1:
|
||||
if args.adapter_path:
|
||||
parser.error("Adapters not supported in distributed mode")
|
||||
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
|
||||
else:
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={
|
||||
"trust_remote_code": True if args.trust_remote_code else None
|
||||
},
|
||||
)
|
||||
|
||||
print(f"[INFO] Starting chat session with {args.model}. To exit, enter 'q'.")
|
||||
def print_help():
|
||||
rprint("The command list:")
|
||||
rprint("- 'q' to exit")
|
||||
rprint("- 'r' to reset the chat")
|
||||
rprint("- 'h' to display these commands")
|
||||
|
||||
rprint(f"[INFO] Starting chat session with {args.model}.")
|
||||
print_help()
|
||||
prompt_cache = make_prompt_cache(model, args.max_kv_size)
|
||||
while True:
|
||||
query = input(">> ")
|
||||
query = input(">> " if rank == 0 else "")
|
||||
if query == "q":
|
||||
break
|
||||
messages = [{"role": "user", "content": query}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
if query == "r":
|
||||
prompt_cache = make_prompt_cache(model, args.max_kv_size)
|
||||
continue
|
||||
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})
|
||||
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),
|
||||
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)
|
||||
),
|
||||
),
|
||||
prompt_cache=prompt_cache,
|
||||
):
|
||||
print(response.text, flush=True, end="")
|
||||
print()
|
||||
rprint(response.text, flush=True, end="")
|
||||
rprint()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from inspect import isfunction
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from transformers.utils.chat_template_utils import get_json_schema
|
||||
|
||||
TOOLS_SYSTEM_TEMPLATE = """## Tools
|
||||
|
||||
You have access to a set of tools you can use to answer the user's question.
|
||||
You can invoke functions by writing a "<{dsml_token}function_calls>" block like the following as part of your reply to the user:
|
||||
<{dsml_token}function_calls>
|
||||
<{dsml_token}invoke name="$FUNCTION_NAME">
|
||||
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
<{dsml_token}invoke name="$FUNCTION_NAME2">
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
</{dsml_token}function_calls>
|
||||
|
||||
String and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The "string" attribute should be set to "true" for string type parameters and "false" for other types (numbers, booleans, arrays, objects).
|
||||
|
||||
If the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:
|
||||
|
||||
<{dsml_token}function_calls>
|
||||
...
|
||||
</{dsml_token}function_calls>
|
||||
|
||||
<function_results>
|
||||
...
|
||||
</function_results>
|
||||
|
||||
{thinking_start_token}...thinking about results{thinking_end_token}
|
||||
|
||||
Here are the functions available in JSONSchema format:
|
||||
<functions>
|
||||
{tool_schemas}
|
||||
</functions>
|
||||
"""
|
||||
|
||||
bos_token: str = "<|begin▁of▁sentence|>"
|
||||
eos_token: str = "<|end▁of▁sentence|>"
|
||||
thinking_start_token: str = "<think>"
|
||||
thinking_end_token: str = "</think>"
|
||||
dsml_token: str = "|DSML|"
|
||||
system_msg_template: str = "{content}"
|
||||
user_msg_template: str = "<|User|>{content}<|Assistant|>"
|
||||
assistant_msg_template: str = "{reasoning}{content}{tool_calls}<|end▁of▁sentence|>"
|
||||
thinking_template = "{reasoning_content}"
|
||||
|
||||
response_format_template: str = (
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
|
||||
)
|
||||
tool_call_template: str = (
|
||||
'<{dsml_token}invoke name="{name}">\n{arguments}\n</{dsml_token}invoke>'
|
||||
)
|
||||
tool_calls_template = (
|
||||
"<{dsml_token}function_calls>\n{tool_calls}\n</{dsml_token}function_calls>"
|
||||
)
|
||||
|
||||
tool_output_template: str = "\n<result>{content}</result>"
|
||||
|
||||
|
||||
def to_json(value: Any) -> str:
|
||||
try:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
except:
|
||||
return json.dumps(value, ensure_ascii=True)
|
||||
|
||||
|
||||
def tools_from_openai_format(tools):
|
||||
def normalize_tool(tool):
|
||||
if isfunction(tool):
|
||||
return get_json_schema(tool)
|
||||
return tool["function"]
|
||||
|
||||
return [normalize_tool(tool) for tool in tools]
|
||||
|
||||
|
||||
def tool_calls_from_openai_format(tool_calls):
|
||||
return [
|
||||
{
|
||||
"name": tool_call["function"]["name"],
|
||||
"arguments": tool_call["function"]["arguments"],
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
|
||||
p_dsml_template = """<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>"""
|
||||
P_dsml_strs = []
|
||||
|
||||
arguments = json.loads(tool_call["arguments"])
|
||||
|
||||
for k, v in arguments.items():
|
||||
p_dsml_str = p_dsml_template.format(
|
||||
dsml_token=dsml_token,
|
||||
key=k,
|
||||
is_str="true" if isinstance(v, str) else "false",
|
||||
value=v if isinstance(v, str) else to_json(v),
|
||||
)
|
||||
|
||||
P_dsml_strs.append(p_dsml_str)
|
||||
|
||||
return "\n".join(P_dsml_strs)
|
||||
|
||||
|
||||
def decode_dsml_to_arguments(
|
||||
tool_name: str, tool_args: Dict[str, Tuple[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
def _decode_value(key: str, value: str, string: str):
|
||||
if string == "true":
|
||||
value = to_json(value)
|
||||
return f"{to_json(key)}: {value}"
|
||||
|
||||
tool_args_json = (
|
||||
"{"
|
||||
+ ", ".join(
|
||||
[_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
return dict(name=tool_name, arguments=tool_args_json)
|
||||
|
||||
|
||||
def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
|
||||
tools_json = [to_json(t) for t in tools]
|
||||
|
||||
return TOOLS_SYSTEM_TEMPLATE.format(
|
||||
tool_schemas="\n".join(tools_json),
|
||||
dsml_token=dsml_token,
|
||||
thinking_start_token=thinking_start_token,
|
||||
thinking_end_token=thinking_end_token,
|
||||
)
|
||||
|
||||
|
||||
def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
|
||||
last_user_index = -1
|
||||
for idx in range(len(messages) - 1, -1, -1):
|
||||
if messages[idx].get("role") in ["user", "developer"]:
|
||||
last_user_index = idx
|
||||
break
|
||||
return last_user_index
|
||||
|
||||
|
||||
def render_message(
|
||||
index: int,
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
tools: Any = None,
|
||||
) -> str:
|
||||
assert 0 <= index < len(messages)
|
||||
assert thinking_mode in [
|
||||
"chat",
|
||||
"thinking",
|
||||
], f"Invalid thinking_mode `{thinking_mode}`"
|
||||
|
||||
prompt = ""
|
||||
msg = messages[index]
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
tools = tools or msg.get("tools")
|
||||
response_format = msg.get("response_format")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_content = msg.get("reasoning_content")
|
||||
|
||||
if tool_calls:
|
||||
tool_calls = tool_calls_from_openai_format(tool_calls)
|
||||
|
||||
if role == "system":
|
||||
prompt += system_msg_template.format(content=content or "")
|
||||
if tools:
|
||||
prompt += "\n\n" + render_tools(tools_from_openai_format(tools))
|
||||
|
||||
if response_format:
|
||||
prompt += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
elif role == "developer":
|
||||
assert content, f"Invalid message for role `{role}`: {msg}"
|
||||
content_developer = ""
|
||||
if tools:
|
||||
content_developer += "\n\n" + render_tools(tools_from_openai_format(tools))
|
||||
|
||||
if response_format:
|
||||
content_developer += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
content_developer += "\n\n# The user's message is: {}".format(content)
|
||||
|
||||
prompt += user_msg_template.format(content=content_developer)
|
||||
if index == last_user_idx and thinking_mode == "thinking":
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
elif role == "user":
|
||||
prompt += user_msg_template.format(content=content)
|
||||
|
||||
if index == last_user_idx and thinking_mode == "thinking":
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
elif role == "tool":
|
||||
prev_assistant_idx = index - 1
|
||||
assistant_msg = messages[prev_assistant_idx]
|
||||
while prev_assistant_idx >= 0 and assistant_msg.get("role") == "tool":
|
||||
prev_assistant_idx -= 1
|
||||
assistant_msg = messages[prev_assistant_idx]
|
||||
|
||||
assert (
|
||||
index == 0
|
||||
or prev_assistant_idx >= 0
|
||||
and assistant_msg.get("role") == "assistant"
|
||||
), f"Invalid messages at {index}:\n{assistant_msg}"
|
||||
|
||||
tool_call_order = index - prev_assistant_idx
|
||||
assistant_tool_calls = assistant_msg.get("tool_calls")
|
||||
assert (
|
||||
assistant_tool_calls and len(assistant_tool_calls) >= tool_call_order
|
||||
), "No tool calls but found tool output"
|
||||
|
||||
if tool_call_order == 1:
|
||||
prompt += "\n\n<function_results>"
|
||||
|
||||
prompt += tool_output_template.format(content=content)
|
||||
|
||||
if tool_call_order == len(assistant_tool_calls):
|
||||
prompt += "\n</function_results>"
|
||||
|
||||
if index >= last_user_idx and thinking_mode == "thinking":
|
||||
prompt += "\n\n" + thinking_start_token
|
||||
else:
|
||||
prompt += "\n\n" + thinking_end_token
|
||||
|
||||
elif role == "assistant":
|
||||
prev_assistant_idx = index
|
||||
thinking_part = ""
|
||||
|
||||
tool_calls_content = ""
|
||||
if tool_calls:
|
||||
tool_calls = [
|
||||
tool_call_template.format(
|
||||
dsml_token=dsml_token,
|
||||
name=tool_call.get("name"),
|
||||
arguments=encode_arguments_to_dsml(tool_call),
|
||||
)
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
tool_calls_content += "\n\n" + tool_calls_template.format(
|
||||
dsml_token=dsml_token, tool_calls="\n".join(tool_calls)
|
||||
)
|
||||
|
||||
summary_content = content or ""
|
||||
|
||||
if thinking_mode == "thinking" and index > last_user_idx:
|
||||
assert (
|
||||
reasoning_content or tool_calls
|
||||
), f"ThinkingMode: {thinking_mode}, invalid message without reasoning_content/tool_calls `{msg}` after last user message"
|
||||
thinking_part = (
|
||||
thinking_template.format(reasoning_content=reasoning_content or "")
|
||||
+ thinking_end_token
|
||||
)
|
||||
|
||||
prompt += assistant_msg_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tool_calls_content,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown role: {role}")
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def drop_thinking_messages(
|
||||
messages: List[Dict[str, Any]], last_user_idx: Optional[int] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
messages_wo_thinking: List[Dict[str, Any]] = []
|
||||
last_user_idx = (
|
||||
find_last_user_index(messages) if last_user_idx is None else last_user_idx
|
||||
)
|
||||
for idx, msg in enumerate(messages):
|
||||
role = msg.get("role")
|
||||
if role in ["user", "system", "tool"] or idx >= last_user_idx:
|
||||
messages_wo_thinking.append(msg)
|
||||
continue
|
||||
|
||||
elif role == "assistant":
|
||||
msg_wo_thinking = copy.copy(msg)
|
||||
msg_wo_thinking.pop("reasoning_content", None)
|
||||
messages_wo_thinking.append(msg_wo_thinking)
|
||||
|
||||
return messages_wo_thinking
|
||||
|
||||
|
||||
def encode_messages(
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str = "thinking",
|
||||
context: Optional[List[Dict[str, Any]]] = None,
|
||||
drop_thinking: bool = True,
|
||||
add_default_bos_token: bool = True,
|
||||
tools: Any = None,
|
||||
) -> str:
|
||||
context = context if context else []
|
||||
full_messages = context + messages
|
||||
prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
|
||||
|
||||
if thinking_mode == "thinking" and drop_thinking:
|
||||
full_messages = drop_thinking_messages(full_messages)
|
||||
|
||||
for idx in range(len(messages)):
|
||||
prompt += render_message(
|
||||
idx + len(context),
|
||||
full_messages,
|
||||
thinking_mode=thinking_mode,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def apply_chat_template(
|
||||
messages, continue_final_message=False, add_generation_prompt=False, **kwargs
|
||||
):
|
||||
out = encode_messages(messages, **kwargs)
|
||||
if continue_final_message and add_generation_prompt:
|
||||
raise ValueError(
|
||||
"Only one of continue_final_message or add_generation_prompt can be True"
|
||||
)
|
||||
if not add_generation_prompt and messages[-1]["role"] == "user":
|
||||
out = out.removesuffix("<|Assistant|><think>")
|
||||
if continue_final_message and messages[-1]["role"] == "assistant":
|
||||
out = out.removesuffix(eos_token)
|
||||
return out
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
subcommands = (
|
||||
"benchmark",
|
||||
"cache_prompt",
|
||||
"chat",
|
||||
"convert",
|
||||
"evaluate",
|
||||
"fuse",
|
||||
"generate",
|
||||
"lora",
|
||||
"manage",
|
||||
"perplexity",
|
||||
"awq",
|
||||
"dwq",
|
||||
"dynamic_quant",
|
||||
"gptq",
|
||||
"server",
|
||||
"upload",
|
||||
"share",
|
||||
)
|
||||
subpackages = {
|
||||
"awq": "quant",
|
||||
"dwq": "quant",
|
||||
"dynamic_quant": "quant",
|
||||
"gptq": "quant",
|
||||
}
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
subcommand = sys.argv.pop(1)
|
||||
if subcommand in subcommands:
|
||||
if subpackage := subpackages.get(subcommand):
|
||||
subcommand = f"{subpackage}.{subcommand}"
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
elif subcommand == "--version":
|
||||
from mlx_lm import __version__
|
||||
|
||||
print(__version__)
|
||||
elif subcommand in ("-h", "--help"):
|
||||
print(f"The supported subcommands are {subcommands}")
|
||||
print()
|
||||
print(
|
||||
"For help on an individual subcommand, pass --help "
|
||||
"to the subcommand. For example: mlx_lm.generate --help"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
+212
-7
@@ -1,8 +1,178 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
from .utils import convert
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_map_with_path
|
||||
|
||||
from .utils import (
|
||||
dequantize_model,
|
||||
load,
|
||||
quantize_model,
|
||||
save,
|
||||
upload_to_hub,
|
||||
)
|
||||
|
||||
|
||||
def mixed_quant_predicate_builder(
|
||||
recipe: str, model: nn.Module, group_size: int = 64
|
||||
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
|
||||
mode = "affine"
|
||||
high_bits = 6
|
||||
|
||||
if recipe == "mixed_2_6":
|
||||
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,
|
||||
) -> 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
|
||||
)
|
||||
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:
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
if "down_proj" in path and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
if "lm_head" in path:
|
||||
return {"group_size": group_size, "bits": high_bits, "mode": mode}
|
||||
|
||||
return {"group_size": group_size, "bits": low_bits, "mode": mode}
|
||||
|
||||
return mixed_quant_predicate
|
||||
|
||||
|
||||
QUANT_RECIPES = ["mixed_2_6", "mixed_3_4", "mixed_3_6", "mixed_4_6"]
|
||||
|
||||
MODEL_CONVERSION_DTYPES = ["float16", "bfloat16", "float32"]
|
||||
|
||||
|
||||
def convert(
|
||||
hf_path: str,
|
||||
mlx_path: str = "mlx_model",
|
||||
quantize: bool = False,
|
||||
q_group_size: Optional[int] = None,
|
||||
q_bits: Optional[int] = None,
|
||||
q_mode: str = "affine",
|
||||
dtype: Optional[str] = None,
|
||||
upload_repo: str = None,
|
||||
revision: Optional[str] = None,
|
||||
dequantize: bool = False,
|
||||
quant_predicate: Optional[
|
||||
Union[Callable[[str, nn.Module, dict], Union[bool, dict]], str]
|
||||
] = None,
|
||||
trust_remote_code: bool = False,
|
||||
):
|
||||
# Check the save path is empty
|
||||
if isinstance(mlx_path, str):
|
||||
mlx_path = Path(mlx_path)
|
||||
|
||||
if mlx_path.exists():
|
||||
raise ValueError(
|
||||
f"Cannot save to the path {mlx_path} as it already exists."
|
||||
" Please delete the file/directory or specify a new path to save to."
|
||||
)
|
||||
|
||||
print("[INFO] Loading")
|
||||
model, tokenizer, config = load(
|
||||
hf_path,
|
||||
revision=revision,
|
||||
return_config=True,
|
||||
tokenizer_config={"trust_remote_code": trust_remote_code},
|
||||
lazy=True,
|
||||
)
|
||||
|
||||
if isinstance(quant_predicate, str):
|
||||
if q_mode != "affine":
|
||||
raise ValueError(f"Quant predicates only support 'affine' quantization.")
|
||||
quant_predicate = mixed_quant_predicate_builder(
|
||||
quant_predicate,
|
||||
model,
|
||||
q_group_size,
|
||||
)
|
||||
|
||||
if dtype is None:
|
||||
dtype = config.get("torch_dtype", None)
|
||||
if dtype is None and (text_config := config.get("text_config", None)):
|
||||
dtype = text_config.get("dtype", None)
|
||||
if dtype in MODEL_CONVERSION_DTYPES:
|
||||
print("[INFO] Using dtype:", dtype)
|
||||
dtype = getattr(mx, dtype)
|
||||
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()))
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if dequantize:
|
||||
print("[INFO] Dequantizing")
|
||||
config.pop("quantization", None)
|
||||
config.pop("quantization_config", None)
|
||||
model = dequantize_model(model)
|
||||
|
||||
save(
|
||||
mlx_path,
|
||||
hf_path,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
)
|
||||
|
||||
if upload_repo is not None:
|
||||
upload_to_hub(mlx_path, upload_repo)
|
||||
|
||||
|
||||
def configure_parser() -> argparse.ArgumentParser:
|
||||
@@ -16,7 +186,12 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
description="Convert Hugging Face model to MLX format"
|
||||
)
|
||||
|
||||
parser.add_argument("--hf-path", type=str, help="Path to the Hugging Face model.")
|
||||
parser.add_argument(
|
||||
"--hf-path",
|
||||
"--model",
|
||||
type=str,
|
||||
help="Path to the model. This can be a local path or a Hugging Face Hub model identifier.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mlx-path", type=str, default="mlx_model", help="Path to save the MLX model."
|
||||
)
|
||||
@@ -24,17 +199,37 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
"-q", "--quantize", help="Generate a quantized model.", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-group-size", help="Group size for quantization.", type=int, default=64
|
||||
"--q-group-size",
|
||||
help="Group size for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
|
||||
"--q-bits",
|
||||
help="Bits per weight for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-mode",
|
||||
help="The quantization mode.",
|
||||
type=str,
|
||||
default="affine",
|
||||
choices=["affine", "mxfp4", "nvfp4", "mxfp8"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quant-predicate",
|
||||
help=f"Mixed-bit quantization recipe.",
|
||||
choices=QUANT_RECIPES,
|
||||
type=str,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dtype",
|
||||
help="Type to save the non-quantized parameters.",
|
||||
help="Type to save the non-quantized parameters. Defaults to config.json's `torch_dtype` or the current model weights dtype.",
|
||||
type=str,
|
||||
choices=["float16", "bfloat16", "float32"],
|
||||
default="float16",
|
||||
choices=MODEL_CONVERSION_DTYPES,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upload-repo",
|
||||
@@ -49,6 +244,12 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
help="Trust remote code when loading tokenizer.",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -59,4 +260,8 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(
|
||||
"Calling `python -m mlx_lm.convert ...` directly is deprecated."
|
||||
" Use `mlx_lm.convert ...` or `python -m mlx_lm convert ...` instead."
|
||||
)
|
||||
main()
|
||||
|
||||
+282
-161
@@ -5,12 +5,14 @@ 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 Optional, Union
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import lm_eval
|
||||
import mlx.core as mx
|
||||
@@ -20,19 +22,12 @@ from lm_eval.api.model import LM
|
||||
from lm_eval.api.registry import register_model
|
||||
from tqdm import tqdm
|
||||
|
||||
from .generate import batch_generate
|
||||
from .models.cache import make_prompt_cache
|
||||
from .utils import load, stream_generate
|
||||
from .sample_utils import make_sampler
|
||||
from .utils import load
|
||||
|
||||
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
|
||||
DEFAULT_MAX_TOKENS = 8192
|
||||
|
||||
|
||||
def _rstrip_until(s, untils):
|
||||
@@ -43,114 +38,104 @@ def _rstrip_until(s, untils):
|
||||
return s[: min(f)]
|
||||
|
||||
|
||||
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],
|
||||
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],
|
||||
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):
|
||||
|
||||
apply_chat_template = chat_template_fn()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path_or_hf_repo: str,
|
||||
batch_size: int = 16,
|
||||
max_tokens: Optional[int] = None,
|
||||
batch_size: int = 8,
|
||||
use_chat_template: Optional[bool] = None,
|
||||
trust_remote_code: bool = False,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
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
|
||||
tokenizer_config = {"trust_remote_code": True if trust_remote_code else None}
|
||||
self._model, self.tokenizer = load(
|
||||
path_or_hf_repo, tokenizer_config=tokenizer_config
|
||||
)
|
||||
self._max_tokens = max_tokens
|
||||
self._batch_size = batch_size
|
||||
self.use_chat_template = use_chat_template
|
||||
if use_chat_template is None:
|
||||
self.use_chat_template = self.tokenizer.chat_template is not None
|
||||
self._sampler = sampler
|
||||
|
||||
def _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)
|
||||
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)
|
||||
inputs, targets = inputs[..., :-1], inputs[..., 1:]
|
||||
|
||||
cache = make_prompt_cache(self._model)
|
||||
|
||||
mask = targets != PAD
|
||||
|
||||
cache = cache or make_prompt_cache(self._model)
|
||||
offset = 0
|
||||
scores, is_greedy = [], []
|
||||
for i in range(0, inputs.shape[1], step_size):
|
||||
logits = self._model(inputs[:, i : i + step_size], cache=cache)
|
||||
inp = inputs[:, i : i + step_size]
|
||||
T = inp.shape[1]
|
||||
|
||||
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 = mask[:, i : i + step_size] * (
|
||||
targets[:, i : i + step_size] == mx.argmax(logits, axis=-1)
|
||||
)
|
||||
|
||||
ig = targets[:, i : i + step_size] == mx.argmax(logits, axis=-1)
|
||||
ig = mx.where(mx.arange(offset, T + offset) < lengths[:, None], ig, False)
|
||||
|
||||
mx.eval(score, ig)
|
||||
mx.metal.clear_cache()
|
||||
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, 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
|
||||
return scores, lengths, is_greedy
|
||||
|
||||
def _tokenize(self, texts):
|
||||
return [
|
||||
@@ -160,6 +145,10 @@ class MLXLM(LM):
|
||||
for t in texts
|
||||
]
|
||||
|
||||
@property
|
||||
def tokenizer_name(self) -> str:
|
||||
return self.tokenizer.name_or_path.replace("/", "__")
|
||||
|
||||
def loglikelihood(self, requests) -> list[tuple[float, bool]]:
|
||||
"""Compute log-likelihood of generating a continuation from a context.
|
||||
Downstream tasks should attempt to use loglikelihood instead of other
|
||||
@@ -182,39 +171,63 @@ class MLXLM(LM):
|
||||
"""
|
||||
logging.info("Estimating loglikelihood for %d pairs." % len(requests))
|
||||
|
||||
# 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 = mx.distributed.init()
|
||||
|
||||
# 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)),
|
||||
)
|
||||
# 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()]
|
||||
|
||||
# truncate requests for completed sequences longer than model context.
|
||||
shortened = []
|
||||
completion_spans = []
|
||||
long_completions = 0
|
||||
for prefix, completed in zip(tokenized[0::2], tokenized[1::2]):
|
||||
max_completed_l, prefix_l = length_stats[prefix]
|
||||
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)
|
||||
|
||||
# compute truncation length
|
||||
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.
|
||||
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:
|
||||
long_completions += 1
|
||||
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))
|
||||
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()
|
||||
|
||||
if long_completions > 0:
|
||||
logging.info(
|
||||
@@ -222,16 +235,31 @@ class MLXLM(LM):
|
||||
+ "completion longer than context."
|
||||
)
|
||||
|
||||
# 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]
|
||||
# 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)
|
||||
|
||||
tokenizer_name = lm_eval.models.huggingface.HFLM.tokenizer_name
|
||||
apply_chat_template = lm_eval.models.huggingface.HFLM.apply_chat_template
|
||||
# Arrange the indices to match the scores from each node and then
|
||||
# inverse sort the scores
|
||||
all_indices = []
|
||||
for rank in range(group.size()):
|
||||
rank_indices = [
|
||||
idx for question in indices[rank :: group.size()] for idx in question
|
||||
]
|
||||
rank_indices += [num_results] * (per_group - len(rank_indices))
|
||||
all_indices.extend(rank_indices)
|
||||
inv_sort = mx.argsort(mx.array(all_indices))
|
||||
scores = scores[:num_results][inv_sort]
|
||||
is_greedy = is_greedy[:num_results][inv_sort]
|
||||
|
||||
return list(zip(scores.tolist(), is_greedy.tolist()))
|
||||
|
||||
def loglikelihood_rolling(self, requests) -> list[float]:
|
||||
"""Compute full log-likelihood of a string, with no truncation, for perplexity computation
|
||||
@@ -268,8 +296,15 @@ class MLXLM(LM):
|
||||
logging.info(
|
||||
"Estimating loglikelihood rolling for %d sequences." % len(requests)
|
||||
)
|
||||
inputs = [req.args[0] for req in requests]
|
||||
return [t[0] for t in self._loglikelihood(inputs)]
|
||||
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
|
||||
|
||||
def generate_until(self, requests) -> list[str]:
|
||||
"""Generate greedily until a stopping sequence
|
||||
@@ -285,32 +320,77 @@ class MLXLM(LM):
|
||||
continuation: str
|
||||
The generated continuation.
|
||||
"""
|
||||
group = mx.distributed.init()
|
||||
|
||||
# split data accross ranks
|
||||
total_requests = len(requests)
|
||||
requests = requests[group.rank() :: group.size()]
|
||||
|
||||
logging.info("Generating continuation for %d sequences." % len(requests))
|
||||
contexts, options = zip(*[req.args for req in requests])
|
||||
# contrary to the doc the second element of the tuple contains
|
||||
# The second element of the tuple contains:
|
||||
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
|
||||
keys = list(options[0].keys())
|
||||
assert "until" in keys
|
||||
untils = [x["until"] for x in options]
|
||||
completions = []
|
||||
|
||||
for context, until in tqdm(zip(contexts, untils), total=len(contexts)):
|
||||
context = self._tokenize(context)
|
||||
max_tokens = min(
|
||||
self._max_tokens,
|
||||
self.tokenizer.model_max_length - len(context),
|
||||
# Tokenize all contexts
|
||||
contexts = [
|
||||
self.tokenizer.encode(
|
||||
context, add_special_tokens=not self.use_chat_template
|
||||
)
|
||||
text = ""
|
||||
for response in stream_generate(
|
||||
self._model, self.tokenizer, prompt=context, max_tokens=max_tokens
|
||||
):
|
||||
text += response.text
|
||||
if any(u in text for u in until):
|
||||
text = _rstrip_until(text, until)
|
||||
completions.append(text)
|
||||
break
|
||||
else:
|
||||
completions.append(text)
|
||||
for context in contexts
|
||||
]
|
||||
|
||||
# TODO consider multi-token, per-prompt stop conditions
|
||||
max_tokens = [
|
||||
self._max_tokens or opt.get("max_gen_tokens", DEFAULT_MAX_TOKENS)
|
||||
for opt in options
|
||||
]
|
||||
|
||||
completions = batch_generate(
|
||||
model=self._model,
|
||||
tokenizer=self.tokenizer,
|
||||
prompts=contexts,
|
||||
max_tokens=max_tokens,
|
||||
verbose=True,
|
||||
sampler=self._sampler,
|
||||
).texts
|
||||
|
||||
for e, (text, opt) in enumerate(zip(completions, options)):
|
||||
completions[e] = _rstrip_until(text, opt["until"])
|
||||
if self.tokenizer.has_thinking:
|
||||
completions[e] = _lstrip(text, self.tokenizer.think_end)
|
||||
|
||||
# Gather the completions
|
||||
if group.size() > 1:
|
||||
with mx.stream(mx.cpu):
|
||||
pad_to = (total_requests + group.size() - 1) // group.size()
|
||||
pad = pad_to - len(completions)
|
||||
completions = [list(c.encode("utf-8")) for c in completions]
|
||||
max_len = mx.array(max(len(c) for c in completions))
|
||||
max_len = mx.distributed.all_max(max_len).item()
|
||||
lengths = mx.array([len(c) for c in completions] + [0] * pad)
|
||||
completions = mx.array(
|
||||
[c + [0] * (max_len - len(c)) for c in completions]
|
||||
+ [[0] * max_len] * pad,
|
||||
mx.uint8,
|
||||
)
|
||||
completions = (
|
||||
mx.distributed.all_gather(completions[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
lengths = (
|
||||
mx.distributed.all_gather(lengths[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
completions = completions[:total_requests]
|
||||
lengths = lengths[:total_requests]
|
||||
completions = [
|
||||
bytearray(c[:l]).decode() for c, l in zip(completions, lengths)
|
||||
]
|
||||
|
||||
return completions
|
||||
|
||||
|
||||
@@ -324,17 +404,19 @@ 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=0, help="Number of shots")
|
||||
parser.add_argument("--num-shots", type=int, default=None, help="Number of shots")
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
help="Maximum nunber of tokens to generate. Defaults to the model's max context length.",
|
||||
help="Maximum number of tokens to generate. When set, this value takes"
|
||||
" precedence over task specific defaults.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
default=1.0,
|
||||
default=None,
|
||||
help="Limit the number of examples per task.",
|
||||
type=float,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=123, help="Random seed.")
|
||||
parser.add_argument(
|
||||
@@ -352,6 +434,27 @@ 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)
|
||||
@@ -362,12 +465,27 @@ 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,
|
||||
batch_size=args.batch_size,
|
||||
use_chat_template=args.apply_chat_template,
|
||||
trust_remote_code=args.trust_remote_code,
|
||||
sampler=sampler,
|
||||
)
|
||||
MLXLM.apply_chat_template = chat_template_fn(**args.chat_template_args)
|
||||
|
||||
results = lm_eval.simple_evaluate(
|
||||
model=lm,
|
||||
tasks=args.tasks,
|
||||
@@ -379,14 +497,17 @@ 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,
|
||||
)
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from mlx_lm import batch_generate, load
|
||||
|
||||
# Specify the checkpoint
|
||||
checkpoint = "mlx-community/Llama-3.2-3B-Instruct-4bit"
|
||||
|
||||
# Load the corresponding model and tokenizer
|
||||
model, tokenizer = load(path_or_hf_repo=checkpoint)
|
||||
|
||||
# A batch of prompts
|
||||
prompts = [
|
||||
"Write a story about Einstein.",
|
||||
"Why is the sky blue?",
|
||||
"What time is it?",
|
||||
"How tall is Mt Everest?",
|
||||
]
|
||||
|
||||
# Apply the chat template and encode to tokens
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts
|
||||
]
|
||||
|
||||
# Set `verbose=True` to see generation statistics
|
||||
result = batch_generate(
|
||||
model, tokenizer, prompts, verbose=False, return_prompt_caches=True, max_tokens=2048
|
||||
)
|
||||
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])
|
||||
@@ -15,7 +15,10 @@ prompt_cache = make_prompt_cache(model)
|
||||
# User turn
|
||||
prompt = "Hi my name is <Name>."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
@@ -23,14 +26,16 @@ response = generate(
|
||||
tokenizer,
|
||||
prompt=prompt,
|
||||
verbose=True,
|
||||
temp=0.0,
|
||||
prompt_cache=prompt_cache,
|
||||
)
|
||||
|
||||
# User turn
|
||||
prompt = "What's my name?"
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
|
||||
@@ -14,7 +14,8 @@ conversation = [{"role": "user", "content": prompt}]
|
||||
|
||||
# Transform the prompt into the chat template
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
conversation=conversation, add_generation_prompt=True
|
||||
conversation=conversation,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Specify the maximum number of tokens
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The path to the local model directory or Hugging Face repo.
|
||||
model: "mlx_model"
|
||||
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
|
||||
|
||||
# Whether or not to train (boolean)
|
||||
train: true
|
||||
@@ -7,8 +7,17 @@ train: true
|
||||
# The fine-tuning method: "lora", "dora", or "full".
|
||||
fine_tune_type: lora
|
||||
|
||||
# The Optimizer with its possible inputs
|
||||
optimizer: adamw
|
||||
# optimizer_config:
|
||||
# adamw:
|
||||
# betas: [0.9, 0.98]
|
||||
# eps: 1e-6
|
||||
# weight_decay: 0.05
|
||||
# bias_correction: true
|
||||
|
||||
# Directory with {train, valid, test}.jsonl files
|
||||
data: "/path/to/training/data"
|
||||
data: "mlx-community/WikiSQL"
|
||||
|
||||
# The PRNG seed
|
||||
seed: 0
|
||||
@@ -28,12 +37,19 @@ 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
|
||||
|
||||
@@ -72,9 +88,8 @@ lora_parameters:
|
||||
# arguments: [1e-5, 1000, 1e-7] # passed to scheduler
|
||||
|
||||
#hf_dataset:
|
||||
# name: "billsum"
|
||||
# path: "billsum"
|
||||
# train_split: "train[:1000]"
|
||||
# valid_split: "train[-100:]"
|
||||
# prompt_feature: "text"
|
||||
# completion_feature: "summary"
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="not-needed",
|
||||
base_url="http://localhost:8080/v1",
|
||||
)
|
||||
|
||||
model = "mlx-community/Qwen3-4B-Thinking-2507-4bit"
|
||||
|
||||
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
|
||||
|
||||
# Non-streaming example
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model, messages=messages, max_tokens=2048
|
||||
)
|
||||
|
||||
reasoning = response.choices[0].message.reasoning
|
||||
content = response.choices[0].message.content
|
||||
|
||||
print("=== reasoning ===\n")
|
||||
print(f"\033[37m{reasoning}\033[0m")
|
||||
print("=== content ===\n")
|
||||
print(content)
|
||||
|
||||
# Streaming example
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
max_tokens=2048,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
if (reasoning := chunk.choices[0].delta.reasoning) is not None:
|
||||
print(f"\033[37m{reasoning}\033[0m", end="")
|
||||
if (content := chunk.choices[0].delta.content) is not None:
|
||||
print(f"{content}", end="")
|
||||
print()
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
"""
|
||||
import json
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
|
||||
|
||||
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
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)
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
"""
|
||||
Run with:
|
||||
|
||||
```
|
||||
mlx.launch \
|
||||
--backend jaccl \
|
||||
--env MLX_METAL_FAST_SYNCH=1 \
|
||||
--hostfile /path/to/hosts.json \
|
||||
/path/to/sharded_generate.py \
|
||||
--prompt 'Hello world'
|
||||
```
|
||||
|
||||
For more information on running distributed programs with MLX see the documentation:
|
||||
|
||||
https://ml-explore.github.io/mlx/build/html/usage/distributed.html .
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import stream_generate
|
||||
from mlx_lm.utils import sharded_load
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM distributed inference example")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="mlx-community/Llama-3.3-70B-Instruct-4bit",
|
||||
help="HF repo or path to local model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
"-p",
|
||||
default="Write a quicksort in C++.",
|
||||
help="Message to be processed by the model ('-' reads from stdin)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
"-m",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Maximum number of tokens to generate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
|
||||
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for response in stream_generate(
|
||||
model, tokenizer, prompt, max_tokens=args.max_tokens
|
||||
):
|
||||
rprint(response.text, end="", flush=True)
|
||||
|
||||
rprint()
|
||||
rprint("=" * 10)
|
||||
rprint(
|
||||
f"Prompt: {response.prompt_tokens} tokens, "
|
||||
f"{response.prompt_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(
|
||||
f"Generation: {response.generation_tokens} tokens, "
|
||||
f"{response.generation_tps:.3f} tokens-per-sec"
|
||||
)
|
||||
rprint(f"Peak memory: {response.peak_memory:.3f} GB")
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import json
|
||||
|
||||
from mlx_lm import generate, load
|
||||
from mlx_lm.models.cache import make_prompt_cache
|
||||
|
||||
# Specify the checkpoint
|
||||
checkpoint = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
|
||||
# Load the corresponding model and tokenizer
|
||||
model, tokenizer = load(path_or_hf_repo=checkpoint)
|
||||
|
||||
|
||||
# An example tool, make sure to include a docstring and type hints
|
||||
def multiply(a: float, b: float):
|
||||
"""
|
||||
A function that multiplies two numbers
|
||||
|
||||
Args:
|
||||
a: The first number to multiply
|
||||
b: The second number to multiply
|
||||
"""
|
||||
return a * b
|
||||
|
||||
|
||||
tools = {"multiply": multiply}
|
||||
|
||||
# Specify the prompt and conversation history
|
||||
prompt = "Multiply 12234585 and 48838483920."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
tools=list(tools.values()),
|
||||
)
|
||||
|
||||
prompt_cache = make_prompt_cache(model)
|
||||
|
||||
# Generate the initial tool call:
|
||||
response = generate(
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
prompt=prompt,
|
||||
max_tokens=2048,
|
||||
verbose=True,
|
||||
prompt_cache=prompt_cache,
|
||||
)
|
||||
|
||||
# Parse the tool call:
|
||||
# - The tool call format is model specific.
|
||||
# - The tokenizer's tool parser expects tool call text to be already extracted.
|
||||
start_tool = response.find(tokenizer.tool_call_start) + len(tokenizer.tool_call_start)
|
||||
end_tool = response.find(tokenizer.tool_call_end)
|
||||
tool_call = tokenizer.tool_parser(response[start_tool:end_tool].strip())
|
||||
tool_result = tools[tool_call["name"]](**tool_call["arguments"])
|
||||
|
||||
# Put the tool result in the prompt
|
||||
messages = [{"role": "tool", "name": tool_call["name"], "content": tool_result}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Generate the final response:
|
||||
response = generate(
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
prompt=prompt,
|
||||
max_tokens=2048,
|
||||
verbose=True,
|
||||
prompt_cache=prompt_cache,
|
||||
)
|
||||
+31
-50
@@ -1,19 +1,13 @@
|
||||
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 (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
save_config,
|
||||
save_weights,
|
||||
dequantize_model,
|
||||
load,
|
||||
save,
|
||||
upload_to_hub,
|
||||
)
|
||||
|
||||
@@ -38,12 +32,6 @@ 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.",
|
||||
@@ -51,8 +39,8 @@ def parse_arguments() -> argparse.Namespace:
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--de-quantize",
|
||||
help="Generate a de-quantized model.",
|
||||
"--dequantize",
|
||||
help="Generate a dequantized model.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -73,39 +61,34 @@ def main() -> None:
|
||||
print("Loading pretrained model")
|
||||
args = parse_arguments()
|
||||
|
||||
model_path = get_model_path(args.model)
|
||||
model, config, tokenizer = fetch_from_hub(model_path)
|
||||
|
||||
model.freeze()
|
||||
model = load_adapters(model, args.adapter_path)
|
||||
model, tokenizer, config = load(
|
||||
args.model, adapter_path=args.adapter_path, return_config=True
|
||||
)
|
||||
|
||||
fused_linears = [
|
||||
(n, m.fuse()) for n, m in model.named_modules() if hasattr(m, "fuse")
|
||||
(n, m.fuse(dequantize=args.dequantize))
|
||||
for n, m in model.named_modules()
|
||||
if hasattr(m, "fuse")
|
||||
]
|
||||
|
||||
if fused_linears:
|
||||
model.update_modules(tree_unflatten(fused_linears))
|
||||
|
||||
if args.de_quantize:
|
||||
print("De-quantizing model")
|
||||
model = dequantize(model)
|
||||
|
||||
weights = dict(tree_flatten(model.parameters()))
|
||||
if args.dequantize:
|
||||
print("Dequantizing model")
|
||||
model = dequantize_model(model)
|
||||
config.pop("quantization", None)
|
||||
config.pop("quantization_config", None)
|
||||
|
||||
save_path = Path(args.save_path)
|
||||
|
||||
save_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")
|
||||
save(
|
||||
save_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
donate_model=False,
|
||||
)
|
||||
|
||||
if args.export_gguf:
|
||||
model_type = config["model_type"]
|
||||
@@ -113,18 +96,16 @@ def main() -> None:
|
||||
raise ValueError(
|
||||
f"Model type {model_type} not supported for GGUF conversion."
|
||||
)
|
||||
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
|
||||
weights = dict(tree_flatten(model.parameters()))
|
||||
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
|
||||
|
||||
if args.upload_repo is not None:
|
||||
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)
|
||||
upload_to_hub(args.save_path, args.upload_repo)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
+1876
-18
File diff suppressed because it is too large
Load Diff
+110
-30
@@ -1,19 +1,19 @@
|
||||
# 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
|
||||
import mlx.nn as nn
|
||||
import mlx.optimizers as optim
|
||||
import numpy as np
|
||||
import yaml
|
||||
|
||||
from .tokenizer_utils import TokenizerWrapper
|
||||
from .tuner.datasets import load_dataset
|
||||
from .tuner.callbacks import get_reporting_callbacks
|
||||
from .tuner.datasets import CacheDataset, load_dataset
|
||||
from .tuner.trainer import TrainingArgs, TrainingCallback, evaluate, train
|
||||
from .tuner.utils import (
|
||||
build_schedule,
|
||||
@@ -21,7 +21,7 @@ from .tuner.utils import (
|
||||
load_adapters,
|
||||
print_trainable_parameters,
|
||||
)
|
||||
from .utils import load, save_config
|
||||
from .utils import _parse_size, load, save_config
|
||||
|
||||
yaml_loader = yaml.SafeLoader
|
||||
yaml_loader.add_implicit_resolver(
|
||||
@@ -40,10 +40,18 @@ yaml_loader.add_implicit_resolver(
|
||||
)
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
"model": "mlx_model",
|
||||
"model": "Qwen/Qwen3-0.6b",
|
||||
"train": False,
|
||||
"fine_tune_type": "lora",
|
||||
"data": "data/",
|
||||
"optimizer": "adam",
|
||||
"optimizer_config": {
|
||||
"adam": {},
|
||||
"adamw": {},
|
||||
"muon": {},
|
||||
"sgd": {},
|
||||
"adafactor": {},
|
||||
},
|
||||
"data": "mlx-community/WikiSQL",
|
||||
"seed": 0,
|
||||
"num_layers": 16,
|
||||
"batch_size": 4,
|
||||
@@ -58,8 +66,15 @@ CONFIG_DEFAULTS = {
|
||||
"test": False,
|
||||
"test_batches": 500,
|
||||
"max_seq_length": 2048,
|
||||
"config": None,
|
||||
"grad_checkpoint": False,
|
||||
"grad_accumulation_steps": 1,
|
||||
"clear_cache_threshold": 0,
|
||||
"lr_schedule": None,
|
||||
"lora_parameters": {"rank": 8, "alpha": 16, "dropout": 0.0, "scale": 10.0},
|
||||
"lora_parameters": {"rank": 8, "dropout": 0.0, "scale": 20.0},
|
||||
"mask_prompt": False,
|
||||
"report_to": None,
|
||||
"project_name": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +82,7 @@ def build_parser():
|
||||
parser = argparse.ArgumentParser(description="LoRA or QLoRA finetuning.")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
help="The path to the local model directory or Hugging Face repo.",
|
||||
)
|
||||
|
||||
@@ -89,9 +105,21 @@ def build_parser():
|
||||
"--fine-tune-type",
|
||||
type=str,
|
||||
choices=["lora", "dora", "full"],
|
||||
default="lora",
|
||||
help="Type of fine-tuning to perform: lora, dora, or full.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--optimizer",
|
||||
type=str,
|
||||
choices=["adam", "adamw", "muon", "sgd", "adafactor"],
|
||||
default=None,
|
||||
help="Optimizer to use for training: adam, adamw, sgd, or adafactor.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-prompt",
|
||||
action="store_true",
|
||||
help="Mask the prompt in the loss when training",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-layers",
|
||||
type=int,
|
||||
@@ -115,6 +143,11 @@ def build_parser():
|
||||
type=int,
|
||||
help="Number of training steps between validations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--grad-accumulation-steps",
|
||||
type=int,
|
||||
help="Number of steps to accumulate before each optimizer update.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume-adapter-file",
|
||||
type=str,
|
||||
@@ -149,7 +182,7 @@ def build_parser():
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
default=None,
|
||||
type=str,
|
||||
help="A YAML configuration file with the training options",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -158,22 +191,48 @@ def build_parser():
|
||||
help="Use gradient checkpointing to reduce memory use.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=None, help="The PRNG seed")
|
||||
parser.add_argument(
|
||||
"--clear-cache-threshold",
|
||||
type=_parse_size,
|
||||
default=0,
|
||||
help="Clear the allocator cache between steps if it grows too large.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-to",
|
||||
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
|
||||
|
||||
|
||||
def train_model(
|
||||
args,
|
||||
model: nn.Module,
|
||||
tokenizer: TokenizerWrapper,
|
||||
train_set,
|
||||
valid_set,
|
||||
training_callback: TrainingCallback = None,
|
||||
):
|
||||
mx.random.seed(args.seed)
|
||||
model.freeze()
|
||||
if args.num_layers > len(model.layers):
|
||||
raise ValueError(
|
||||
f"Requested to train {args.num_layers} layers "
|
||||
f"but the model only has {len(model.layers)} layers."
|
||||
)
|
||||
|
||||
if args.fine_tune_type == "full":
|
||||
for l in model.layers[-min(args.num_layers, 0) :]:
|
||||
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(
|
||||
@@ -209,33 +268,44 @@ 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,
|
||||
)
|
||||
|
||||
model.train()
|
||||
opt = optim.Adam(
|
||||
learning_rate=(
|
||||
build_schedule(args.lr_schedule) if args.lr_schedule else args.learning_rate
|
||||
)
|
||||
)
|
||||
# Initialize the selected optimizer
|
||||
lr = build_schedule(args.lr_schedule) if args.lr_schedule else args.learning_rate
|
||||
|
||||
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}")
|
||||
|
||||
opt = opt_class(learning_rate=lr, **optimizer_config)
|
||||
|
||||
# Train model
|
||||
train(
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
args=training_args,
|
||||
optimizer=opt,
|
||||
train_dataset=train_set,
|
||||
val_dataset=valid_set,
|
||||
train_dataset=CacheDataset(train_set),
|
||||
val_dataset=CacheDataset(valid_set),
|
||||
training_callback=training_callback,
|
||||
)
|
||||
|
||||
|
||||
def evaluate_model(args, model: nn.Module, tokenizer: TokenizerWrapper, test_set):
|
||||
model.eval()
|
||||
|
||||
def evaluate_model(args, model: nn.Module, test_set):
|
||||
test_loss = evaluate(
|
||||
model=model,
|
||||
dataset=test_set,
|
||||
tokenizer=tokenizer,
|
||||
dataset=CacheDataset(test_set),
|
||||
batch_size=args.batch_size,
|
||||
num_batches=args.test_batches,
|
||||
max_seq_length=args.max_seq_length,
|
||||
@@ -248,9 +318,15 @@ def evaluate_model(args, model: nn.Module, tokenizer: TokenizerWrapper, 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)
|
||||
model, tokenizer = load(args.model, tokenizer_config={"trust_remote_code": True})
|
||||
|
||||
print("Loading datasets")
|
||||
train_set, valid_set, test_set = load_dataset(args, tokenizer)
|
||||
@@ -262,13 +338,13 @@ def run(args, training_callback: TrainingCallback = None):
|
||||
|
||||
elif args.train:
|
||||
print("Training")
|
||||
train_model(args, model, tokenizer, train_set, valid_set, training_callback)
|
||||
train_model(args, model, 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, tokenizer, test_set)
|
||||
evaluate_model(args, model, test_set)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -294,4 +370,8 @@ 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()
|
||||
|
||||
+20
-1
@@ -2,7 +2,22 @@ import argparse
|
||||
from typing import List, Union
|
||||
|
||||
from huggingface_hub import scan_cache_dir
|
||||
from transformers.commands.user import tabulate
|
||||
|
||||
|
||||
def tabulate(rows: List[List[Union[str, int]]], headers: List[str]) -> str:
|
||||
"""
|
||||
Inspired by:
|
||||
- stackoverflow.com/a/8356620/593036
|
||||
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data
|
||||
"""
|
||||
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)]
|
||||
row_format = ("{{:{}}} " * len(headers)).format(*col_widths)
|
||||
lines = []
|
||||
lines.append(row_format.format(*headers))
|
||||
lines.append(row_format.format(*["-" * w for w in col_widths]))
|
||||
for row in rows:
|
||||
lines.append(row_format.format(*row))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def ask_for_confirmation(message: str) -> bool:
|
||||
@@ -121,4 +136,8 @@ 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
@@ -1,172 +0,0 @@
|
||||
# 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()
|
||||
@@ -0,0 +1,263 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, 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(swiglu(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
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright © 2023-2026 Apple Inc.
|
||||
|
||||
from functools import partial
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(gate, x):
|
||||
return nn.silu(gate) * x
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def xielu(x, alpha_p, alpha_n, beta, eps):
|
||||
alpha_p = nn.softplus(alpha_p)
|
||||
alpha_n = beta + nn.softplus(alpha_n)
|
||||
return mx.where(
|
||||
x > 0,
|
||||
alpha_p * mx.square(x) + beta * x,
|
||||
(mx.expm1(mx.minimum(x, eps)) - x) * alpha_n + beta * x,
|
||||
)
|
||||
|
||||
|
||||
class XieLU(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
alpha_p_init=0.8,
|
||||
alpha_n_init=0.8,
|
||||
beta=0.5,
|
||||
eps=-1e-6,
|
||||
):
|
||||
super().__init__()
|
||||
alpha_p_tensor = mx.array(alpha_p_init)
|
||||
alpha_n_tensor = mx.array(alpha_n_init - beta)
|
||||
self.alpha_p = mx.log(mx.exp(alpha_p_tensor) - 1)
|
||||
self.alpha_n = mx.log(mx.exp(alpha_n_tensor) - 1)
|
||||
|
||||
self.beta = mx.array(beta)
|
||||
self.eps = mx.array(eps)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return xielu(x, self.alpha_p, self.alpha_n, self.beta, self.eps)
|
||||
@@ -0,0 +1,390 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ConcatenateKVCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,405 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
layer_types: List[str]
|
||||
vocab_size: int = 200192
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 6144
|
||||
moe_intermediate_size: int = 1024
|
||||
num_hidden_layers: int = 32
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 4
|
||||
head_dim: int = 64
|
||||
max_position_embeddings: int = 131072
|
||||
rms_norm_eps: float = 1e-5
|
||||
rope_theta: float = 10000
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
# MoE config
|
||||
num_experts: int = 128
|
||||
num_experts_per_tok: int = 8
|
||||
num_shared_experts: int = 1
|
||||
num_dense_layers: int = 2
|
||||
route_norm: bool = True
|
||||
route_scale: float = 2.826
|
||||
score_func: str = "sigmoid"
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
sliding_window: int = 2048
|
||||
mup_enabled: bool = True
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, is_local_attention: bool = False):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.is_local_attention = is_local_attention
|
||||
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
|
||||
if is_local_attention:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False, # traditional
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
else:
|
||||
self.rope = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(keys)
|
||||
|
||||
if self.is_local_attention and self.rope is not None:
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
gate = mx.sigmoid(self.gate_proj(x))
|
||||
output = output * gate
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
hidden_dim = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoERouter(nn.Module):
|
||||
"""Router module that wraps the gate for proper weight naming."""
|
||||
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.gate(x)
|
||||
|
||||
|
||||
class AfmoeMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts = args.num_experts
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.route_norm = args.route_norm
|
||||
self.route_scale = args.route_scale
|
||||
self.score_func = args.score_func
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
|
||||
self.router = MoERouter(args)
|
||||
|
||||
self.expert_bias = mx.zeros((args.num_experts,))
|
||||
|
||||
self.experts = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
if args.num_shared_experts > 0:
|
||||
shared_intermediate_size = (
|
||||
args.moe_intermediate_size * args.num_shared_experts
|
||||
)
|
||||
self.shared_experts = MLP(args, intermediate_size=shared_intermediate_size)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
|
||||
if self.score_func == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
|
||||
# Add expert bias for selection
|
||||
selection_scores = scores + self.expert_bias
|
||||
|
||||
# Group-based expert selection if n_group > 1
|
||||
if self.n_group > 1:
|
||||
selection_scores = mx.unflatten(
|
||||
selection_scores, axis=-1, shape=(self.n_group, -1)
|
||||
)
|
||||
group_scores = mx.topk(selection_scores, 2, axis=-1).sum(
|
||||
axis=-1, keepdims=True
|
||||
)
|
||||
k = self.n_group - self.topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
selection_scores = mx.put_along_axis(
|
||||
selection_scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
selection_scores = mx.flatten(selection_scores, -2, -1)
|
||||
|
||||
# Select top-k experts
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.argpartition(-selection_scores, kth=k - 1, axis=-1)[..., :k]
|
||||
|
||||
selected_scores = mx.take_along_axis(scores, inds, axis=-1)
|
||||
|
||||
if self.route_norm and self.num_experts_per_tok > 1:
|
||||
denominator = selected_scores.sum(axis=-1, keepdims=True)
|
||||
selected_scores = selected_scores / denominator
|
||||
|
||||
selected_scores = selected_scores * self.route_scale
|
||||
|
||||
y = self.experts(x, inds)
|
||||
y = (y * selected_scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
if self.args.num_shared_experts > 0:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.self_attn = Attention(args, is_local_attention=use_sliding)
|
||||
|
||||
if layer_idx < args.num_dense_layers:
|
||||
self.mlp = MLP(args)
|
||||
else:
|
||||
self.mlp = AfmoeMoE(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.pre_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_mlp_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
r = self.post_attention_layernorm(r)
|
||||
h = x + r
|
||||
|
||||
r = self.mlp(self.pre_mlp_layernorm(h))
|
||||
r = self.post_mlp_layernorm(r)
|
||||
return h + r
|
||||
|
||||
|
||||
class AfmoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
self.mup_enabled = args.mup_enabled
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(
|
||||
args=args, layer_idx=idx, use_sliding=layer_type == "sliding_attention"
|
||||
)
|
||||
for idx, layer_type in enumerate(self.layer_types)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for idx, layer in enumerate(self.layers):
|
||||
if layer.use_sliding:
|
||||
self.swa_idx = idx
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if self.mup_enabled:
|
||||
h = h * math.sqrt(self.hidden_size)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
swa_mask = None
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = AfmoeModel(args)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove unused precomputed rotary freqs
|
||||
weights = {k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
# Stack experts weights for SwitchGLU
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if l < self.args.num_dense_layers:
|
||||
continue
|
||||
prefix = f"model.layers.{l}"
|
||||
for n in ["up_proj", "down_proj", "gate_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{n}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{n}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.experts.{n}.{k}"] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if "router.gate" in path:
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,195 @@
|
||||
# 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 .activations import XieLU
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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):
|
||||
for k, v in weights.items():
|
||||
if k.endswith("alpha_p") or k.endswith("alpha_n"):
|
||||
weights[k] = v.squeeze()
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,251 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ArraysCache, CacheList, KVCache, 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(swiglu(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 = ArraysCache(size=2)
|
||||
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
|
||||
@@ -0,0 +1,401 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@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 aggregate_expert_outputs(expert_outputs, scores):
|
||||
return (
|
||||
(expert_outputs * scores[..., None]).sum(axis=-2).astype(expert_outputs.dtype)
|
||||
)
|
||||
|
||||
|
||||
class BailingMoeMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.intermediate_size = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
self.intermediate_size, args.hidden_size, bias=args.use_bias
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class BailingMoeAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
if (rope_dim := args.rotary_dim) is None:
|
||||
rope_dim = int(self.head_dim * args.partial_rotary_factor)
|
||||
self.rope = initialize_rope(
|
||||
rope_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
|
||||
q_size = self.num_attention_heads * self.head_dim
|
||||
kv_size = self.num_key_value_heads * self.head_dim
|
||||
q, k, v = mx.split(qkv, [q_size, q_size + kv_size], axis=-1)
|
||||
|
||||
queries = q.reshape(B, L, self.num_attention_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = k.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = v.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
score_function,
|
||||
):
|
||||
|
||||
in_type = gates.dtype
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
orig_scores = scores
|
||||
if e_score_correction_bias is not None:
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0, scores.dtype), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(scores, kth=-k, axis=-1)[..., -k:]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores.astype(in_type)
|
||||
|
||||
|
||||
class BailingMoeGate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.enable_routed_scaling = args.moe_router_enable_routed_scaling
|
||||
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.expert_bias = (
|
||||
mx.zeros((args.num_experts,))
|
||||
if args.moe_router_enable_expert_bias
|
||||
else None
|
||||
)
|
||||
self.score_function = args.score_function
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
self.gate_proj(x),
|
||||
self.expert_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
self.score_function,
|
||||
)
|
||||
|
||||
|
||||
class BailingMoeSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
self.gate = BailingMoeGate(args)
|
||||
shared_dim = (
|
||||
args.moe_shared_expert_intermediate_size or args.moe_intermediate_size
|
||||
)
|
||||
self.shared_experts = (
|
||||
BailingMoeMLP(
|
||||
args=args,
|
||||
intermediate_size=shared_dim * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts > 0 and args.moe_router_enable_shared_expert
|
||||
else None
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = aggregate_expert_outputs(out, topk_weight)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class BailingMoeDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.attention = BailingMoeAttention(args)
|
||||
|
||||
self.mlp = (
|
||||
BailingMoeSparseMoeBlock(args)
|
||||
if (
|
||||
args.num_experts is not None and layer_idx >= args.first_k_dense_replace
|
||||
)
|
||||
else BailingMoeMLP(args)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class BailingMoeModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
BailingMoeDecoderLayer(args, layer_idx=i)
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
h = self.word_embeddings(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.norm_head = args.norm_head
|
||||
self.model_type = args.model_type
|
||||
self.model = BailingMoeModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.word_embeddings.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if self.norm_head:
|
||||
w = weights["lm_head.weight"]
|
||||
dtype = w.dtype
|
||||
weight_norm = (
|
||||
mx.linalg.norm(w.astype(mx.float32), axis=0, keepdims=True) + 1e-7
|
||||
)
|
||||
weights["lm_head.weight"] = (w / weight_norm).astype(dtype)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
|
||||
if l >= self.args.first_k_dense_replace:
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(
|
||||
to_join
|
||||
)
|
||||
|
||||
if f"{prefix}.mlp.gate.weight" in weights:
|
||||
gate_weight = weights.pop(f"{prefix}.mlp.gate.weight")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.weight"] = gate_weight
|
||||
|
||||
if f"{prefix}.mlp.gate.bias" in weights:
|
||||
gate_bias = weights.pop(f"{prefix}.mlp.gate.bias")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.bias"] = gate_bias
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate.gate_proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,595 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
max_position_embeddings: int
|
||||
moe_intermediate_size: int
|
||||
num_experts: int
|
||||
num_shared_experts: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_experts_per_tok: int
|
||||
num_hidden_layers: int
|
||||
num_key_value_heads: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
vocab_size: int
|
||||
first_k_dense_replace: int
|
||||
layer_group_size: int
|
||||
group_norm_size: int
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
rope_traditional: bool = False
|
||||
use_bias: bool = False
|
||||
use_qkv_bias: bool = False
|
||||
norm_head: bool = False
|
||||
norm_softmax: bool = False
|
||||
use_qk_norm: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
moe_router_enable_expert_bias: bool = False
|
||||
moe_router_enable_routed_scaling: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
score_function: str = "softmax"
|
||||
n_group: int = 1
|
||||
topk_group: int = 4
|
||||
use_rmsnorm: bool = True
|
||||
moe_shared_expert_intermediate_size: Optional[int] = None
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
head_dim: Optional[int] = None
|
||||
|
||||
|
||||
def recurrent_gla(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
g: mx.array,
|
||||
scale: float,
|
||||
h: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
"""
|
||||
Recurrence per (b, h):
|
||||
h_t = h_{t-1} * exp(g_t)
|
||||
h_t = h_t + k_t^T @ v_t
|
||||
y_t = (q_t @ h_t) * scale
|
||||
Returns y with shape [B, H, T, Dv].
|
||||
"""
|
||||
B, Hq, L, K = q.shape
|
||||
Hv = k.shape[1]
|
||||
V = v.shape[-1]
|
||||
|
||||
outputs = []
|
||||
exp_g = mx.exp(g)[:, None, None].astype(q.dtype)
|
||||
q = q * scale
|
||||
for t in range(L):
|
||||
q_t = q[:, :, t : t + 1]
|
||||
k_t = k[:, :, t : t + 1]
|
||||
v_t = v[:, :, t : t + 1]
|
||||
h_up = k_t.transpose(0, 1, 3, 2) @ v_t
|
||||
if h is not None:
|
||||
h = h * exp_g + h_up
|
||||
else:
|
||||
h = h_up
|
||||
o_t = q_t @ h
|
||||
outputs.append(o_t)
|
||||
|
||||
return mx.concatenate(outputs, axis=2), h
|
||||
|
||||
|
||||
class GroupRMSNorm(nn.Module):
|
||||
def __init__(self, dims: int, eps: float = 1e-5, groups: int = 1):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((dims,))
|
||||
self.groups = groups
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
shape = x.shape
|
||||
x = mx.unflatten(x, axis=-1, shape=(self.groups, -1))
|
||||
x = mx.fast.rms_norm(x, weight=None, eps=self.eps)
|
||||
return self.weight * mx.flatten(x, -2)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.intermediate_size = (
|
||||
intermediate_size
|
||||
if intermediate_size is not None
|
||||
else args.intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
self.intermediate_size, args.hidden_size, bias=args.use_bias
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, self.intermediate_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim or args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
|
||||
q_size = self.num_attention_heads * self.head_dim
|
||||
kv_size = self.num_key_value_heads * self.head_dim
|
||||
q, k, v = mx.split(qkv, [q_size, q_size + kv_size], axis=-1)
|
||||
|
||||
queries = q.reshape(B, L, self.num_attention_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = k.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = v.reshape(B, L, self.num_key_value_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
class LinearAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
self.use_qk_norm = args.use_qk_norm
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_attention_heads
|
||||
self.head_dim = args.hidden_size // self.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.num_key_value_groups = self.num_attention_heads // self.num_key_value_heads
|
||||
assert self.num_key_value_groups == 1, "Grouped linear not yet supported."
|
||||
|
||||
self.query_key_value = nn.Linear(
|
||||
args.hidden_size,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads) * self.head_dim,
|
||||
bias=args.use_qkv_bias,
|
||||
)
|
||||
|
||||
self.dense = nn.Linear(
|
||||
self.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
|
||||
self.g_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.g_norm = GroupRMSNorm(
|
||||
args.num_attention_heads * self.head_dim,
|
||||
eps=args.rms_norm_eps,
|
||||
groups=args.group_norm_size,
|
||||
)
|
||||
|
||||
if args.use_qk_norm:
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
args.rope_theta,
|
||||
traditional=args.rope_traditional,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
self._slope = self._get_slopes()
|
||||
|
||||
def _get_slopes(self) -> mx.array:
|
||||
n = self.num_attention_heads
|
||||
|
||||
def power_of_2_slopes(n):
|
||||
return [2 ** (-(2 ** -(math.log2(n) - 3)) * (i + 1)) for i in range(n)]
|
||||
|
||||
if math.log2(n).is_integer():
|
||||
slopes = power_of_2_slopes(n)
|
||||
else:
|
||||
p = 2 ** math.floor(math.log2(n))
|
||||
slopes = power_of_2_slopes(p) + power_of_2_slopes(2 * p)[::2][: n - p]
|
||||
|
||||
slopes = mx.array(slopes, dtype=mx.float32)
|
||||
denom = max(1, self.num_hidden_layers - 1)
|
||||
layer_pos = max(0, self.layer_idx - 1)
|
||||
layer_factor = 1 - (layer_pos / denom) + 1e-5
|
||||
return -slopes * layer_factor
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qkv = self.query_key_value(x)
|
||||
qkv_mix = qkv.reshape(
|
||||
B,
|
||||
L,
|
||||
(self.num_attention_heads + 2 * self.num_key_value_heads),
|
||||
self.head_dim,
|
||||
)
|
||||
q, k, v = mx.split(
|
||||
qkv_mix,
|
||||
[
|
||||
self.num_attention_heads,
|
||||
self.num_attention_heads + self.num_key_value_heads,
|
||||
],
|
||||
axis=2,
|
||||
)
|
||||
|
||||
queries = q.transpose(0, 2, 1, 3)
|
||||
keys = k.transpose(0, 2, 1, 3)
|
||||
values = v.transpose(0, 2, 1, 3)
|
||||
|
||||
if self.use_qk_norm:
|
||||
queries = self.query_layernorm(queries)
|
||||
keys = self.key_layernorm(keys)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
if cache is None:
|
||||
cache = [None]
|
||||
output, cache[0] = recurrent_gla(
|
||||
q=queries,
|
||||
k=keys,
|
||||
v=values,
|
||||
g=self._slope,
|
||||
scale=self.scale,
|
||||
h=cache[0],
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
output = self.g_norm(output) * mx.sigmoid(self.g_proj(x))
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
def group_expert_select(
|
||||
gates: mx.array,
|
||||
e_score_correction_bias: mx.array,
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
norm_topk_prob: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
in_type = gates.dtype
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
else:
|
||||
scores = mx.softmax(gates.astype(mx.float32), axis=-1)
|
||||
orig_scores = scores
|
||||
if e_score_correction_bias is not None:
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores.astype(in_type)
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.enable_routed_scaling = args.moe_router_enable_routed_scaling
|
||||
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.expert_bias = (
|
||||
mx.zeros((args.num_experts,))
|
||||
if args.moe_router_enable_expert_bias
|
||||
else None
|
||||
)
|
||||
self.score_function = args.score_function
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return group_expert_select(
|
||||
self.gate_proj(x),
|
||||
self.expert_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
self.score_function,
|
||||
)
|
||||
|
||||
|
||||
class SparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
bias=args.use_bias,
|
||||
)
|
||||
self.gate = Gate(args)
|
||||
shared_dim = (
|
||||
args.moe_shared_expert_intermediate_size or args.moe_intermediate_size
|
||||
)
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args=args,
|
||||
intermediate_size=shared_dim * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts > 0 and args.moe_router_enable_shared_expert
|
||||
else None
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = (out * topk_weight[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_global = (
|
||||
(layer_idx + 1) % args.layer_group_size == 0
|
||||
or layer_idx
|
||||
>= args.num_hidden_layers // args.layer_group_size * args.layer_group_size
|
||||
)
|
||||
|
||||
if self.is_global:
|
||||
self.attention = Attention(args)
|
||||
else:
|
||||
self.attention = LinearAttention(args, layer_idx=layer_idx)
|
||||
|
||||
self.mlp = (
|
||||
SparseMoeBlock(args)
|
||||
if (
|
||||
args.num_experts is not None and layer_idx >= args.first_k_dense_replace
|
||||
)
|
||||
else MLP(args)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
offset: int = 0,
|
||||
) -> mx.array:
|
||||
if self.is_global:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
r = self.attention(self.input_layernorm(x), mask, cache, offset=offset)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gla_idx = 0
|
||||
self.attn_idx = args.layer_group_size - 1
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.word_embeddings(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
offset = 0
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
gla_mask = create_ssm_mask(h, cache[self.gla_idx])
|
||||
if cache[self.attn_idx] is not None:
|
||||
offset = cache[self.attn_idx].offset
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_global else gla_mask
|
||||
h = layer(h, mask, c, offset=offset)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.norm_head = args.norm_head
|
||||
self.model_type = args.model_type
|
||||
self.model = LanguageModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.word_embeddings.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if self.norm_head:
|
||||
w = weights["lm_head.weight"]
|
||||
dtype = w.dtype
|
||||
weight_norm = (
|
||||
mx.linalg.norm(w.astype(mx.float32), axis=0, keepdims=True) + 1e-7
|
||||
)
|
||||
weights["lm_head.weight"] = (w / weight_norm).astype(dtype)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Handle MoE layers
|
||||
if l >= self.args.first_k_dense_replace:
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(
|
||||
to_join
|
||||
)
|
||||
|
||||
if f"{prefix}.mlp.gate.weight" in weights:
|
||||
gate_weight = weights.pop(f"{prefix}.mlp.gate.weight")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.weight"] = gate_weight
|
||||
|
||||
if f"{prefix}.mlp.gate.bias" in weights:
|
||||
gate_bias = weights.pop(f"{prefix}.mlp.gate.bias")
|
||||
weights[f"{prefix}.mlp.gate.gate_proj.bias"] = gate_bias
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate.gate_proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for l in self.layers:
|
||||
if l.is_global:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(ArraysCache(size=1))
|
||||
return caches
|
||||
+46
-30
@@ -7,8 +7,6 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
from mlx.utils import tree_map
|
||||
|
||||
from .cache import QuantizedKVCache
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseModelArgs:
|
||||
@@ -27,40 +25,42 @@ def create_causal_mask(
|
||||
N: int,
|
||||
offset: int = 0,
|
||||
window_size: Optional[int] = None,
|
||||
lengths: Optional[mx.array] = None,
|
||||
right_padding: Optional[mx.array] = None,
|
||||
left_padding: Optional[mx.array] = None,
|
||||
):
|
||||
rinds = mx.arange(offset + N)
|
||||
linds = mx.arange(offset, offset + N) if offset else rinds
|
||||
linds = linds[:, None]
|
||||
rinds = rinds[None]
|
||||
mask = linds < rinds
|
||||
mask = linds >= rinds
|
||||
if window_size is not None:
|
||||
mask = mask | (linds > rinds + window_size)
|
||||
if lengths is not None:
|
||||
lengths = lengths[:, None, None, None]
|
||||
mask = mask | (rinds >= lengths)
|
||||
return mask * -1e9
|
||||
|
||||
|
||||
def create_attention_mask(h: mx.array, cache: Optional[Any] = None):
|
||||
T = h.shape[1]
|
||||
if T > 1:
|
||||
window_size = None
|
||||
offset = 0
|
||||
if cache is not None and cache[0] is not None:
|
||||
c = cache[0]
|
||||
if hasattr(c, "max_size"):
|
||||
offset = min(c.max_size, c.offset)
|
||||
window_size = c.max_size
|
||||
else:
|
||||
offset = c.offset
|
||||
mask = create_causal_mask(T, offset, window_size=window_size)
|
||||
mask = mask.astype(h.dtype)
|
||||
else:
|
||||
mask = 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)
|
||||
return mask
|
||||
|
||||
|
||||
def create_attention_mask(
|
||||
h, cache=None, window_size: Optional[int] = 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
|
||||
|
||||
|
||||
def quantized_scaled_dot_product_attention(
|
||||
queries: mx.array,
|
||||
q_keys: tuple[mx.array, mx.array, mx.array],
|
||||
@@ -85,7 +85,15 @@ def quantized_scaled_dot_product_attention(
|
||||
queries, *q_keys, transpose=True, group_size=group_size, bits=bits
|
||||
)
|
||||
if mask is not None:
|
||||
scores += mask
|
||||
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 = mx.softmax(scores, axis=-1, precise=True)
|
||||
out = mx.quantized_matmul(
|
||||
scores, *q_values, transpose=False, group_size=group_size, bits=bits
|
||||
@@ -104,8 +112,11 @@ def scaled_dot_product_attention(
|
||||
cache,
|
||||
scale: float,
|
||||
mask: Optional[mx.array],
|
||||
sinks: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
if isinstance(cache, QuantizedKVCache):
|
||||
if hasattr(cache, "bits"):
|
||||
if sinks is not None:
|
||||
raise ValueError("Quantized SDPA does not support attention sinks.")
|
||||
return quantized_scaled_dot_product_attention(
|
||||
queries,
|
||||
keys,
|
||||
@@ -117,5 +128,10 @@ def scaled_dot_product_attention(
|
||||
)
|
||||
else:
|
||||
return mx.fast.scaled_dot_product_attention(
|
||||
queries, keys, values, scale=scale, mask=mask
|
||||
queries,
|
||||
keys,
|
||||
values,
|
||||
scale=scale,
|
||||
mask=mask,
|
||||
sinks=sinks,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# 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
|
||||
@@ -0,0 +1,208 @@
|
||||
# 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
|
||||
+1346
-21
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -109,7 +110,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -155,17 +156,15 @@ class CohereModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -182,10 +181,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = out * self.model.args.logit_scale
|
||||
return out
|
||||
|
||||
+28
-16
@@ -6,6 +6,7 @@ from typing import Optional, Tuple
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
|
||||
@@ -83,15 +84,17 @@ class Attention(nn.Module):
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
if self.use_sliding_window and mask is not None:
|
||||
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
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
queries.astype(sdpa_type),
|
||||
keys,
|
||||
values,
|
||||
cache=cache,
|
||||
scale=self.scale,
|
||||
mask=mask,
|
||||
).astype(queries.dtype)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
@@ -104,7 +107,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -126,9 +129,11 @@ class TransformerBlock(nn.Module):
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Tuple[mx.array, mx.array]] = None,
|
||||
) -> mx.array:
|
||||
|
||||
h = self.input_layernorm(x)
|
||||
attn_h = self.self_attn(h, mask, cache)
|
||||
ff_h = self.mlp(h)
|
||||
|
||||
return attn_h + ff_h + x
|
||||
|
||||
|
||||
@@ -139,6 +144,7 @@ class CohereModel(nn.Module):
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
assert self.vocab_size > 0
|
||||
self.window_size = args.sliding_window
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args, layer_idx=i)
|
||||
@@ -151,18 +157,25 @@ 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)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
j = self.args.sliding_window_pattern
|
||||
full_mask = create_attention_mask(h, cache[j - 1])
|
||||
swa_mask = create_attention_mask(h, cache[0], window_size=self.window_size)
|
||||
|
||||
for i, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
is_global = (
|
||||
i % self.args.sliding_window_pattern
|
||||
== self.args.sliding_window_pattern - 1
|
||||
)
|
||||
|
||||
mask = full_mask if is_global else swa_mask
|
||||
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
@@ -178,10 +191,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = out * self.model.args.logit_scale
|
||||
return out
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import numpy as np
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -105,10 +106,9 @@ 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 = self.act_fn(self.w1(x)) * self.v1(x)
|
||||
current_hidden_states = swiglu(self.w1(x), self.v1(x))
|
||||
current_hidden_states = self.w2(current_hidden_states)
|
||||
return current_hidden_states
|
||||
|
||||
@@ -197,17 +197,15 @@ class DBRX(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.wte(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.blocks)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.blocks, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -225,10 +223,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -118,10 +119,9 @@ 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(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
@@ -211,15 +211,14 @@ class DeepseekModel(nn.Module):
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -238,9 +237,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -148,7 +151,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)
|
||||
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
|
||||
)
|
||||
@@ -158,7 +161,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)
|
||||
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
|
||||
@@ -258,7 +261,7 @@ class DeepseekV2MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -282,12 +285,12 @@ class MoEGate(nn.Module):
|
||||
if self.topk_method == "group_limited_greedy":
|
||||
bsz, seq_len = x.shape[:2]
|
||||
scores = scores.reshape(bsz, seq_len, self.n_group, -1)
|
||||
group_scores = scores.max(axis=-1)
|
||||
group_scores = scores.max(axis=-1, keepdims=True)
|
||||
k = self.n_group - self.topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-1)[..., :k]
|
||||
batch_idx = mx.expand_dims(mx.arange(bsz), (1, 2))
|
||||
seq_idx = mx.expand_dims(mx.arange(seq_len), (0, 2))
|
||||
scores[batch_idx, seq_idx, group_idx] = 0.0
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, group_idx, mx.array(0.0, scores.dtype), axis=-2
|
||||
)
|
||||
scores = scores.reshape(bsz, seq_len, -1)
|
||||
|
||||
k = self.top_k
|
||||
@@ -314,13 +317,21 @@ class DeepseekV2MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -355,7 +366,7 @@ class DeepseekV2DecoderLayer(nn.Module):
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV2Model(nn.Module):
|
||||
class DeepseekV2Model(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
@@ -370,18 +381,32 @@ class DeepseekV2Model(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)
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -398,9 +423,8 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, mask)
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
@@ -416,6 +440,62 @@ class Model(nn.Module):
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.kv_b_proj = shard_linear(
|
||||
layer.self_attn.kv_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV2MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
# Copyright © 2024 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 mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .mla import MultiLinear
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "deepseek_v3"
|
||||
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
|
||||
|
||||
|
||||
class DeepseekV3Attention(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
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
else:
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=1e-6)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6)
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.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
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q = self.q_proj(x)
|
||||
else:
|
||||
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DeepseekV3MLP(nn.Module):
|
||||
def __init__(
|
||||
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV3MoE(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 = DeepseekV3MLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DeepseekV3DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = DeepseekV3Attention(config)
|
||||
self.mlp = (
|
||||
DeepseekV3MoE(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 DeepseekV3MLP(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 DeepseekV3Model(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 = [
|
||||
DeepseekV3DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0], return_array=True)
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = 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)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-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)
|
||||
|
||||
# Remap for int4
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if k.endswith("weight_shape"):
|
||||
base = k.replace("weight_shape", "")
|
||||
new_weights[base + "weight"] = weights[base + "weight_packed"].view(
|
||||
mx.uint32
|
||||
)
|
||||
s = weights[base + "weight_scale"]
|
||||
new_weights[base + "scales"] = s
|
||||
new_weights[base + "biases"] = -8 * s
|
||||
elif not (k.endswith("weight_scale") or k.endswith("weight_packed")):
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Dequantize fp8
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.model.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV3MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,654 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
from .mla import MultiLinear
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "deepseek_v32"
|
||||
vocab_size: int = 102400
|
||||
hidden_size: int = 4096
|
||||
index_head_dim: int = 128
|
||||
index_n_heads: int = 64
|
||||
index_topk: int = 2048
|
||||
intermediate_size: int = 11008
|
||||
moe_intermediate_size: int = 1407
|
||||
num_hidden_layers: int = 30
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 32
|
||||
n_shared_experts: Optional[int] = None
|
||||
n_routed_experts: Optional[int] = None
|
||||
routed_scaling_factor: float = 1.0
|
||||
kv_lora_rank: int = 512
|
||||
q_lora_rank: int = 1536
|
||||
qk_rope_head_dim: int = 64
|
||||
v_head_dim: int = 128
|
||||
qk_nope_head_dim: int = 128
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
num_experts_per_tok: int = 1
|
||||
moe_layer_freq: int = 1
|
||||
first_k_dense_replace: int = 0
|
||||
max_position_embeddings: int = 2048
|
||||
rms_norm_eps: float = 1e-6
|
||||
rope_theta: float = 10000.0
|
||||
rope_scaling: Dict = None
|
||||
attention_bias: bool = False
|
||||
|
||||
|
||||
class Indexer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.dim = args.hidden_size
|
||||
self.n_heads = args.index_n_heads
|
||||
self.head_dim = args.index_head_dim
|
||||
self.rope_head_dim = args.qk_rope_head_dim
|
||||
self.index_topk = args.index_topk
|
||||
self.q_lora_rank = args.q_lora_rank
|
||||
self.wq_b = nn.Linear(
|
||||
self.q_lora_rank, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.wk = nn.Linear(self.dim, self.head_dim, bias=False)
|
||||
self.k_norm = nn.LayerNorm(self.head_dim)
|
||||
self.weights_proj = nn.Linear(self.dim, self.n_heads, bias=False)
|
||||
self.softmax_scale = self.head_dim**-0.5
|
||||
self.rope = initialize_rope(
|
||||
dims=args.qk_rope_head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
qr: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
# Computes top_k indices for attention
|
||||
b, s, _ = x.shape
|
||||
q = self.wq_b(qr)
|
||||
q = q.reshape(b, s, self.n_heads, self.head_dim).swapaxes(1, 2)
|
||||
k = self.wk(x)
|
||||
k = self.k_norm(k)
|
||||
k = mx.reshape(k, (b, 1, s, self.head_dim))
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
|
||||
q = self.rope(q, offset=offset)
|
||||
k = self.rope(k, offset=offset)
|
||||
|
||||
if cache is not None:
|
||||
k, _ = cache.update_and_fetch(k, mx.zeros([b, 1, s, 0]))
|
||||
if k.shape[2] <= self.index_topk:
|
||||
return None
|
||||
scores = q @ k.swapaxes(-1, -2)
|
||||
scores = mx.maximum(scores, 0)
|
||||
weights = self.weights_proj(x) * (self.n_heads**-0.5 * self.softmax_scale)
|
||||
weights = weights.swapaxes(-1, -2)[..., None]
|
||||
scores = scores * weights
|
||||
scores = scores.sum(axis=1, keepdims=True)
|
||||
if mask is not None:
|
||||
scores = mx.where(mask, scores, -float("inf"))
|
||||
return mx.argpartition(scores, kth=-self.index_topk, axis=-1)[
|
||||
..., -self.index_topk :
|
||||
]
|
||||
|
||||
|
||||
class DeepseekV32Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_heads = config.num_attention_heads
|
||||
self.max_position_embeddings = config.max_position_embeddings
|
||||
self.rope_theta = config.rope_theta
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.qk_rope_head_dim = config.qk_rope_head_dim
|
||||
self.kv_lora_rank = config.kv_lora_rank
|
||||
self.v_head_dim = config.v_head_dim
|
||||
self.qk_nope_head_dim = config.qk_nope_head_dim
|
||||
self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
|
||||
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=1e-6)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6)
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.indexer = Indexer(config)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
qr = self.q_a_layernorm(self.q_a_proj(x))
|
||||
q = self.q_b_proj(qr)
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
offset = cache[0].offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache[0].update_and_fetch(kv_latent, k_pe)
|
||||
else:
|
||||
cache = [None] * 2
|
||||
|
||||
topk_indices = self.indexer(x, qr, mask, cache=cache[1])
|
||||
if topk_indices is not None:
|
||||
if L == 1:
|
||||
idx = topk_indices[:, :, 0, :, None]
|
||||
kv_latent = mx.take_along_axis(
|
||||
kv_latent,
|
||||
mx.broadcast_to(idx, idx.shape[:-1] + (kv_latent.shape[-1],)),
|
||||
axis=2,
|
||||
)
|
||||
k_pe = mx.take_along_axis(
|
||||
k_pe,
|
||||
mx.broadcast_to(idx, idx.shape[:-1] + (k_pe.shape[-1],)),
|
||||
axis=2,
|
||||
)
|
||||
if mask is not None:
|
||||
mask = mx.take_along_axis(mask, topk_indices, axis=-1)
|
||||
else:
|
||||
shape = list(topk_indices.shape)
|
||||
shape[-1] = kv_latent.shape[2]
|
||||
sparse_mask = mx.zeros(shape, dtype=mx.bool_)
|
||||
sparse_mask = mx.put_along_axis(
|
||||
sparse_mask, topk_indices, mx.array(True), axis=-1
|
||||
)
|
||||
if mask is not None:
|
||||
sparse_mask = sparse_mask & mask
|
||||
mask = sparse_mask
|
||||
# Ensure the indexer cache is evaluated even if the topk_indices are unused
|
||||
# to keep the graph from getting too large
|
||||
if cache is not None and cache[0] is not None:
|
||||
cache[0].keys = mx.depends(cache[0].keys, (cache[1].keys, cache[1].values))
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DeepseekV32MLP(nn.Module):
|
||||
def __init__(
|
||||
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV32MoE(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.num_experts_per_tok = config.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
if config.n_shared_experts is not None:
|
||||
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
|
||||
self.shared_experts = DeepseekV32MLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class DeepseekV32DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = DeepseekV32Attention(config)
|
||||
self.mlp = (
|
||||
DeepseekV32MoE(config)
|
||||
if (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
and layer_idx % config.moe_layer_freq == 0
|
||||
)
|
||||
else DeepseekV32MLP(config)
|
||||
)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class DeepseekV32Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.layers = [
|
||||
DeepseekV32DecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.start_idx = 0
|
||||
self.end_idx = len(self.layers)
|
||||
self.num_layers = self.end_idx
|
||||
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.pipeline_rank = 0
|
||||
self.pipeline_size = 1
|
||||
|
||||
def pipeline(self, group):
|
||||
# Split layers in reverse so rank=0 gets the last layers and
|
||||
# rank=pipeline_size-1 gets the first
|
||||
self.pipeline_rank = group.rank()
|
||||
self.pipeline_size = group.size()
|
||||
layers_per_rank = len(self.layers) // self.pipeline_size
|
||||
extra = len(self.layers) - layers_per_rank * self.pipeline_size
|
||||
if self.pipeline_rank < extra:
|
||||
layers_per_rank += 1
|
||||
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
|
||||
self.end_idx = self.start_idx + layers_per_rank
|
||||
self.layers = self.layers[: self.end_idx]
|
||||
self.layers[: self.start_idx] = [None] * self.start_idx
|
||||
self.num_layers = len(self.layers) - self.start_idx
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * self.num_layers
|
||||
mask = create_attention_mask(
|
||||
h, cache[0][0] if cache[0] else None, return_array=True
|
||||
)
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for i in range(self.num_layers):
|
||||
h = self.layers[self.start_idx + i](h, mask, cache[i])
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1][0].keys = mx.depends(cache[-1][0].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = DeepseekV32Model(config)
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Remove multi-token prediction layers
|
||||
mpt_layer = self.args.num_hidden_layers
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
parts = k.split(".")
|
||||
if len(parts) >= 3 and parts[1] == "layers" and int(parts[2]) >= mpt_layer:
|
||||
continue
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
pad_side = (-n) % bs
|
||||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||||
weight = weight.reshape(
|
||||
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
|
||||
)
|
||||
weight = (weight * scale_inv[:, None, :, None]).reshape(
|
||||
m + pad_bottom, n + pad_side
|
||||
)
|
||||
return weight[:m, :n].astype(dtype)
|
||||
|
||||
# Dequantize
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if "weight_scale_inv" in k:
|
||||
scale_inv = v
|
||||
wk = k.replace("_scale_inv", "")
|
||||
weight = weights[wk]
|
||||
weight = dequant(weight, scale_inv)
|
||||
new_weights[wk] = weight
|
||||
elif k not in new_weights:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.model.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV32MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers[self.model.start_idx : self.model.end_idx]
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def make_cache(self):
|
||||
return [CacheList(KVCache(), KVCache()) for _ in self.layers]
|
||||
@@ -0,0 +1,316 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@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(swiglu(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
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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 .activations import swiglu
|
||||
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(swiglu(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
|
||||
@@ -0,0 +1,289 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@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(swiglu(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
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -91,7 +92,7 @@ class MLP(nn.Module):
|
||||
self.c_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.c_proj(nn.silu(self.c_fc_0(x)) * self.c_fc_1(x))
|
||||
return self.c_proj(swiglu(self.c_fc_0(x), self.c_fc_1(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -123,16 +124,15 @@ class ExaoneModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.wte(inputs)
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
@@ -151,10 +151,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.transformer.wte.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@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(swiglu(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
|
||||
@@ -0,0 +1,439 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
head_dim: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
num_shared_experts: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
sliding_window: int
|
||||
layer_types: List[str]
|
||||
is_moe_layer: List[bool]
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
routed_scaling_factor: float = 2.5
|
||||
norm_topk_prob: bool = True
|
||||
scoring_func: str = "sigmoid"
|
||||
topk_method: str = "noaux_tc"
|
||||
rope_theta: float = 1000000.0
|
||||
rope_scaling: Optional[dict] = None
|
||||
rope_parameters: Optional[dict] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / (denominator + 1e-20)
|
||||
|
||||
scores = scores * routed_scaling_factor
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.n_routed_experts = args.num_experts
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, args.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert args.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(args)
|
||||
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args,
|
||||
intermediate_size=args.moe_intermediate_size * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts is not None and args.num_shared_experts > 0
|
||||
else None
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.is_sliding_window = args.layer_types[layer_idx] == "sliding_attention"
|
||||
self.apply_rope_all_layers = "sliding_attention" not in args.layer_types
|
||||
self.use_rope = self.is_sliding_window or self.apply_rope_all_layers
|
||||
|
||||
if self.use_rope:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_norm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
if self.use_rope:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
elif self.use_rope:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
|
||||
self.self_attn = Attention(args, layer_idx)
|
||||
self.mlp = MoE(args) if args.is_moe_layer[layer_idx] else MLP(args)
|
||||
self.is_sliding_window = self.self_attn.is_sliding_window
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class ExaoneMoEModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [DecoderLayer(args, idx) for idx in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.swa_idx = None
|
||||
self.ga_idx = None
|
||||
for i, layer in enumerate(self.layers):
|
||||
if layer.is_sliding_window and self.swa_idx is None:
|
||||
self.swa_idx = i
|
||||
if not layer.is_sliding_window and self.ga_idx is None:
|
||||
self.ga_idx = i
|
||||
|
||||
self.window_size = args.sliding_window
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
global_mask = create_attention_mask(
|
||||
h, cache[self.ga_idx] if self.ga_idx is not None else cache[0]
|
||||
)
|
||||
swa_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.swa_idx] if self.swa_idx is not None else cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.is_sliding_window else global_mask
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = ExaoneMoEModel(args)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
new_weights = {k: v for k, v in weights.items() if not k.startswith("mtp.")}
|
||||
weights = new_weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if not self.args.is_moe_layer[l]:
|
||||
continue
|
||||
|
||||
prefix = f"model.layers.{l}"
|
||||
|
||||
bias_key = f"{prefix}.mlp.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{prefix}.mlp.gate.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
first_key = f"{prefix}.mlp.experts.0.{m}.{k}"
|
||||
last_key = (
|
||||
f"{prefix}.mlp.experts.{self.args.num_experts - 1}.{m}.{k}"
|
||||
)
|
||||
if first_key in weights and last_key in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.layers:
|
||||
if layer.is_sliding_window:
|
||||
caches.append(
|
||||
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
|
||||
)
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
if isinstance(layer.mlp, MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
if layer.mlp.shared_experts is not None:
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
@@ -0,0 +1,504 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, CacheList, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool = False
|
||||
attention_in_multiplier: float = 1.0
|
||||
attention_out_multiplier: float = 0.9375
|
||||
embedding_multiplier: float = 5.656854249492381
|
||||
head_dim: int = 64
|
||||
hidden_size: int = 1024
|
||||
initializer_range: float = 0.02
|
||||
intermediate_size: int = 2048
|
||||
key_multiplier: float = 0.390625
|
||||
lm_head_multiplier: float = 0.0390625
|
||||
mamba_chunk_size: int = 128
|
||||
mamba_conv_bias: bool = True
|
||||
mamba_d_conv: int = 4
|
||||
mamba_d_head: int = 64
|
||||
mamba_d_ssm: int = 1536
|
||||
mamba_d_state: int = 128
|
||||
mamba_expand: int = 2
|
||||
mamba_n_groups: int = 1
|
||||
mamba_n_heads: int = 24
|
||||
mamba_norm_before_gate: bool = False
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_rms_norm: bool = False
|
||||
mamba_use_mlp: bool = True
|
||||
max_position_embeddings: int = 131072
|
||||
mlp_bias: bool = False
|
||||
mlp_expansion_factor: int = 8
|
||||
mlp_multipliers: List[float] = field(
|
||||
default_factory=lambda: [0.8838834764831844, 0.5859375]
|
||||
)
|
||||
model_type: str = "falcon_h1"
|
||||
num_attention_heads: int = 8
|
||||
num_hidden_layers: int = 36
|
||||
num_key_value_heads: int = 2
|
||||
projectors_bias: bool = False
|
||||
rms_norm_eps: float = 1e-05
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[float] = None
|
||||
rope_theta: float = 100000000000.0
|
||||
ssm_in_multiplier: float = 1.25
|
||||
ssm_multipliers: List[float] = field(
|
||||
default_factory=lambda: [
|
||||
0.3535533905932738,
|
||||
0.25,
|
||||
0.3535533905932738,
|
||||
0.5,
|
||||
0.3535533905932738,
|
||||
]
|
||||
)
|
||||
ssm_out_multiplier: float = 0.23570226039551587
|
||||
vocab_size: int = 32784
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class FalconH1RMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size, eps=1e-6, n_groups=1, norm_before_gate=True):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((hidden_size,))
|
||||
self.variance_epsilon = eps
|
||||
self.n_groups = n_groups
|
||||
self.norm_before_gate = norm_before_gate
|
||||
|
||||
def __call__(self, hidden_states, gate=None):
|
||||
if not self.norm_before_gate and gate is not None:
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
|
||||
hidden_states = mx.fast.rms_norm(
|
||||
hidden_states, self.weight, self.variance_epsilon
|
||||
)
|
||||
|
||||
if self.norm_before_gate and gate is not None:
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
||||
def compute_mup_vector(args):
|
||||
intermediate_size = args.mamba_d_ssm
|
||||
groups_time_state_size = args.mamba_n_groups * args.mamba_d_state
|
||||
num_heads = args.mamba_n_heads
|
||||
sizes = [
|
||||
intermediate_size,
|
||||
intermediate_size,
|
||||
groups_time_state_size,
|
||||
groups_time_state_size,
|
||||
num_heads,
|
||||
]
|
||||
return mx.concatenate(
|
||||
[
|
||||
mx.broadcast_to(mx.array(m), (s,))
|
||||
for s, m in zip(sizes, args.ssm_multipliers)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class FalconH1Attention(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.head_dim, bias=args.attention_bias
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.head_dim, self.hidden_size, bias=args.attention_bias
|
||||
)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
args.rope_traditional,
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(self, x, mask=None, cache=None):
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.num_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, mask=mask, scale=self.scale, cache=cache
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class FalconH1Mixer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.num_heads = args.mamba_n_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_d_ssm
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
|
||||
self.layer_norm_epsilon = args.rms_norm_eps
|
||||
self.groups_time_state_size = args.mamba_n_groups * self.ssm_state_size
|
||||
|
||||
self.n_groups = args.mamba_n_groups
|
||||
self.head_dim = args.mamba_d_head
|
||||
self.chunk_size = args.mamba_chunk_size
|
||||
|
||||
self.time_step_limit = (0.0, float("inf"))
|
||||
self.time_step_min = 0.001
|
||||
self.time_step_max = 0.1
|
||||
|
||||
self.conv_dim = self.intermediate_size + 2 * self.n_groups * self.ssm_state_size
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.conv_dim,
|
||||
out_channels=self.conv_dim,
|
||||
bias=self.use_conv_bias,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.conv_dim,
|
||||
)
|
||||
|
||||
projection_size = self.intermediate_size + self.conv_dim + self.num_heads
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
projection_size,
|
||||
bias=args.mamba_proj_bias,
|
||||
)
|
||||
|
||||
self.dt_bias = mx.ones(self.num_heads)
|
||||
|
||||
A = mx.arange(1, self.num_heads + 1)
|
||||
self.A_log = mx.log(A)
|
||||
|
||||
self.mamba_rms_norm = args.mamba_rms_norm
|
||||
if self.mamba_rms_norm:
|
||||
self.norm = FalconH1RMSNormGated(
|
||||
self.intermediate_size,
|
||||
eps=self.layer_norm_epsilon,
|
||||
n_groups=self.n_groups,
|
||||
norm_before_gate=args.mamba_norm_before_gate,
|
||||
)
|
||||
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.projectors_bias
|
||||
)
|
||||
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
t = padded_input.shape[1]
|
||||
ends = mx.clip(cache.lengths, 0, t - n_keep)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(padded_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = padded_input[:, -n_keep:, :]
|
||||
else:
|
||||
padded_input = mx.pad(
|
||||
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
|
||||
)
|
||||
|
||||
conv_output = self.conv1d(padded_input)
|
||||
return nn.silu(conv_output)
|
||||
|
||||
def _ssm(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
hidden_states = hidden_states.reshape(
|
||||
batch_size, seq_len, self.num_heads, self.head_dim
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
lengths,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(self, input_states, cache=None, mask: Optional[mx.array] = None):
|
||||
projected_states = self.in_proj(input_states)
|
||||
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected_states,
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
|
||||
if cache:
|
||||
cache.advance(y.shape[1])
|
||||
|
||||
if self.mamba_rms_norm:
|
||||
y = self.norm(y, gate)
|
||||
else:
|
||||
y = swiglu(gate, y)
|
||||
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class FalconH1MLP(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = args.intermediate_size
|
||||
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x):
|
||||
y = swiglu(self.gate_proj(x), self.up_proj(x))
|
||||
y = self.down_proj(y)
|
||||
return y
|
||||
|
||||
|
||||
class FalconH1DecoderLayer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.feed_forward = FalconH1MLP(args)
|
||||
|
||||
head_dim = args.head_dim
|
||||
self.channels_attn = (
|
||||
args.num_attention_heads * head_dim
|
||||
+ 2 * args.num_key_value_heads * head_dim
|
||||
)
|
||||
|
||||
self.mamba = FalconH1Mixer(args=args)
|
||||
|
||||
self.self_attn = FalconH1Attention(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_ff_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
h: mx.array,
|
||||
cache,
|
||||
attn_mask: Optional[mx.array],
|
||||
mamba_mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
|
||||
residual = h
|
||||
h = self.input_layernorm(h)
|
||||
|
||||
mamba_h = self.mamba(input_states=h, cache=cache[0], mask=mamba_mask)
|
||||
|
||||
attn_h = self.self_attn(
|
||||
h,
|
||||
mask=attn_mask,
|
||||
cache=cache[1],
|
||||
)
|
||||
|
||||
h = residual + mamba_h + attn_h
|
||||
|
||||
residual = h
|
||||
h = self.pre_ff_layernorm(h)
|
||||
h = self.feed_forward(h)
|
||||
return residual + h
|
||||
|
||||
|
||||
class FalconH1Model(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(self.vocab_size, self.hidden_size)
|
||||
|
||||
self._mup_vector = compute_mup_vector(args)
|
||||
self.layers = [
|
||||
FalconH1DecoderLayer(args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.final_layernorm = nn.RMSNorm(self.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
h = h
|
||||
|
||||
if cache is None:
|
||||
cache = [(None, None) * len(self.layers)]
|
||||
|
||||
mamba_mask = create_ssm_mask(h, cache[0][0])
|
||||
attn_mask = create_attention_mask(h, cache[0][1])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(
|
||||
h,
|
||||
cache=c,
|
||||
attn_mask=attn_mask,
|
||||
mamba_mask=mamba_mask,
|
||||
)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = FalconH1Model(args=args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
hidden_states = self.model(inputs, cache=cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(hidden_states)
|
||||
return out * (self.args.lm_head_multiplier / self.args.embedding_multiplier)
|
||||
else:
|
||||
return self.lm_head(hidden_states)
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Check if needs sanitization
|
||||
c1d = weights["model.layers.0.mamba.conv1d.weight"]
|
||||
if c1d.shape[-1] <= c1d.shape[1]:
|
||||
return weights
|
||||
|
||||
sanitized_weights = {}
|
||||
args = self.args
|
||||
|
||||
for name, param in weights.items():
|
||||
# Fold-in multipliers
|
||||
if name.endswith("embed_tokens.weight"):
|
||||
param *= args.embedding_multiplier
|
||||
elif name.endswith("lm_head.weight"):
|
||||
param *= args.lm_head_multiplier
|
||||
elif name.endswith("q_proj.weight") or name.endswith("k_proj.weight"):
|
||||
param *= args.attention_in_multiplier
|
||||
elif name.endswith("key_proj.weight"):
|
||||
param *= args.attention_in_multiplier * args.key_multiplier
|
||||
elif name.endswith("o_proj.weight"):
|
||||
param *= args.attention_out_multiplier
|
||||
elif name.endswith("out_proj.weight"):
|
||||
param *= args.ssm_out_multiplier
|
||||
elif name.endswith("gate_proj.weight"):
|
||||
param *= args.mlp_multipliers[0]
|
||||
elif name.endswith("down_proj.weight"):
|
||||
param *= args.mlp_multipliers[1]
|
||||
elif name.endswith("in_proj.weight"):
|
||||
param *= (
|
||||
args.ssm_in_multiplier
|
||||
* self.model._mup_vector.astype(param.dtype)[:, None]
|
||||
)
|
||||
elif "conv1d.weight" in name:
|
||||
param = param.transpose(0, 2, 1)
|
||||
sanitized_weights[name] = param
|
||||
return sanitized_weights
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
CacheList(ArraysCache(size=2), KVCache())
|
||||
for _ in range(self.args.num_hidden_layers)
|
||||
]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,283 @@
|
||||
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))
|
||||
|
||||
|
||||
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);
|
||||
}}
|
||||
}} else {{
|
||||
y[dv_idx] = static_cast<InT>(0);
|
||||
}}
|
||||
// Increment data pointers to next time step
|
||||
q_ += Hk * Dk;
|
||||
k_ += Hk * Dk;
|
||||
v_ += Hv * Dv;
|
||||
y += Hv * Dv;
|
||||
{g_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<StT>(state[i]);
|
||||
}}
|
||||
"""
|
||||
inputs = ["q", "k", "v", "g", "beta", "state_in", "T"]
|
||||
if has_mask:
|
||||
inputs.append("mask")
|
||||
|
||||
suffix = ""
|
||||
if vectorized:
|
||||
suffix += "_vec"
|
||||
if has_mask:
|
||||
suffix += "_mask"
|
||||
|
||||
return mx.fast.metal_kernel(
|
||||
name=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:
|
||||
mask = mx.expand_dims(mask, axis=(1, 2, 3))
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y.astype(q.dtype), state
|
||||
|
||||
|
||||
def gated_delta_kernel(
|
||||
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
|
||||
state_type = state.dtype
|
||||
if g.ndim == 4:
|
||||
kernel = _gated_delta_kernel_vec
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_vec_masked
|
||||
inputs.append(mask)
|
||||
else:
|
||||
kernel = _gated_delta_kernel
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_masked
|
||||
inputs.append(mask)
|
||||
|
||||
return kernel(
|
||||
inputs=inputs,
|
||||
template=[
|
||||
("InT", input_type),
|
||||
("StT", state_type),
|
||||
("Dk", Dk),
|
||||
("Dv", Dv),
|
||||
("Hk", Hk),
|
||||
("Hv", Hv),
|
||||
],
|
||||
grid=(32, Dv, B * Hv),
|
||||
threadgroup=(32, 4, 1),
|
||||
output_shapes=[(B, T, Hv, Dv), state.shape],
|
||||
output_dtypes=[input_type, state_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=mx.float32)
|
||||
|
||||
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=mx.float32)
|
||||
|
||||
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)
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
@@ -138,18 +138,16 @@ class GemmaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
h = h * (self.args.hidden_size**0.5)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
@@ -166,10 +164,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
return out
|
||||
|
||||
|
||||
+10
-8
@@ -1,7 +1,7 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
@@ -94,7 +94,12 @@ class Attention(nn.Module):
|
||||
scores *= self.attn_logit_softcapping
|
||||
|
||||
if mask is not None:
|
||||
scores = scores + mask
|
||||
if mask.dtype == mx.bool_:
|
||||
scores = mx.where(
|
||||
mask, scores, mx.array(mx.finfo(scores.dtype).min, scores.dtype)
|
||||
)
|
||||
else:
|
||||
scores = scores + mask
|
||||
scores = mx.softmax(scores, precise=True, axis=-1)
|
||||
output = scores @ values
|
||||
if self.repeats > 1:
|
||||
@@ -160,18 +165,16 @@ class GemmaModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
h = h * (self.args.hidden_size**0.5)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
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)
|
||||
|
||||
@@ -189,10 +192,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
out = mx.tanh(out / self.final_logit_softcapping)
|
||||
out = out * self.final_logit_softcapping
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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 gemma3_text
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
text_config: dict
|
||||
vocab_size: int = 262208
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["vocab_size"] = self.vocab_size
|
||||
self.text_config["num_attention_heads"] = self.text_config.get(
|
||||
"num_attention_heads", 8
|
||||
)
|
||||
self.text_config["num_key_value_heads"] = self.text_config.get(
|
||||
"num_key_value_heads", 4
|
||||
)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = gemma3_text.Model(
|
||||
gemma3_text.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"]))
|
||||
lm_weights = self.language_model.sanitize(lm_weights)
|
||||
weights["language_model"] = tree_unflatten(list(lm_weights.items()))
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return self.language_model.make_cache()
|
||||
@@ -0,0 +1,257 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
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 .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int = 1152
|
||||
num_hidden_layers: int = 26
|
||||
intermediate_size: int = 6912
|
||||
num_attention_heads: int = 4
|
||||
head_dim: int = 256
|
||||
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_local_base_freq: float = 10_000.0
|
||||
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):
|
||||
def __init__(self, args: ModelArgs, 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.repeats = n_heads // n_kv_heads
|
||||
self.head_dim = head_dim = args.head_dim
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.scale = args.query_pre_attn_scalar**-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 = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
|
||||
self.is_sliding = (layer_idx + 1) % args.sliding_window_pattern != 0
|
||||
|
||||
if self.is_sliding:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_local_base_freq,
|
||||
traditional=False,
|
||||
)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = 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)
|
||||
|
||||
queries = self.q_norm(queries)
|
||||
keys = self.k_norm(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)
|
||||
|
||||
# Sliding window
|
||||
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 RMSNorm(nn.Module):
|
||||
def __init__(self, dims: int, eps: float = 1e-5):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((dims,))
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x):
|
||||
return mx.fast.rms_norm(x, 1.0 + self.weight, self.eps)
|
||||
|
||||
|
||||
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.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__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.self_attn = Attention(args, layer_idx)
|
||||
self.mlp = MLP(args.hidden_size, args.intermediate_size)
|
||||
self.input_layernorm = RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_feedforward_layernorm = RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = 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 = clip_residual(x, self.post_attention_layernorm(r))
|
||||
r = self.mlp(self.pre_feedforward_layernorm(h))
|
||||
out = clip_residual(h, self.post_feedforward_layernorm(r))
|
||||
return out
|
||||
|
||||
|
||||
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
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args, layer_idx=layer_idx)
|
||||
for layer_idx in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
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)
|
||||
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 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
|
||||
)
|
||||
mask = global_mask if is_global else sliding_window_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 = 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,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if "lm_head.weight" not in weights:
|
||||
self.tie_word_embeddings = True
|
||||
self.pop("lm_head")
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for i in range(self.args.num_hidden_layers):
|
||||
if (
|
||||
i % self.args.sliding_window_pattern
|
||||
== self.args.sliding_window_pattern - 1
|
||||
):
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
@@ -0,0 +1,613 @@
|
||||
# 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()
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import gemma4_text
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "gemma4"
|
||||
text_config: dict = None
|
||||
vocab_size: int = 262144
|
||||
|
||||
def __post_init__(self):
|
||||
if self.text_config is None:
|
||||
self.text_config = {}
|
||||
self.text_config["vocab_size"] = self.vocab_size
|
||||
self.text_config["num_attention_heads"] = self.text_config.get(
|
||||
"num_attention_heads", 8
|
||||
)
|
||||
self.text_config["num_key_value_heads"] = self.text_config.get(
|
||||
"num_key_value_heads", 1
|
||||
)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = gemma4_text.Model(
|
||||
gemma4_text.ModelArgs.from_dict(args.text_config)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs,
|
||||
cache=cache,
|
||||
input_embeddings=input_embeddings,
|
||||
per_layer_inputs=per_layer_inputs,
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
starts_w_model = k.startswith("model.")
|
||||
|
||||
k = k.removeprefix("model.")
|
||||
if k.startswith(
|
||||
(
|
||||
"vision_tower",
|
||||
"multi_modal_projector",
|
||||
"audio_tower",
|
||||
"embed_audio",
|
||||
"embed_vision",
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
if not starts_w_model:
|
||||
new_weights[k] = v
|
||||
continue
|
||||
|
||||
if k.startswith("language_model"):
|
||||
k = k.replace("language_model.", "language_model.model.")
|
||||
|
||||
new_weights[k] = v
|
||||
|
||||
return self.language_model.sanitize(new_weights)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
return self.language_model.quant_predicate
|
||||
|
||||
def make_cache(self):
|
||||
return self.language_model.make_cache()
|
||||
@@ -0,0 +1,688 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache, _BaseCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "gemma4_text"
|
||||
hidden_size: int = 1536
|
||||
num_hidden_layers: int = 35
|
||||
intermediate_size: int = 6144
|
||||
num_attention_heads: int = 8
|
||||
head_dim: int = 256
|
||||
global_head_dim: int = 512
|
||||
global_partial_rotary_factor: float = 0.25
|
||||
rms_norm_eps: float = 1e-6
|
||||
vocab_size: int = 262144
|
||||
vocab_size_per_layer_input: int = 262144
|
||||
num_key_value_heads: int = 1
|
||||
num_global_key_value_heads: Optional[int] = None
|
||||
num_kv_shared_layers: int = 20
|
||||
pad_token_id: int = 0
|
||||
hidden_size_per_layer_input: int = 256
|
||||
rope_traditional: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
rope_parameters: Optional[Dict] = None
|
||||
sliding_window: int = 512
|
||||
sliding_window_pattern: int = 5
|
||||
max_position_embeddings: int = 131072
|
||||
attention_k_eq_v: bool = False
|
||||
final_logit_softcapping: float = 30.0
|
||||
use_double_wide_mlp: bool = True
|
||||
enable_moe_block: bool = False
|
||||
num_experts: Optional[int] = None
|
||||
top_k_experts: Optional[int] = None
|
||||
moe_intermediate_size: Optional[int] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is None:
|
||||
self.rope_parameters = {
|
||||
"full_attention": {
|
||||
"partial_rotary_factor": 0.25,
|
||||
"rope_theta": 1000000.0,
|
||||
"rope_type": "proportional",
|
||||
},
|
||||
"sliding_attention": {
|
||||
"partial_rotary_factor": 1.0,
|
||||
"rope_theta": 10000.0,
|
||||
"rope_type": "default",
|
||||
},
|
||||
}
|
||||
if self.layer_types is None:
|
||||
pattern = ["sliding_attention"] * (self.sliding_window_pattern - 1) + [
|
||||
"full_attention"
|
||||
]
|
||||
self.layer_types = (pattern * (self.num_hidden_layers // len(pattern) + 1))[
|
||||
: self.num_hidden_layers
|
||||
]
|
||||
|
||||
|
||||
class RMSNormNoScale(nn.Module):
|
||||
"""RMSNorm without learnable scale."""
|
||||
|
||||
def __init__(self, dim: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return mx.fast.rms_norm(x, None, self.eps)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def logit_softcap(softcap, x):
|
||||
return mx.tanh(x / softcap) * softcap
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _complete_square(x2, y2, xy):
|
||||
return x2 + mx.expand_dims(y2, -1) - 2 * xy
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def geglu(gate, x):
|
||||
return nn.gelu_approx(gate) * x
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int = 0):
|
||||
super().__init__()
|
||||
first_kv_shared_layer_idx = (
|
||||
config.num_hidden_layers - config.num_kv_shared_layers
|
||||
)
|
||||
is_kv_shared_layer = layer_idx >= first_kv_shared_layer_idx > 0
|
||||
use_double_wide = config.use_double_wide_mlp and is_kv_shared_layer
|
||||
intermediate_size = config.intermediate_size * (2 if use_double_wide else 1)
|
||||
|
||||
self.gate_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(intermediate_size, config.hidden_size, bias=False)
|
||||
self.up_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(geglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Router(nn.Module):
|
||||
"""Expert router: norm -> scale -> project -> top-k -> renormalize."""
|
||||
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.eps = config.rms_norm_eps
|
||||
self.proj = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
||||
self.scale = mx.ones((config.hidden_size,))
|
||||
self.per_expert_scale = mx.ones((config.num_experts,))
|
||||
self._root_size = config.hidden_size**-0.5
|
||||
|
||||
def __call__(self, x: mx.array):
|
||||
x = mx.fast.rms_norm(x, self.scale * self._root_size, self.eps)
|
||||
|
||||
expert_scores = self.proj(x)
|
||||
|
||||
top_k_indices = mx.argpartition(
|
||||
expert_scores, kth=-self.config.top_k_experts, axis=-1
|
||||
)
|
||||
top_k_indices = top_k_indices[..., -self.config.top_k_experts :]
|
||||
|
||||
top_k_weights = mx.take_along_axis(expert_scores, top_k_indices, axis=-1)
|
||||
top_k_weights = mx.softmax(top_k_weights, axis=-1)
|
||||
top_k_weights = top_k_weights * self.per_expert_scale[top_k_indices]
|
||||
|
||||
return top_k_indices, top_k_weights
|
||||
|
||||
|
||||
class GeGLU(nn.Module):
|
||||
"""GELU-gated linear unit activation for SwitchGLU."""
|
||||
|
||||
def __call__(self, x, gate):
|
||||
return geglu(gate, x)
|
||||
|
||||
|
||||
class Experts(nn.Module):
|
||||
"""Sparse MoE using SwitchGLU with gather_mm."""
|
||||
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.switch_glu = SwitchGLU(
|
||||
input_dims=config.hidden_size,
|
||||
hidden_dims=config.moe_intermediate_size,
|
||||
num_experts=config.num_experts,
|
||||
activation=GeGLU(),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, top_k_indices: mx.array, top_k_weights: mx.array
|
||||
) -> mx.array:
|
||||
w = mx.expand_dims(top_k_weights, -1)
|
||||
y = self.switch_glu(x, top_k_indices)
|
||||
|
||||
return (w * y).sum(-2)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.layer_idx = layer_idx
|
||||
self.layer_type = config.layer_types[layer_idx]
|
||||
self.is_sliding = self.layer_type == "sliding_attention"
|
||||
self.has_kv = layer_idx < config.num_hidden_layers - config.num_kv_shared_layers
|
||||
|
||||
self.head_dim = (
|
||||
config.global_head_dim
|
||||
if self.layer_type == "full_attention"
|
||||
and hasattr(config, "global_head_dim")
|
||||
and config.global_head_dim
|
||||
else config.head_dim
|
||||
)
|
||||
|
||||
dim = config.hidden_size
|
||||
self.n_heads = config.num_attention_heads
|
||||
|
||||
# K-eq-V for full attention layers (26B/31B models)
|
||||
self.use_k_eq_v = config.attention_k_eq_v and not self.is_sliding
|
||||
if self.use_k_eq_v and config.num_global_key_value_heads is not None:
|
||||
self.n_kv_heads = config.num_global_key_value_heads
|
||||
else:
|
||||
self.n_kv_heads = config.num_key_value_heads
|
||||
|
||||
self.scale = 1.0
|
||||
|
||||
self.q_proj = nn.Linear(dim, self.n_heads * self.head_dim, bias=False)
|
||||
if self.has_kv:
|
||||
self.k_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
|
||||
if not self.use_k_eq_v:
|
||||
self.v_proj = nn.Linear(
|
||||
dim, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(self.n_heads * self.head_dim, dim, bias=False)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
if self.has_kv:
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.v_norm = RMSNormNoScale(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
# RoPE (with partial rotation support)
|
||||
layer_key = "sliding_attention" if self.is_sliding else "full_attention"
|
||||
rope_params = config.rope_parameters.get(layer_key, {})
|
||||
rope_theta = rope_params.get("rope_theta", 10000.0)
|
||||
self.rope = initialize_rope(
|
||||
dims=self.head_dim,
|
||||
traditional=config.rope_traditional,
|
||||
base=rope_theta,
|
||||
scaling_config=rope_params,
|
||||
max_position_embeddings=config.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
shared_kv: Optional[tuple] = None,
|
||||
offset: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.q_proj(x).reshape(B, L, self.n_heads, self.head_dim)
|
||||
queries = self.q_norm(queries)
|
||||
|
||||
if shared_kv is not None:
|
||||
keys, values = shared_kv
|
||||
elif not self.has_kv:
|
||||
raise ValueError(
|
||||
f"Layer {self.layer_idx} is a KV-shared layer but received no shared_kv"
|
||||
)
|
||||
else:
|
||||
keys = self.k_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim)
|
||||
values = keys
|
||||
if not self.use_k_eq_v:
|
||||
values = self.v_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim)
|
||||
|
||||
offset = mx.array(cache.offset) if cache is not None else 0
|
||||
|
||||
keys = self.k_norm(keys)
|
||||
keys = keys.transpose(0, 2, 1, 3)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
values = self.v_norm(values)
|
||||
values = values.transpose(0, 2, 1, 3)
|
||||
|
||||
queries = queries.transpose(0, 2, 1, 3)
|
||||
queries = self.rope(queries, offset=offset)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output), (keys, values), offset
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.layer_idx = layer_idx
|
||||
self.layer_type = config.layer_types[layer_idx]
|
||||
self.self_attn = Attention(config, layer_idx)
|
||||
self.mlp = MLP(config, layer_idx)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.pre_feedforward_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
# MoE (26B model)
|
||||
self.enable_moe = config.enable_moe_block
|
||||
if self.enable_moe:
|
||||
self.router = Router(config)
|
||||
self.experts = Experts(config)
|
||||
self.post_feedforward_layernorm_1 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm_2 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.pre_feedforward_layernorm_2 = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
# Per-layer input gating (2B/4B models)
|
||||
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
|
||||
if self.hidden_size_per_layer_input:
|
||||
self.per_layer_input_gate = nn.Linear(
|
||||
config.hidden_size, self.hidden_size_per_layer_input, bias=False
|
||||
)
|
||||
self.per_layer_projection = nn.Linear(
|
||||
self.hidden_size_per_layer_input, config.hidden_size, bias=False
|
||||
)
|
||||
self.post_per_layer_input_norm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
else:
|
||||
self.per_layer_input_gate = None
|
||||
self.per_layer_projection = None
|
||||
self.post_per_layer_input_norm = None
|
||||
|
||||
# Layer scalar
|
||||
self.layer_scalar = mx.ones((1,))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
per_layer_input: Optional[mx.array] = None,
|
||||
shared_kv: Optional[tuple] = None,
|
||||
offset: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
residual = x
|
||||
|
||||
h = self.input_layernorm(x)
|
||||
h, shared_kv, offset = self.self_attn(
|
||||
h, mask, cache, shared_kv=shared_kv, offset=offset
|
||||
)
|
||||
h = self.post_attention_layernorm(h)
|
||||
h = residual + h
|
||||
|
||||
residual = h
|
||||
|
||||
if self.enable_moe:
|
||||
h1 = self.pre_feedforward_layernorm(h)
|
||||
h1 = self.mlp(h1)
|
||||
h1 = self.post_feedforward_layernorm_1(h1)
|
||||
|
||||
top_k_indices, top_k_weights = self.router(h)
|
||||
h2 = self.pre_feedforward_layernorm_2(h)
|
||||
h2 = self.experts(h2, top_k_indices, top_k_weights)
|
||||
h2 = self.post_feedforward_layernorm_2(h2)
|
||||
|
||||
h = h1 + h2
|
||||
else:
|
||||
h = self.pre_feedforward_layernorm(h)
|
||||
h = self.mlp(h)
|
||||
|
||||
h = self.post_feedforward_layernorm(h)
|
||||
h = residual + h
|
||||
|
||||
# Per-layer input gating
|
||||
if (
|
||||
self.per_layer_input_gate is not None
|
||||
and self.per_layer_projection is not None
|
||||
and self.post_per_layer_input_norm is not None
|
||||
and per_layer_input is not None
|
||||
):
|
||||
residual = h
|
||||
gate = self.per_layer_input_gate(h)
|
||||
gate = nn.gelu_approx(gate)
|
||||
gate = mx.multiply(gate, per_layer_input)
|
||||
gate = self.per_layer_projection(gate)
|
||||
gate = self.post_per_layer_input_norm(gate)
|
||||
h = residual + gate
|
||||
|
||||
if self.layer_scalar is not None:
|
||||
h = h * self.layer_scalar
|
||||
|
||||
return h, shared_kv, offset
|
||||
|
||||
|
||||
class Gemma4TextModel(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.vocab_size = config.vocab_size
|
||||
self.window_size = config.sliding_window
|
||||
self.sliding_window_pattern = config.sliding_window_pattern
|
||||
self.num_hidden_layers = config.num_hidden_layers
|
||||
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.embed_scale = config.hidden_size**0.5
|
||||
self.layers = [
|
||||
DecoderLayer(config, layer_idx=i) for i in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
# Per-layer input embeddings (2B/4B models)
|
||||
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
|
||||
if self.hidden_size_per_layer_input:
|
||||
self.embed_tokens_per_layer = nn.Embedding(
|
||||
config.vocab_size_per_layer_input,
|
||||
config.num_hidden_layers * config.hidden_size_per_layer_input,
|
||||
)
|
||||
self.embed_tokens_per_layer_scale = config.hidden_size_per_layer_input**0.5
|
||||
self.per_layer_input_scale = 2.0**-0.5
|
||||
self.per_layer_projection_scale = config.hidden_size**-0.5
|
||||
self.per_layer_model_projection = nn.Linear(
|
||||
config.hidden_size,
|
||||
config.num_hidden_layers * config.hidden_size_per_layer_input,
|
||||
bias=False,
|
||||
)
|
||||
self.per_layer_projection_norm = nn.RMSNorm(
|
||||
config.hidden_size_per_layer_input, eps=config.rms_norm_eps
|
||||
)
|
||||
else:
|
||||
self.embed_tokens_per_layer = None
|
||||
self.per_layer_input_scale = None
|
||||
self.per_layer_projection_scale = None
|
||||
self.per_layer_model_projection = None
|
||||
self.per_layer_projection_norm = None
|
||||
|
||||
# Arrange for shared KVs
|
||||
self.previous_kvs = list(range(len(self.layers)))
|
||||
if config.num_kv_shared_layers > 0:
|
||||
N = len(self.layers)
|
||||
M = N - config.num_kv_shared_layers
|
||||
kvs_by_type = {}
|
||||
for i in range(M):
|
||||
kvs_by_type[self.layers[i].layer_type] = i
|
||||
for j in range(M, N):
|
||||
self.previous_kvs[j] = kvs_by_type[self.layers[j].layer_type]
|
||||
|
||||
def _get_per_layer_inputs(
|
||||
self,
|
||||
input_ids: Optional[mx.array],
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
if input_ids is None:
|
||||
if input_embeddings is None:
|
||||
raise RuntimeError(
|
||||
"input_embeddings must be provided when input_ids are omitted."
|
||||
)
|
||||
|
||||
# Split the sequence dimension if this still holds too much
|
||||
# memory. 260k vocab means the distance tensor would be ~1GB
|
||||
# per 2k tokens in bf16.
|
||||
#
|
||||
# If the embedding is quantized we have to dequantize it anyway to
|
||||
# perform the match test.
|
||||
norms_embedding = self.embed_tokens.weight.square().sum(-1)
|
||||
norms_input = input_embeddings.square().sum(-1)
|
||||
distance = _complete_square(
|
||||
norms_embedding,
|
||||
norms_input,
|
||||
self.embed_tokens.as_linear(input_embeddings),
|
||||
)
|
||||
|
||||
# Checks can be added if needed but they necessarily break the GPU
|
||||
# pipelining and force an eval.
|
||||
#
|
||||
# match_counts = (distance < eps).sum(-1)
|
||||
#
|
||||
input_ids = mx.argmin(distance, -1)
|
||||
|
||||
result = self.embed_tokens_per_layer(input_ids)
|
||||
result = result * self.embed_tokens_per_layer_scale
|
||||
return mx.unflatten(
|
||||
result,
|
||||
-1,
|
||||
(self.config.num_hidden_layers, self.hidden_size_per_layer_input),
|
||||
)
|
||||
|
||||
def _project_per_layer_inputs(
|
||||
self,
|
||||
input_embeddings: mx.array,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
per_layer_projection = self.per_layer_model_projection(input_embeddings)
|
||||
per_layer_projection = per_layer_projection * self.per_layer_projection_scale
|
||||
per_layer_projection = mx.unflatten(
|
||||
per_layer_projection,
|
||||
-1,
|
||||
(self.config.num_hidden_layers, self.hidden_size_per_layer_input),
|
||||
)
|
||||
per_layer_projection = self.per_layer_projection_norm(per_layer_projection)
|
||||
|
||||
if per_layer_inputs is None:
|
||||
return per_layer_projection
|
||||
|
||||
return (per_layer_projection + per_layer_inputs) * self.per_layer_input_scale
|
||||
|
||||
def _make_masks(self, h, cache):
|
||||
mask = {}
|
||||
masks = []
|
||||
for l, c in zip(self.layers, cache):
|
||||
if l.layer_type not in mask:
|
||||
if l.layer_type == "full_attention":
|
||||
mask["full_attention"] = create_attention_mask(h, c)
|
||||
elif l.layer_type == "sliding_attention":
|
||||
mask["sliding_attention"] = create_attention_mask(
|
||||
h, c, window_size=self.window_size
|
||||
)
|
||||
masks.append(mask[l.layer_type])
|
||||
return masks
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array = None,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
# Make the initial hidden state
|
||||
if input_embeddings is None:
|
||||
input_embeddings = self.embed_tokens(inputs)
|
||||
h = input_embeddings
|
||||
h = h * self.embed_scale
|
||||
|
||||
# Get the extra inputs per layer if we have per layer embeddings
|
||||
if self.hidden_size_per_layer_input:
|
||||
if per_layer_inputs is None:
|
||||
per_layer_inputs = self._get_per_layer_inputs(inputs, input_embeddings)
|
||||
per_layer_inputs = self._project_per_layer_inputs(h, per_layer_inputs)
|
||||
if per_layer_inputs is not None:
|
||||
per_layer_inputs = [
|
||||
per_layer_inputs[:, :, i, :] for i, _ in enumerate(self.layers)
|
||||
]
|
||||
else:
|
||||
per_layer_inputs = [None] * len(self.layers)
|
||||
|
||||
# Make the kv cache list, be sure to append None for all the shared kv
|
||||
# layers
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
else:
|
||||
cache = cache + [None] * (len(self.layers) - len(cache))
|
||||
|
||||
# Apply each layer. We save all intermediate kvs and offset and grab
|
||||
# the previous one for the shared kv layers.
|
||||
masks = self._make_masks(h, cache)
|
||||
intermediates = [(None, None)] * len(self.layers)
|
||||
for idx, (layer, c, mask, prev_idx, per_layer_input) in enumerate(
|
||||
zip(
|
||||
self.layers,
|
||||
cache,
|
||||
masks,
|
||||
self.previous_kvs,
|
||||
per_layer_inputs,
|
||||
)
|
||||
):
|
||||
kvs, offset = intermediates[prev_idx]
|
||||
|
||||
h, kvs, offset = layer(
|
||||
h,
|
||||
mask,
|
||||
c,
|
||||
per_layer_input=per_layer_input,
|
||||
shared_kv=kvs,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
intermediates[idx] = (kvs, offset)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Gemma4TextModel(args)
|
||||
self.final_logit_softcapping = args.final_logit_softcapping
|
||||
self.tie_word_embeddings = args.tie_word_embeddings
|
||||
if not self.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
per_layer_inputs: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(
|
||||
inputs,
|
||||
cache=cache,
|
||||
input_embeddings=input_embeddings,
|
||||
per_layer_inputs=per_layer_inputs,
|
||||
)
|
||||
if self.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
if self.final_logit_softcapping is not None:
|
||||
out = logit_softcap(self.final_logit_softcapping, out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
sanitized = {}
|
||||
first_kv_shared = self.args.num_hidden_layers - self.args.num_kv_shared_layers
|
||||
for k, v in weights.items():
|
||||
if any(
|
||||
s in k
|
||||
for s in (
|
||||
"self_attn.rotary_emb",
|
||||
"input_max",
|
||||
"input_min",
|
||||
"output_max",
|
||||
"output_min",
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# KV-shared layers reuse K/V from earlier layers — drop their projections
|
||||
if any(
|
||||
s in k
|
||||
for s in (".self_attn.k_proj", ".self_attn.v_proj", ".self_attn.k_norm")
|
||||
):
|
||||
try:
|
||||
layer_idx = int(k.split("layers.")[1].split(".")[0])
|
||||
if layer_idx >= first_kv_shared:
|
||||
continue
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
|
||||
if k.endswith(".experts.gate_up_proj"):
|
||||
base = k.removesuffix(".gate_up_proj")
|
||||
gate, up = map(mx.contiguous, mx.split(v, 2, axis=-2))
|
||||
sanitized[f"{base}.switch_glu.gate_proj.weight"] = gate
|
||||
sanitized[f"{base}.switch_glu.up_proj.weight"] = up
|
||||
continue
|
||||
|
||||
if k.endswith(".experts.down_proj"):
|
||||
base = k.removesuffix(".down_proj")
|
||||
sanitized[f"{base}.switch_glu.down_proj.weight"] = v
|
||||
continue
|
||||
|
||||
sanitized[k] = v
|
||||
|
||||
return sanitized
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router.proj"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def head_dim(self):
|
||||
return self.args.head_dim
|
||||
|
||||
@property
|
||||
def n_kv_heads(self):
|
||||
return self.args.num_key_value_heads
|
||||
|
||||
def make_cache(self):
|
||||
first_kv_shared = self.args.num_hidden_layers - self.args.num_kv_shared_layers
|
||||
caches = []
|
||||
for i in range(first_kv_shared):
|
||||
if self.args.layer_types[i] == "full_attention":
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(
|
||||
RotatingKVCache(
|
||||
max_size=self.args.sliding_window,
|
||||
keep=0,
|
||||
)
|
||||
)
|
||||
return caches
|
||||
@@ -0,0 +1,188 @@
|
||||
# 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 .activations import swiglu
|
||||
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(swiglu(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
|
||||
@@ -0,0 +1,181 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
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(swiglu(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
|
||||
@@ -0,0 +1,403 @@
|
||||
# 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 mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@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(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class 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
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class 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.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
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = 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}")
|
||||
}
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,531 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .mla import MultiLinear
|
||||
from .pipeline import PipelineMixin
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "glm4_moe_lite"
|
||||
vocab_size: int = 154880
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 10240
|
||||
moe_intermediate_size: int = 1536
|
||||
num_hidden_layers: int = 47
|
||||
num_attention_heads: int = 20
|
||||
num_key_value_heads: int = 20
|
||||
n_shared_experts: Optional[int] = 1
|
||||
n_routed_experts: Optional[int] = 64
|
||||
routed_scaling_factor: float = 1.8
|
||||
kv_lora_rank: int = 512
|
||||
q_lora_rank: int = 768
|
||||
qk_rope_head_dim: int = 64
|
||||
qk_nope_head_dim: int = 192
|
||||
v_head_dim: int = 256
|
||||
topk_method: str = "noaux_tc"
|
||||
scoring_func: str = "sigmoid"
|
||||
norm_topk_prob: bool = True
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
num_experts_per_tok: int = 4
|
||||
moe_layer_freq: int = 1
|
||||
first_k_dense_replace: int = 1
|
||||
max_position_embeddings: int = 202752
|
||||
rms_norm_eps: float = 1e-5
|
||||
rope_theta: float = 1_000_000.0
|
||||
rope_scaling: Optional[Dict] = None
|
||||
attention_bias: bool = False
|
||||
attention_dropout: float = 0.0
|
||||
partial_rotary_factor: float = 1.0
|
||||
tie_word_embeddings: bool = False
|
||||
num_nextn_predict_layers: int = 1
|
||||
quantization: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class Glm4MoeLiteAttention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_heads = config.num_attention_heads
|
||||
self.max_position_embeddings = config.max_position_embeddings
|
||||
rope_params = config.rope_scaling
|
||||
self.rope_theta = config.rope_theta
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.qk_rope_head_dim = config.qk_rope_head_dim
|
||||
self.kv_lora_rank = config.kv_lora_rank
|
||||
self.v_head_dim = config.v_head_dim
|
||||
self.qk_nope_head_dim = config.qk_nope_head_dim
|
||||
self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
|
||||
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
else:
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
|
||||
self.q_b_proj = nn.Linear(
|
||||
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=config.rms_norm_eps)
|
||||
head_dim = self.qk_nope_head_dim + self.v_head_dim
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
self.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if rope_params is not None:
|
||||
mscale_all_dim = rope_params.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = rope_params["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=True,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=rope_params,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q = self.q_proj(x)
|
||||
else:
|
||||
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
offset = cache.offset if cache is not None else 0
|
||||
q_pe = self.rope(q_pe, offset)
|
||||
k_pe = self.rope(k_pe, offset)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class Glm4MoeLiteMLP(nn.Module):
|
||||
def __init__(
|
||||
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / (denominator + 1e-20)
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
self.n_routed_experts = config.n_routed_experts
|
||||
self.routed_scaling_factor = config.routed_scaling_factor
|
||||
self.n_group = config.n_group
|
||||
self.topk_group = config.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert config.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class Glm4MoeLiteMoE(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.num_experts_per_tok = config.num_experts_per_tok
|
||||
self.switch_mlp = SwitchGLU(
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
if config.n_shared_experts is not None:
|
||||
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
|
||||
self.shared_experts = Glm4MoeLiteMLP(
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Glm4MoeLiteDecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.self_attn = Glm4MoeLiteAttention(config)
|
||||
use_moe = (
|
||||
config.n_routed_experts is not None
|
||||
and layer_idx >= config.first_k_dense_replace
|
||||
and layer_idx % config.moe_layer_freq == 0
|
||||
)
|
||||
self.mlp = Glm4MoeLiteMoE(config) if use_moe else Glm4MoeLiteMLP(config)
|
||||
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class Glm4MoeLiteModel(PipelineMixin, nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.vocab_size = config.vocab_size
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.layers = [
|
||||
Glm4MoeLiteDecoderLayer(config, idx)
|
||||
for idx in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(x)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
mask = create_attention_mask(h, cache[0], return_array=True)
|
||||
|
||||
# Receive from the previous process in the pipeline
|
||||
if pipeline_rank < pipeline_size - 1:
|
||||
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
|
||||
|
||||
for l, c in zip(self.pipeline_layers, cache):
|
||||
h = l(h, mask, cache=c)
|
||||
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.model = Glm4MoeLiteModel(config)
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
def is_mpt_layer(key):
|
||||
subkeys = key.split(".")
|
||||
if len(subkeys) < 3:
|
||||
return False
|
||||
if (
|
||||
subkeys[1] == "layers"
|
||||
and int(subkeys[2]) >= self.args.num_hidden_layers
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
new_weights = {}
|
||||
for k, v in weights.items():
|
||||
if is_mpt_layer(k):
|
||||
continue
|
||||
else:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
prefix = f"model.layers.{l}.self_attn"
|
||||
if f"{prefix}.kv_b_proj.weight" in weights:
|
||||
layer = self.layers[l].self_attn.embed_q
|
||||
quantized = f"{prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(f"{prefix}.kv_b_proj.weight")
|
||||
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
|
||||
# Try to infer bits and group size
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
num_heads = self.args.num_attention_heads
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(
|
||||
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
|
||||
)
|
||||
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
|
||||
if quantized:
|
||||
wk, wk_scales, wk_biases = mx.quantize(
|
||||
wk, bits=bits, group_size=group_size
|
||||
)
|
||||
wv, wv_scales, wv_biases = mx.quantize(
|
||||
wv, bits=bits, group_size=group_size
|
||||
)
|
||||
weights[f"{prefix}.embed_q.scales"] = wk_scales
|
||||
weights[f"{prefix}.unembed_out.scales"] = wv_scales
|
||||
weights[f"{prefix}.embed_q.biases"] = wk_biases
|
||||
weights[f"{prefix}.unembed_out.biases"] = wv_biases
|
||||
weights[f"{prefix}.embed_q.weight"] = wk
|
||||
weights[f"{prefix}.unembed_out.weight"] = wv
|
||||
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
rank = group.rank()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
num_heads = layer.self_attn.num_heads
|
||||
sh = rank * num_heads
|
||||
eh = sh + num_heads
|
||||
|
||||
def shard_heads(w):
|
||||
return w[sh:eh]
|
||||
|
||||
layer.self_attn.embed_q.apply(shard_heads)
|
||||
layer.self_attn.unembed_out.apply(shard_heads)
|
||||
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, Glm4MoeLiteMLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
if getattr(layer.mlp, "shared_experts", None) is not None:
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .deepseek_v32 import Model as DSV32Model
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
index_head_dim: int
|
||||
index_n_heads: int
|
||||
index_topk: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
n_shared_experts: Optional[int]
|
||||
n_routed_experts: Optional[int]
|
||||
routed_scaling_factor: float
|
||||
kv_lora_rank: int
|
||||
q_lora_rank: int
|
||||
qk_rope_head_dim: int
|
||||
v_head_dim: int
|
||||
qk_nope_head_dim: int
|
||||
topk_method: str
|
||||
scoring_func: str
|
||||
norm_topk_prob: bool
|
||||
n_group: int
|
||||
topk_group: int
|
||||
num_experts_per_tok: int
|
||||
moe_layer_freq: int
|
||||
first_k_dense_replace: int
|
||||
max_position_embeddings: int
|
||||
rms_norm_eps: float
|
||||
rope_parameters: Dict
|
||||
attention_bias: bool
|
||||
rope_scaling: Dict = None
|
||||
rope_theta: Optional[float] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.rope_scaling = self.rope_parameters
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
|
||||
|
||||
class Model(DSV32Model):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__(config)
|
||||
+14
-15
@@ -1,11 +1,10 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2023 - 2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from typing import Any, Optional
|
||||
|
||||
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
|
||||
|
||||
@@ -126,25 +125,26 @@ class GPT2Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
_, L = inputs.shape
|
||||
|
||||
hidden_states = self.wte(inputs)
|
||||
|
||||
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,10 +161,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model.wte.as_linear(out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
@@ -137,24 +137,21 @@ 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 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)
|
||||
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):
|
||||
hidden_states = layer(hidden_states, mask, cache=c)
|
||||
@@ -174,10 +171,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.transformer(inputs, mask, cache)
|
||||
out = self.transformer(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.transformer.wte.as_linear(out)
|
||||
else:
|
||||
|
||||
+20
-14
@@ -1,11 +1,10 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from typing import Any, Optional
|
||||
|
||||
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
|
||||
|
||||
@@ -24,6 +23,7 @@ class ModelArgs(BaseModelArgs):
|
||||
vocab_size: int
|
||||
rotary_emb_base: int
|
||||
rotary_pct: float
|
||||
use_parallel_residual: bool = True
|
||||
num_key_value_heads: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -108,6 +108,7 @@ class TransformerBlock(nn.Module):
|
||||
self.layer_norm_eps = args.layer_norm_eps
|
||||
self.attention = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.use_parallel_residual = args.use_parallel_residual
|
||||
self.input_layernorm = nn.LayerNorm(
|
||||
self.hidden_size,
|
||||
eps=self.layer_norm_eps,
|
||||
@@ -122,12 +123,20 @@ class TransformerBlock(nn.Module):
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
residual = x
|
||||
# NeoX runs attention and feedforward network in parallel.
|
||||
attn = self.attention(self.input_layernorm(x), mask, cache)
|
||||
ffn = self.mlp(self.post_attention_layernorm(x))
|
||||
out = attn + ffn + residual
|
||||
return out
|
||||
if self.use_parallel_residual:
|
||||
residual = x
|
||||
# Run attention and feedforward network in parallel.
|
||||
attn = self.attention(self.input_layernorm(x), mask, cache)
|
||||
ffn = self.mlp(self.post_attention_layernorm(x))
|
||||
out = attn + ffn + residual
|
||||
return out
|
||||
else:
|
||||
# Run attention and feedforward network sequentially.
|
||||
attn_output = self.attention(self.input_layernorm(x), mask, cache)
|
||||
x = x + attn_output
|
||||
ffn_output = self.mlp(self.post_attention_layernorm(x))
|
||||
x = x + ffn_output
|
||||
return x
|
||||
|
||||
|
||||
class GPTNeoXModel(nn.Module):
|
||||
@@ -146,19 +155,17 @@ class GPTNeoXModel(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
_, L = inputs.shape
|
||||
|
||||
hidden_states = self.embed_in(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(hidden_states, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
hidden_states = layer(hidden_states, mask, cache=c)
|
||||
|
||||
@@ -178,10 +185,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
# 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 mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
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)
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
g = self.router(x)
|
||||
experts, indices = mlx_topk(g, k=self.num_experts_per_tok, axis=-1)
|
||||
expert_weights = mx.softmax(experts, axis=-1, precise=True)
|
||||
|
||||
# Experts block
|
||||
x = self.experts(x, indices)
|
||||
|
||||
x = x * mx.expand_dims(expert_weights, axis=-1)
|
||||
|
||||
y = x.sum(axis=-2)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
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
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
R = group.rank()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, sharding="sharded-to-all", group=group
|
||||
)
|
||||
|
||||
layer.self_attn.num_attention_heads //= N
|
||||
layer.self_attn.num_key_value_heads //= N
|
||||
layer.self_attn.num_key_value_groups = (
|
||||
layer.self_attn.num_attention_heads
|
||||
// layer.self_attn.num_key_value_heads
|
||||
)
|
||||
|
||||
layer.self_attn.sinks = layer.self_attn.sinks[
|
||||
layer.self_attn.num_attention_heads
|
||||
* R : layer.self_attn.num_attention_heads
|
||||
* (R + 1)
|
||||
]
|
||||
|
||||
shard_inplace(layer.mlp.experts.gate_proj, "all-to-sharded", group=group)
|
||||
shard_inplace(layer.mlp.experts.down_proj, "sharded-to-all", group=group)
|
||||
layer.mlp.experts.down_proj.bias /= N
|
||||
shard_inplace(
|
||||
layer.mlp.experts.up_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
|
||||
layer.mlp.sharding_group = group
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@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
|
||||
@@ -0,0 +1,193 @@
|
||||
# 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 .activations import swiglu
|
||||
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
|
||||
logits_scaling: float
|
||||
attention_multiplier: float
|
||||
embedding_multiplier: float
|
||||
residual_multiplier: float
|
||||
max_position_embeddings: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
mlp_bias: bool
|
||||
rope_theta: float
|
||||
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.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)
|
||||
|
||||
# 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)
|
||||
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
|
||||
if hasattr(args, "mlp_bias"):
|
||||
mlp_bias = args.mlp_bias
|
||||
else:
|
||||
mlp_bias = False
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class 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.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.mlp(self.post_attention_layernorm(h))
|
||||
out = h + r * self.residual_multiplier
|
||||
return out
|
||||
|
||||
|
||||
class GraniteModel(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)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.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 = GraniteModel(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
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
logits_scaling: float
|
||||
attention_multiplier: float
|
||||
embedding_multiplier: float
|
||||
residual_multiplier: float
|
||||
max_position_embeddings: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
rope_theta: float
|
||||
num_local_experts: int
|
||||
num_experts_per_tok: int
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class GraniteMoeAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.hidden_size // n_heads
|
||||
|
||||
self.scale = args.attention_multiplier
|
||||
attention_bias = args.attention_bias
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=attention_bias)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
False,
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class GraniteMoeTopKGating(nn.Module):
|
||||
def __init__(self, input_size: int, num_experts: int, top_k: int):
|
||||
super().__init__()
|
||||
self.num_experts = num_experts
|
||||
self.input_size = input_size
|
||||
self.top_k = top_k
|
||||
self.layer = nn.Linear(input_size, num_experts, bias=False)
|
||||
|
||||
def __call__(self, hidden_states: mx.array):
|
||||
logits = self.layer(hidden_states)
|
||||
top_k_idx = mx.argpartition(logits, kth=-self.top_k, axis=-1)[
|
||||
..., -self.top_k :
|
||||
]
|
||||
top_k_logits = mx.take_along_axis(logits, top_k_idx, axis=-1)
|
||||
top_k_gates = mx.softmax(top_k_logits.astype(mx.float32), axis=-1)
|
||||
return top_k_idx, top_k_gates
|
||||
|
||||
|
||||
class GraniteMoeMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.input_size = args.hidden_size
|
||||
self.hidden_size = args.intermediate_size
|
||||
self.switch_mlp = SwitchGLU(
|
||||
self.input_size, self.hidden_size, args.num_local_experts
|
||||
)
|
||||
self.router = GraniteMoeTopKGating(
|
||||
input_size=self.input_size,
|
||||
num_experts=args.num_local_experts,
|
||||
top_k=args.num_experts_per_tok,
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
token_ids, gates = self.router(x)
|
||||
y = self.switch_mlp(x, token_ids)
|
||||
return (y * gates[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
|
||||
class GraniteMoeDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = GraniteMoeAttention(args)
|
||||
self.block_sparse_moe = GraniteMoeMoE(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.residual_multiplier = args.residual_multiplier
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r * self.residual_multiplier
|
||||
r = self.block_sparse_moe(self.post_attention_layernorm(h))
|
||||
out = h + r * self.residual_multiplier
|
||||
return out
|
||||
|
||||
|
||||
class GraniteMoEModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
GraniteMoeDecoderLayer(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.embedding_multiplier = args.embedding_multiplier
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs) * self.embedding_multiplier
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = GraniteMoEModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
self.logits_scaling = args.logits_scaling
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out / self.logits_scaling
|
||||
|
||||
def sanitize(self, weights):
|
||||
if "model.layers.0.block_sparse_moe.input_linear.weight" not in weights:
|
||||
return weights
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.block_sparse_moe"
|
||||
key = f"{prefix}.input_linear.weight"
|
||||
value = weights.pop(key)
|
||||
gate_proj, up_proj = mx.split(value, 2, axis=1)
|
||||
weights[key.replace("input_linear", "switch_mlp.gate_proj")] = gate_proj
|
||||
weights[key.replace("input_linear", "switch_mlp.up_proj")] = up_proj
|
||||
key = f"{prefix}.output_linear.weight"
|
||||
weights[key.replace("output_linear", "switch_mlp.down_proj")] = weights.pop(
|
||||
key
|
||||
)
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("block_sparse_moe.router.layer"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,559 @@
|
||||
# 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 .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .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 = swiglu(gate, hidden_states)
|
||||
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 _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
t = padded_input.shape[1]
|
||||
ends = mx.clip(cache.lengths, 0, t - n_keep)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(padded_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = padded_input[:, -n_keep:, :]
|
||||
else:
|
||||
padded_input = mx.pad(
|
||||
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
|
||||
)
|
||||
|
||||
conv_output = self.conv1d(padded_input)
|
||||
return nn.silu(conv_output)
|
||||
|
||||
def _ssm(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
cache: Optional[ArraysCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
hidden_states = hidden_states.reshape(
|
||||
batch_size, seq_len, self.num_heads, self.head_dim
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D.astype(hidden_states.dtype),
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[ArraysCache] = 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,
|
||||
)
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
y = self._ssm(hidden_states_ssm, B, C, dt, cache, mask)
|
||||
if cache:
|
||||
cache.advance(y.shape[1])
|
||||
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(swiglu(gate, up))
|
||||
|
||||
|
||||
class GraniteMoeHybridMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
mlp_bias = args.mlp_bias
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class GraniteMoeHybridLayer(nn.Module):
|
||||
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(ArraysCache(size=2))
|
||||
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
|
||||
@@ -0,0 +1,183 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
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
|
||||
attention_bias: bool
|
||||
head_dim: int
|
||||
max_position_embeddings: int
|
||||
mlp_bias: bool
|
||||
model_type: str
|
||||
rope_theta: float
|
||||
tie_word_embeddings: bool
|
||||
|
||||
|
||||
class HeliumAttention(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=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.rope = nn.RoPE(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)
|
||||
|
||||
# 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)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class HeliumMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.intermediate_size = args.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) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class HeliumDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.self_attn = HeliumAttention(args)
|
||||
self.mlp = HeliumMLP(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 HeliumModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.vocab_size = args.vocab_size
|
||||
|
||||
assert self.vocab_size > 0
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [HeliumDecoderLayer(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,
|
||||
) -> 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 = HeliumModel(args)
|
||||
|
||||
self.vocab_size = args.vocab_size
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
+59
-19
@@ -1,12 +1,12 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -30,6 +30,7 @@ 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,6 +42,12 @@ 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,
|
||||
@@ -76,7 +83,6 @@ class Attention(nn.Module):
|
||||
|
||||
head_dim = args.hidden_size // n_heads
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.attention_bias)
|
||||
if kv_proj:
|
||||
self.k_proj = nn.Linear(
|
||||
@@ -107,7 +113,6 @@ class Attention(nn.Module):
|
||||
B, L, D = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
|
||||
if kv_states is None:
|
||||
keys, values = self.k_proj(x), self.v_proj(x)
|
||||
kv_states = keys, values
|
||||
@@ -144,7 +149,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
@@ -157,20 +162,29 @@ class Gate(nn.Module):
|
||||
|
||||
|
||||
class MoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int = 0):
|
||||
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:
|
||||
self.shared_mlp = MLP(dim, intermediate_size * args.num_shared_expert)
|
||||
num_shared = _int_or_list(args.num_shared_expert, layer_idx)
|
||||
self.shared_mlp = MLP(dim, int(intermediate_size * num_shared))
|
||||
|
||||
self.num_experts = num_experts = args.num_experts
|
||||
self.top_k = args.moe_topk
|
||||
self.top_k = _int_or_list(args.moe_topk, layer_idx)
|
||||
|
||||
self.gate = Gate(dim, num_experts)
|
||||
self.switch_mlp = SwitchGLU(dim, intermediate_size, 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)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -184,7 +198,7 @@ class MoeBlock(nn.Module):
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
y = (y * scores[..., None].astype(mx.float32)).sum(axis=-2).astype(y.dtype)
|
||||
|
||||
if self.use_shared_mlp:
|
||||
shared_expert_output = self.shared_mlp(x)
|
||||
@@ -194,11 +208,14 @@ class MoeBlock(nn.Module):
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, kv_proj: bool):
|
||||
def __init__(self, args: ModelArgs, kv_proj: bool, layer_idx: int = 0):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.self_attn = Attention(kv_proj, args)
|
||||
self.mlp = MoeBlock(args)
|
||||
if args.num_experts == 1:
|
||||
self.mlp = MLP(args.hidden_size, args.intermediate_size)
|
||||
else:
|
||||
self.mlp = MoeBlock(args, layer_idx)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
@@ -231,7 +248,11 @@ class HunYuanModel(nn.Module):
|
||||
assert self.vocab_size > 0
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
DecoderLayer(args=args, kv_proj=(i % args.cla_share_factor) == 0)
|
||||
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)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
@@ -239,19 +260,16 @@ 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 i % self.args.cla_share_factor == 0:
|
||||
if (not self.args.use_cla) or i % self.args.cla_share_factor == 0:
|
||||
shared_kv_states = None
|
||||
h, shared_kv_states = layer(h, mask, c, shared_kv_states)
|
||||
|
||||
@@ -268,13 +286,35 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
if "model.layers.0.mlp.gate_and_up_proj.weight" in weights:
|
||||
new_weights = {}
|
||||
D = self.args.hidden_size
|
||||
n_kv_heads = self.args.num_key_value_heads
|
||||
n_kv_groups = self.args.num_attention_heads // n_kv_heads
|
||||
head_dim = D // self.args.num_attention_heads
|
||||
for k, v in weights.items():
|
||||
if "qkv_proj" in k:
|
||||
v = v.reshape(n_kv_heads, n_kv_groups + 2, head_dim, -1)
|
||||
splits = v.split([n_kv_groups, n_kv_groups + 1], axis=1)
|
||||
for k_up, v_new in zip(["q_proj", "k_proj", "v_proj"], splits):
|
||||
k_new = k.replace("qkv_proj", k_up)
|
||||
new_weights[k_new] = mx.flatten(v_new, 0, 2)
|
||||
elif "gate_and_up_proj" in k:
|
||||
splits = v.split(2, axis=0)
|
||||
for k_up, v_new in zip(["up_proj", "gate_proj"], splits):
|
||||
k_new = k.replace("gate_and_up_proj", k_up)
|
||||
new_weights[k_new] = v_new
|
||||
else:
|
||||
new_weights[k] = v
|
||||
weights = new_weights
|
||||
|
||||
if "model.layers.0.mlp.experts.0.up_proj.weight" not in weights:
|
||||
return weights
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# 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 .activations import swiglu
|
||||
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(swiglu(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
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -156,7 +157,7 @@ class MLP(nn.Module):
|
||||
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
||||
return self.w2(swiglu(self.w1(x), self.w3(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -193,17 +194,14 @@ class InternLM2Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
h = self.tok_embeddings(inputs)
|
||||
|
||||
if mask is None:
|
||||
mask = create_attention_mask(h, cache)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
@@ -222,10 +220,9 @@ class Model(nn.Module):
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
mask: mx.array = None,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, mask, cache)
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.tok_embeddings.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# 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 .activations import swiglu
|
||||
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
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
bias: bool = False
|
||||
qkv_bias: bool = False
|
||||
max_position_embeddings: int = 32768
|
||||
num_key_value_heads: int = None
|
||||
rope_theta: float = 10000
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
|
||||
if self.rope_scaling:
|
||||
required_keys = {"factor", "rope_type"}
|
||||
if not all(key in self.rope_scaling for key in required_keys):
|
||||
raise ValueError(f"rope_scaling must contain keys {required_keys}")
|
||||
|
||||
if self.rope_scaling["rope_type"] not in ["linear", "dynamic"]:
|
||||
raise ValueError(
|
||||
"rope_scaling 'rope_type' currently only supports 'linear' or 'dynamic"
|
||||
)
|
||||
|
||||
|
||||
class DynamicNTKScalingRoPE(nn.Module):
|
||||
"""Implements the rotary positional encoding with Dynamic NTK scaling."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dims: int,
|
||||
max_position_embeddings: int = 2048,
|
||||
traditional: bool = False,
|
||||
base: float = 10000,
|
||||
scale: float = 1.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.original_base = base
|
||||
self.dims = dims
|
||||
self.traditional = traditional
|
||||
self.scale = scale
|
||||
|
||||
def extra_repr(self):
|
||||
return f"{self.dims}, traditional={self.traditional}, max_position_embeddings={self.max_position_embeddings}, scaling_factor={self.scaling_factor}"
|
||||
|
||||
def __call__(self, x, offset: int = 0):
|
||||
seq_len = x.shape[1] + offset
|
||||
if seq_len > self.max_position_embeddings:
|
||||
base = self.original_base * (
|
||||
(self.scale * seq_len / self.max_position_embeddings) - (self.scale - 1)
|
||||
) ** (self.dims / (self.dims - 2))
|
||||
else:
|
||||
base = self.original_base
|
||||
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
self.dims,
|
||||
traditional=self.traditional,
|
||||
base=base,
|
||||
scale=self.scale,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
qkv_bias = args.qkv_bias
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
self.n_kv_groups = n_heads // args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.hidden_size // n_heads
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=qkv_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=qkv_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=qkv_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=qkv_bias)
|
||||
|
||||
rope_scale = (
|
||||
1 / args.rope_scaling["factor"]
|
||||
if args.rope_scaling is not None
|
||||
and args.rope_scaling["rope_type"] == "linear"
|
||||
else 2.0
|
||||
)
|
||||
|
||||
self.rope = DynamicNTKScalingRoPE(
|
||||
head_dim,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
traditional=args.rope_traditional,
|
||||
base=args.rope_theta,
|
||||
scale=rope_scale,
|
||||
)
|
||||
|
||||
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)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, dim, hidden_dim, bias):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args.hidden_size, args.intermediate_size, args.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))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class InternLM2Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
assert args.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, 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 = InternLM2Model(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
|
||||
return {k: v for k, v in weights.items() if "attention.rope.inv_freq" not in k}
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -0,0 +1,286 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _compute_gate(query: mx.array, weight: mx.array, bias: mx.array) -> mx.array:
|
||||
gate_logits = query @ weight[:, None, :].swapaxes(-1, -2)
|
||||
gate_logits = gate_logits + bias[..., None, None]
|
||||
return mx.sigmoid(gate_logits)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _silu_mul(gate: mx.array, up: mx.array) -> mx.array:
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _mix_attention(
|
||||
gate: mx.array, attn_global: mx.array, attn_local: mx.array
|
||||
) -> mx.array:
|
||||
return gate * attn_global + (1 - gate) * attn_local
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
head_dim: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int = 131072
|
||||
attention_bias: bool = False
|
||||
mlp_bias: bool = False
|
||||
rope_theta: float = 500000.0
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
loop_num: int = 2
|
||||
loop_window_size: int = 64
|
||||
|
||||
|
||||
class LoopGateProjection(nn.Module):
|
||||
def __init__(self, num_heads: int, head_dim: int):
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = head_dim
|
||||
self.weight = mx.zeros((num_heads, head_dim))
|
||||
self.bias = mx.zeros((num_heads,))
|
||||
|
||||
def __call__(self, query: mx.array) -> mx.array:
|
||||
return _compute_gate(query, self.weight, self.bias)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = head_dim = args.head_dim
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=args.attention_bias)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
head_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def get_qkv(
|
||||
self, x: mx.array, offset: int = 0
|
||||
) -> Tuple[mx.array, mx.array, mx.array]:
|
||||
B, L, _ = x.shape
|
||||
queries = self.q_proj(x).reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = self.k_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = self.v_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
return queries, keys, values
|
||||
|
||||
def attention(
|
||||
self,
|
||||
queries: mx.array,
|
||||
keys: mx.array,
|
||||
values: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
return scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(_silu_mul(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
|
||||
class IQuestLoopCoderModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
assert args.loop_num == 2, f"Only loop_num=2 is supported, got {args.loop_num}"
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gate_projections = [
|
||||
LoopGateProjection(args.num_attention_heads, args.head_dim)
|
||||
for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.loop_num = args.loop_num
|
||||
self.loop_window_size = args.loop_window_size
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
):
|
||||
B, L = inputs.shape[:2]
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * (2 * len(self.layers))
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
window_mask = create_attention_mask(
|
||||
h, cache[len(self.layers)], window_size=self.loop_window_size
|
||||
)
|
||||
|
||||
loop1_kv = []
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q1, k1, v1 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
|
||||
if c is not None:
|
||||
k1, v1 = c.update_and_fetch(k1, v1)
|
||||
loop1_kv.append((k1, v1))
|
||||
|
||||
out = layer.self_attn.attention(q1, k1, v1, mask, cache=c)
|
||||
r = layer.self_attn.o_proj(out.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
for layer, gate_proj, c, (k1, v1) in zip(
|
||||
self.layers, self.gate_projections, cache[len(self.layers) :], loop1_kv
|
||||
):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q2, k2, v2 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
gate = gate_proj(q2)
|
||||
attn_global = layer.self_attn.attention(q2, k1, v1, mask, cache=c)
|
||||
|
||||
if c is not None:
|
||||
k2, v2 = c.update_and_fetch(k2, v2)
|
||||
attn_local = layer.self_attn.attention(
|
||||
q2,
|
||||
k2,
|
||||
v2,
|
||||
window_mask,
|
||||
cache=c,
|
||||
)
|
||||
|
||||
mixed = _mix_attention(gate, attn_global, attn_local)
|
||||
r = layer.self_attn.o_proj(mixed.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = IQuestLoopCoderModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
|
||||
for i, layer in enumerate(self.model.layers):
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
gate_proj = self.model.gate_projections[i]
|
||||
heads_per_rank = gate_proj.num_heads // N
|
||||
start = rank * heads_per_rank
|
||||
end = start + heads_per_rank
|
||||
gate_proj.weight = gate_proj.weight[start:end, :]
|
||||
gate_proj.bias = gate_proj.bias[start:end]
|
||||
gate_proj.num_heads = heads_per_rank
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [KVCache() for _ in self.layers] + [
|
||||
RotatingKVCache(max_size=self.args.loop_window_size) for _ in self.layers
|
||||
]
|
||||
@@ -0,0 +1,385 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attn_layer_offset: int
|
||||
attn_layer_period: int
|
||||
expert_layer_offset: int
|
||||
expert_layer_period: int
|
||||
mamba_d_conv: int
|
||||
mamba_d_state: int
|
||||
mamba_expand: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
vocab_size: int
|
||||
mamba_dt_rank: Union[str, int] = "auto"
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_conv_bias: bool = True
|
||||
layers_block_type: Optional[List[str]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if self.mamba_dt_rank == "auto":
|
||||
self.mamba_dt_rank = math.ceil(self.hidden_size / 16)
|
||||
if self.layers_block_type is None:
|
||||
self.layers_block_type = [
|
||||
(
|
||||
"attention"
|
||||
if i % self.attn_layer_period == self.attn_layer_offset
|
||||
else "mamba"
|
||||
)
|
||||
for i in range(self.num_hidden_layers)
|
||||
]
|
||||
|
||||
|
||||
class JambaMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class JambaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.hidden_size // args.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
args.num_attention_heads * self.head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def fma(a, b, c):
|
||||
return a * b + c
|
||||
|
||||
|
||||
class JambaMambaMixer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_expand * args.hidden_size
|
||||
self.time_step_rank = args.mamba_dt_rank
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
self.use_bias = args.mamba_proj_bias
|
||||
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size, self.intermediate_size * 2, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.intermediate_size,
|
||||
out_channels=self.intermediate_size,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.intermediate_size,
|
||||
bias=self.use_conv_bias,
|
||||
padding=0,
|
||||
)
|
||||
self.x_proj = nn.Linear(
|
||||
self.intermediate_size,
|
||||
self.time_step_rank + self.ssm_state_size * 2,
|
||||
bias=False,
|
||||
)
|
||||
self.dt_proj = nn.Linear(self.time_step_rank, self.intermediate_size, bias=True)
|
||||
|
||||
A = mx.repeat(
|
||||
mx.arange(1.0, self.ssm_state_size + 1.0).reshape([1, self.ssm_state_size]),
|
||||
repeats=self.intermediate_size,
|
||||
axis=0,
|
||||
)
|
||||
self.A_log = mx.log(A)
|
||||
self.D = mx.ones([self.intermediate_size])
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.dt_layernorm = nn.RMSNorm(self.time_step_rank, eps=args.rms_norm_eps)
|
||||
self.b_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
self.c_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
|
||||
def ssm_step(self, x, A, state=None):
|
||||
T = x.shape[1]
|
||||
D = self.D
|
||||
deltaBC = self.x_proj(x)
|
||||
delta, B, C = mx.split(
|
||||
deltaBC,
|
||||
[self.time_step_rank, self.time_step_rank + self.ssm_state_size],
|
||||
axis=-1,
|
||||
)
|
||||
delta, B, C = self.dt_layernorm(delta), self.b_layernorm(B), self.c_layernorm(C)
|
||||
delta = nn.softplus(self.dt_proj(delta))
|
||||
new_state = mx.expand_dims(delta * x, -1) * mx.expand_dims(B, -2)
|
||||
dtA = mx.exp(mx.expand_dims(delta, -1) * A)
|
||||
|
||||
# TODO, speed up prefill with chunked scan
|
||||
for t in range(T):
|
||||
if state is not None:
|
||||
new_state[:, t] = fma(state, dtA[:, t], new_state[:, t])
|
||||
state = new_state[:, t]
|
||||
y = (new_state @ mx.expand_dims(C, -1)).squeeze(-1)
|
||||
y = y + D * x
|
||||
return y, new_state[:, -1]
|
||||
|
||||
def _process_sequence(self, x, conv_state, ssm_state):
|
||||
xz = self.in_proj(x)
|
||||
x, z = xz.split(indices_or_sections=2, axis=-1)
|
||||
K = self.conv_kernel_size
|
||||
if conv_state is not None:
|
||||
x_full = mx.concatenate([conv_state, x], axis=1)
|
||||
else:
|
||||
x_full = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
|
||||
conv_out = self.conv1d(x_full)
|
||||
conv_state = x_full[:, -(K - 1) :, :]
|
||||
x = nn.silu(conv_out)
|
||||
A = -mx.exp(self.A_log)
|
||||
y, ssm_state = self.ssm_step(x, A, ssm_state)
|
||||
z = self.out_proj(swiglu(z, y))
|
||||
return z, (conv_state, ssm_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
if cache is None:
|
||||
conv_state, ssm_state = None, None
|
||||
else:
|
||||
conv_state, ssm_state = cache[0], cache[1]
|
||||
|
||||
output, (conv_state, ssm_state) = self._process_sequence(
|
||||
x, conv_state, ssm_state
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = conv_state
|
||||
cache[1] = ssm_state
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class JambaSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
|
||||
self.router = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, args.num_experts
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.stop_gradient(mx.argpartition(-gates, kth=k - 1, axis=-1)[..., :k])
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
scores = mx.softmax(scores, axis=-1, precise=True)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
return y
|
||||
|
||||
|
||||
class JambaDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_type: str, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_attn = layer_type == "attention"
|
||||
if self.is_attn:
|
||||
self.self_attn = JambaAttention(args)
|
||||
else:
|
||||
self.mamba = JambaMambaMixer(args)
|
||||
if (
|
||||
args.num_experts > 1
|
||||
and (layer_idx + args.expert_layer_offset) % args.expert_layer_period == 0
|
||||
):
|
||||
ffn_layer_class = JambaSparseMoeBlock
|
||||
else:
|
||||
ffn_layer_class = JambaMLP
|
||||
self.feed_forward = ffn_layer_class(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_ff_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
if self.is_attn:
|
||||
h = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
h = self.mamba(self.input_layernorm(x), cache)
|
||||
r = x + h
|
||||
out = r + self.feed_forward(self.pre_ff_layernorm(r))
|
||||
return out
|
||||
|
||||
|
||||
class JambaModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [
|
||||
JambaDecoderLayer(args, t, idx)
|
||||
for idx, t in enumerate(args.layers_block_type)
|
||||
]
|
||||
self.final_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.attn_idx = args.layers_block_type.index("attention")
|
||||
self.ssm_idx = args.layers_block_type.index("mamba")
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attn else ssm_mask
|
||||
h = layer(h, mask=mask, cache=c)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.model_type = args.model_type
|
||||
self.args = args
|
||||
self.model = JambaModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.model.layers:
|
||||
if layer.is_attn:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(ArraysCache(size=2))
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in list(weights.items()):
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
base = f"model.layers.{l}.feed_forward"
|
||||
if not any(key.startswith(f"{base}.experts.") for key in weights.keys()):
|
||||
continue
|
||||
|
||||
for proj in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for name in ["weight", "bias", "scales", "biases"]:
|
||||
expert_tensors = [
|
||||
weights.pop(f"{base}.experts.{e}.{proj}.{name}")
|
||||
for e in range(len(weights))
|
||||
if f"{base}.experts.{e}.{proj}.{name}" in weights
|
||||
]
|
||||
if expert_tensors:
|
||||
weights[f"{base}.switch_mlp.{proj}.{name}"] = mx.stack(
|
||||
expert_tensors
|
||||
)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .deepseek_v3 import DeepseekV3Model
|
||||
from .deepseek_v3 import Model as DeepseekV3LM
|
||||
from .deepseek_v3 import ModelArgs as TextConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
text_config: Union[TextConfig, dict]
|
||||
model_type: str = "kimi_k25"
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.text_config, dict):
|
||||
self.text_config = TextConfig.from_dict(self.text_config)
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
def __init__(self, config: TextConfig):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model = DeepseekV3Model(config)
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
return self.lm_head(out)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = config
|
||||
self.model_type = config.model_type
|
||||
self.language_model = LanguageModel(config.text_config)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
return self.language_model(inputs, cache)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights.pop("vision_model", None)
|
||||
weights.pop("multi_modal_projector", None)
|
||||
weights.pop("mm_projector", None)
|
||||
lm_weights = dict(tree_flatten(weights["language_model"]))
|
||||
lm_weights = DeepseekV3LM.sanitize(self.language_model, lm_weights)
|
||||
weights["language_model"] = tree_unflatten(list(lm_weights.items()))
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
DeepseekV3LM.shard(self.language_model, group)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self.language_model.model
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.pipeline_layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,611 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .gated_delta import gated_delta_update
|
||||
from .mla import MultiLinear
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
intermediate_size: int
|
||||
head_dim: int
|
||||
rope_theta: float
|
||||
rms_norm_eps: float
|
||||
linear_attn_config: Dict[str, Any]
|
||||
model_max_length: int
|
||||
num_experts: int
|
||||
moe_intermediate_size: int
|
||||
kv_lora_rank: int
|
||||
rope_scaling: Optional[Dict[str, Any]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
qk_nope_head_dim: Optional[int] = None
|
||||
qk_rope_head_dim: Optional[int] = None
|
||||
v_head_dim: Optional[int] = None
|
||||
mla_use_nope: bool = False
|
||||
num_experts_per_token: int = 1
|
||||
num_shared_experts: int = 0
|
||||
moe_router_activation_func: str = "sigmoid"
|
||||
moe_renormalize: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
first_k_dense_replace: int = 0
|
||||
moe_layer_freq: int = 1
|
||||
use_grouped_topk: bool = True
|
||||
num_expert_group: int = 1
|
||||
topk_group: int = 1
|
||||
|
||||
|
||||
class KimiMLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
hidden_size: Optional[int] = None,
|
||||
intermediate_size: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
dim = hidden_size or args.hidden_size
|
||||
hidden = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.up_proj = nn.Linear(dim, hidden, bias=False)
|
||||
self.down_proj = nn.Linear(hidden, dim, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
@mx.compile
|
||||
def _group_expert_select(
|
||||
gates: mx.array,
|
||||
bias: Optional[mx.array],
|
||||
top_k: int,
|
||||
n_group: int,
|
||||
topk_group: int,
|
||||
routed_scaling_factor: float,
|
||||
renormalize: bool,
|
||||
score_function: str,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if score_function == "sigmoid":
|
||||
scores = mx.sigmoid(gates)
|
||||
elif score_function == "softmax":
|
||||
scores = mx.softmax(gates, axis=-1, precise=True)
|
||||
else:
|
||||
raise ValueError(f"Unsupported MoE router activation '{score_function}'")
|
||||
|
||||
orig_scores = scores
|
||||
if bias is not None:
|
||||
scores = scores + bias.astype(scores.dtype)
|
||||
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores,
|
||||
mx.stop_gradient(group_idx),
|
||||
mx.array(0.0, dtype=scores.dtype),
|
||||
axis=-2,
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
inds = mx.argpartition(-scores, kth=top_k - 1, axis=-1)[..., :top_k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
if top_k > 1 and renormalize:
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
|
||||
return inds, scores * routed_scaling_factor
|
||||
|
||||
|
||||
class KimiSparseMoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
hidden = args.hidden_size
|
||||
experts = args.num_experts
|
||||
if experts is None:
|
||||
raise ValueError("num_experts must be specified for MoE layers")
|
||||
|
||||
self.gate = nn.Linear(hidden, experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(hidden, args.moe_intermediate_size, experts)
|
||||
self.e_score_correction_bias = mx.zeros((experts,), dtype=mx.float32)
|
||||
|
||||
if args.num_shared_experts:
|
||||
shared_hidden = args.moe_intermediate_size * args.num_shared_experts
|
||||
self.shared_experts = KimiMLP(args, intermediate_size=shared_hidden)
|
||||
else:
|
||||
self.shared_experts = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
scores = self.gate(x)
|
||||
inds, weights = _group_expert_select(
|
||||
scores,
|
||||
self.e_score_correction_bias,
|
||||
self.args.num_experts_per_token,
|
||||
self.args.num_expert_group,
|
||||
self.args.topk_group,
|
||||
self.args.routed_scaling_factor,
|
||||
self.args.moe_renormalize,
|
||||
self.args.moe_router_activation_func,
|
||||
)
|
||||
out = self.switch_mlp(x, inds)
|
||||
out = (out * weights[..., None]).sum(axis=-2)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
|
||||
class KimiMLAAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.qk_nope_head_dim = args.qk_nope_head_dim or args.head_dim
|
||||
self.qk_rope_head_dim = args.qk_rope_head_dim or 0
|
||||
self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
self.v_head_dim = args.v_head_dim or args.head_dim
|
||||
self.kv_lora_rank = args.kv_lora_rank
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
hidden = args.hidden_size
|
||||
self.q_proj = nn.Linear(hidden, self.num_heads * self.q_head_dim, bias=False)
|
||||
self.kv_a_proj_with_mqa = nn.Linear(
|
||||
hidden,
|
||||
args.kv_lora_rank + self.qk_rope_head_dim,
|
||||
bias=False,
|
||||
)
|
||||
self.kv_a_layernorm = nn.RMSNorm(args.kv_lora_rank, eps=args.rms_norm_eps)
|
||||
self.embed_q = MultiLinear(
|
||||
self.qk_nope_head_dim, args.kv_lora_rank, self.num_heads
|
||||
)
|
||||
self.unembed_out = MultiLinear(
|
||||
args.kv_lora_rank, self.v_head_dim, self.num_heads
|
||||
)
|
||||
self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, hidden, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[KVCache] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
q = self.q_proj(x).reshape(B, L, self.num_heads, self.q_head_dim)
|
||||
q = q.transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv_latent = self.kv_a_layernorm(compressed_kv)
|
||||
|
||||
kv_latent = mx.expand_dims(kv_latent, axis=1)
|
||||
|
||||
if cache is not None:
|
||||
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
||||
|
||||
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
||||
if mask is not None:
|
||||
pe_scores = mx.where(
|
||||
mask,
|
||||
pe_scores,
|
||||
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
q_nope = self.embed_q(q_nope)
|
||||
k = v = kv_latent
|
||||
else:
|
||||
k = self.embed_q(kv_latent, transpose=False)
|
||||
v = self.unembed_out(kv_latent)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
||||
)
|
||||
|
||||
if L == 1:
|
||||
output = self.unembed_out(output)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class ShortConv1d(nn.Module):
|
||||
def __init__(self, channels: int, kernel_size: int):
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=channels,
|
||||
out_channels=channels,
|
||||
kernel_size=kernel_size,
|
||||
bias=False,
|
||||
groups=channels,
|
||||
padding=0,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
state: Optional[mx.array],
|
||||
mask: Optional[mx.array],
|
||||
lengths: Optional[mx.array],
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if mask is not None:
|
||||
x = mx.where(mask[..., None], x, 0)
|
||||
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(x.shape[0], self.kernel_size - 1, x.shape[-1]), dtype=x.dtype
|
||||
)
|
||||
conv_input = mx.concatenate([state, x], axis=1)
|
||||
out = nn.silu(self.conv(conv_input))
|
||||
n_keep = self.kernel_size - 1
|
||||
if lengths is not None:
|
||||
ends = mx.clip(lengths, 0, x.shape[1])
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
new_state = mx.take_along_axis(conv_input, positions, axis=1)
|
||||
else:
|
||||
new_state = mx.contiguous(conv_input[:, -n_keep:, :])
|
||||
|
||||
return out, new_state
|
||||
|
||||
|
||||
class KimiDeltaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
cfg = args.linear_attn_config
|
||||
|
||||
self.layer_idx = layer_idx
|
||||
self.num_heads = cfg["num_heads"]
|
||||
self.head_dim = cfg["head_dim"]
|
||||
self.conv_kernel = cfg.get("short_conv_kernel_size", 4)
|
||||
|
||||
self.projection_dim = self.num_heads * self.head_dim
|
||||
hidden = args.hidden_size
|
||||
|
||||
self.scale = float(self.head_dim) ** -0.5
|
||||
|
||||
self.q_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.k_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
self.v_proj = nn.Linear(hidden, self.projection_dim, bias=False)
|
||||
|
||||
self.q_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.k_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
self.v_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
||||
|
||||
self.f_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.f_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
self.b_proj = nn.Linear(hidden, self.num_heads, bias=False)
|
||||
|
||||
self.g_a_proj = nn.Linear(hidden, self.head_dim, bias=False)
|
||||
self.g_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
||||
|
||||
self.A_log = mx.expand_dims(
|
||||
mx.log(mx.random.uniform(low=1.0, high=16.0, shape=(self.num_heads,))),
|
||||
(0, 1, 3),
|
||||
)
|
||||
self.dt_bias = mx.zeros((self.projection_dim,))
|
||||
|
||||
self.o_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.o_proj = nn.Linear(self.projection_dim, hidden, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, T, _ = x.shape
|
||||
dtype = x.dtype
|
||||
|
||||
if cache is not None:
|
||||
q_state, k_state, v_state, ssm_state = cache
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
q_state = None
|
||||
k_state = None
|
||||
v_state = None
|
||||
ssm_state = None
|
||||
lengths = None
|
||||
|
||||
if q_state is None:
|
||||
s = mx.zeros((B, self.conv_kernel - 1, self.projection_dim), dtype=dtype)
|
||||
q_state = s
|
||||
k_state = s
|
||||
v_state = s
|
||||
|
||||
q_conv, q_state = self.q_conv(self.q_proj(x), q_state, mask, lengths)
|
||||
k_conv, k_state = self.k_conv(self.k_proj(x), k_state, mask, lengths)
|
||||
v_conv, v_state = self.v_conv(self.v_proj(x), v_state, mask, lengths)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = q_state
|
||||
cache[1] = k_state
|
||||
cache[2] = v_state
|
||||
|
||||
q = q_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
k = k_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
v = v_conv.reshape(B, T, self.num_heads, self.head_dim)
|
||||
|
||||
inv_scale = self.scale
|
||||
q = (inv_scale**2) * mx.fast.rms_norm(q, None, 1e-6)
|
||||
k = inv_scale * mx.fast.rms_norm(k, None, 1e-6)
|
||||
|
||||
a_logits = self.f_b_proj(self.f_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
b_logits = self.b_proj(x).reshape(B, T, self.num_heads)
|
||||
|
||||
out, ssm_state = gated_delta_update(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a_logits,
|
||||
b_logits,
|
||||
self.A_log.reshape(self.num_heads, 1),
|
||||
self.dt_bias.reshape(self.num_heads, self.head_dim),
|
||||
state=ssm_state,
|
||||
mask=mask,
|
||||
use_kernel=not self.training,
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[3] = ssm_state
|
||||
cache.advance(T)
|
||||
|
||||
gate = self.g_b_proj(self.g_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
)
|
||||
out = (
|
||||
self.o_norm(out.reshape(B, T, self.num_heads, self.head_dim))
|
||||
* mx.sigmoid(gate)
|
||||
).reshape(B, T, -1)
|
||||
return self.o_proj(out)
|
||||
|
||||
|
||||
class KimiDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.is_linear = (layer_idx + 1) in kda_layers
|
||||
|
||||
if self.is_linear:
|
||||
self.self_attn = KimiDeltaAttention(args, layer_idx)
|
||||
else:
|
||||
self.self_attn = KimiMLAAttention(args)
|
||||
|
||||
if (
|
||||
args.num_experts > 0
|
||||
and layer_idx >= args.first_k_dense_replace
|
||||
and layer_idx % args.moe_layer_freq == 0
|
||||
):
|
||||
self.mlp = KimiSparseMoE(args)
|
||||
else:
|
||||
self.mlp = KimiMLP(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
attn_cache = None if cache is None else cache
|
||||
y = self.self_attn(self.input_layernorm(x), mask, attn_cache)
|
||||
h = x + y
|
||||
z = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + z
|
||||
|
||||
|
||||
class KimiLinearModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [KimiDecoderLayer(args, i) for i in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
kda_layers = args.linear_attn_config["kda_layers"]
|
||||
self.ssm_idx = kda_layers[0] - 1
|
||||
for i in range(len(self.layers)):
|
||||
if (i + 1) not in kda_layers:
|
||||
self.attn_idx = i
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx], return_array=True)
|
||||
|
||||
for layer, layer_cache in zip(self.layers, cache):
|
||||
mask = ssm_mask if layer.is_linear else attn_mask
|
||||
h = layer(h, mask=mask, cache=layer_cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = KimiLinearModel(args)
|
||||
if args.tie_word_embeddings:
|
||||
self.lm_head = None
|
||||
else:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.lm_head is None:
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
return self.lm_head(out)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
caches: List[Any] = []
|
||||
for layer in self.layers:
|
||||
if layer.is_linear:
|
||||
caches.append(ArraysCache(size=4))
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
||||
weights = {k: v for k, v in weights.items() if not k.startswith("model.mtp")}
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
prefix = f"model.layers.{layer_idx}"
|
||||
|
||||
if isinstance(layer.mlp, KimiSparseMoE):
|
||||
src_prefix = f"{prefix}.block_sparse_moe"
|
||||
dst_prefix = f"{prefix}.mlp"
|
||||
for src, dst in [
|
||||
("w1", "gate_proj"),
|
||||
("w2", "down_proj"),
|
||||
("w3", "up_proj"),
|
||||
]:
|
||||
key = f"{src_prefix}.experts.0.{src}.weight"
|
||||
if key in weights:
|
||||
stacked = [
|
||||
weights.pop(f"{src_prefix}.experts.{i}.{src}.weight")
|
||||
for i in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{dst_prefix}.switch_mlp.{dst}.weight"] = mx.stack(
|
||||
stacked
|
||||
)
|
||||
|
||||
for name in ("gate_proj", "up_proj", "down_proj"):
|
||||
src_key = f"{src_prefix}.shared_experts.{name}.weight"
|
||||
if src_key in weights:
|
||||
weights[f"{dst_prefix}.shared_experts.{name}.weight"] = (
|
||||
weights.pop(src_key)
|
||||
)
|
||||
|
||||
gate_key = f"{src_prefix}.gate.weight"
|
||||
if gate_key in weights:
|
||||
weights[f"{dst_prefix}.gate.weight"] = weights.pop(gate_key)
|
||||
|
||||
bias_key = f"{src_prefix}.gate.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{dst_prefix}.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
attn = getattr(layer, "self_attn", None)
|
||||
if isinstance(attn, KimiDeltaAttention):
|
||||
attn_prefix = f"{prefix}.self_attn"
|
||||
for src_name, dst_name in (
|
||||
("q_conv1d", "q_conv"),
|
||||
("k_conv1d", "k_conv"),
|
||||
("v_conv1d", "v_conv"),
|
||||
):
|
||||
src_key = f"{attn_prefix}.{src_name}.weight"
|
||||
if src_key in weights:
|
||||
w = weights.pop(src_key)
|
||||
if w.ndim == 3:
|
||||
w = w.moveaxis(2, 1)
|
||||
weights[f"{attn_prefix}.{dst_name}.conv.weight"] = w
|
||||
dt_key = f"{attn_prefix}.dt_bias"
|
||||
if dt_key in weights:
|
||||
if weights[dt_key].ndim > 1:
|
||||
weights[dt_key] = mx.reshape(weights[dt_key], (-1,))
|
||||
|
||||
attn_prefix = f"{prefix}.self_attn"
|
||||
kv_b_key = f"{attn_prefix}.kv_b_proj.weight"
|
||||
if kv_b_key in weights:
|
||||
qk_nope = self.args.qk_nope_head_dim or self.args.head_dim
|
||||
v_head = self.args.v_head_dim or self.args.head_dim
|
||||
head_dim = qk_nope + v_head
|
||||
num_heads = self.args.num_attention_heads
|
||||
|
||||
quantized = f"{attn_prefix}.kv_b_proj.scales" in weights
|
||||
v = weights.pop(kv_b_key)
|
||||
|
||||
if quantized:
|
||||
dims = self.args.kv_lora_rank
|
||||
scales = weights.pop(f"{attn_prefix}.kv_b_proj.scales")
|
||||
biases = weights.pop(f"{attn_prefix}.kv_b_proj.biases")
|
||||
bits = (v.shape[-1] * 32) // dims
|
||||
group_size = dims // scales.shape[-1]
|
||||
v = mx.dequantize(
|
||||
v, scales, biases, bits=bits, group_size=group_size
|
||||
)
|
||||
|
||||
v = v.reshape(num_heads, head_dim, -1)
|
||||
wk = mx.contiguous(v[:, :qk_nope, :].swapaxes(-1, -2))
|
||||
wv = mx.contiguous(v[:, qk_nope:, :])
|
||||
|
||||
if quantized:
|
||||
wk, wk_s, wk_b = mx.quantize(wk, bits=bits, group_size=group_size)
|
||||
wv, wv_s, wv_b = mx.quantize(wv, bits=bits, group_size=group_size)
|
||||
weights[f"{attn_prefix}.embed_q.scales"] = wk_s
|
||||
weights[f"{attn_prefix}.embed_q.biases"] = wk_b
|
||||
weights[f"{attn_prefix}.unembed_out.scales"] = wv_s
|
||||
weights[f"{attn_prefix}.unembed_out.biases"] = wv_b
|
||||
|
||||
weights[f"{attn_prefix}.embed_q.weight"] = wk
|
||||
weights[f"{attn_prefix}.unembed_out.weight"] = wv
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(path: str):
|
||||
if "e_score_correction_bias" in path:
|
||||
return False
|
||||
if path.endswith("A_log") or path.endswith("dt_bias"):
|
||||
return False
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("mlp.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import lfm2
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
text_config: dict
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = lfm2.Model(lfm2.ModelArgs.from_dict(args.text_config))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights.pop("multi_modal_projector", None)
|
||||
return dict(tree_flatten(weights))
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return self.language_model.make_cache()
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
|
||||
|
||||
@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 = 1000000.0
|
||||
rope_parameters: Optional[dict] = None
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
self.k_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.out_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_layernorm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_layernorm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, mask=mask, scale=self.scale
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
class ShortConv(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
layer_idx: int,
|
||||
):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.layer_idx = layer_idx
|
||||
self.L_cache = args.conv_L_cache
|
||||
self.bias = args.conv_bias
|
||||
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=args.hidden_size,
|
||||
out_channels=args.hidden_size,
|
||||
kernel_size=self.L_cache,
|
||||
groups=args.hidden_size,
|
||||
bias=self.bias,
|
||||
)
|
||||
self.in_proj = nn.Linear(args.hidden_size, 3 * args.hidden_size, bias=self.bias)
|
||||
self.out_proj = nn.Linear(args.hidden_size, args.hidden_size, bias=self.bias)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size),
|
||||
dtype=Bx.dtype,
|
||||
)
|
||||
else:
|
||||
state = cache[0]
|
||||
Bx = mx.concatenate([state, Bx], axis=1)
|
||||
n_keep = self.L_cache - 1
|
||||
t = x.shape[1]
|
||||
if cache.lengths is not None:
|
||||
ends = mx.clip(cache.lengths, 0, t)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(Bx, positions, axis=1)
|
||||
else:
|
||||
cache[0] = Bx[:, -n_keep:, :]
|
||||
cache.advance(t)
|
||||
else:
|
||||
Bx = mx.pad(Bx, [(0, 0), (self.L_cache - 1, 0), (0, 0)])
|
||||
|
||||
conv_out = self.conv(Bx)
|
||||
|
||||
y = C * conv_out
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
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(swiglu(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
|
||||
]
|
||||
@@ -0,0 +1,387 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
use_expert_bias: bool
|
||||
num_dense_layers: int
|
||||
norm_eps: float
|
||||
conv_bias: bool
|
||||
conv_L_cache: int
|
||||
rope_theta: float = 1000000.0
|
||||
rope_parameters: Optional[dict] = None
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
self.k_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.out_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_layernorm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_layernorm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, mask=mask, scale=self.scale
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
class ShortConv(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
layer_idx: int,
|
||||
):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.layer_idx = layer_idx
|
||||
self.L_cache = args.conv_L_cache
|
||||
self.bias = args.conv_bias
|
||||
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=args.hidden_size,
|
||||
out_channels=args.hidden_size,
|
||||
kernel_size=self.L_cache,
|
||||
groups=args.hidden_size,
|
||||
bias=self.bias,
|
||||
)
|
||||
self.in_proj = nn.Linear(args.hidden_size, 3 * args.hidden_size, bias=self.bias)
|
||||
self.out_proj = nn.Linear(args.hidden_size, args.hidden_size, bias=self.bias)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
|
||||
if cache is not None:
|
||||
if cache[0] is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size),
|
||||
dtype=Bx.dtype,
|
||||
)
|
||||
else:
|
||||
state = cache[0]
|
||||
Bx = mx.concatenate([state, Bx], axis=1)
|
||||
n_keep = self.L_cache - 1
|
||||
t = x.shape[1]
|
||||
if cache.lengths is not None:
|
||||
ends = mx.clip(cache.lengths, 0, t)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(Bx, positions, axis=1)
|
||||
else:
|
||||
cache[0] = Bx[:, -n_keep:, :]
|
||||
cache.advance(t)
|
||||
else:
|
||||
Bx = mx.pad(Bx, [(0, 0), (self.L_cache - 1, 0), (0, 0)])
|
||||
|
||||
conv_out = self.conv(Bx)
|
||||
|
||||
y = C * conv_out
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Lfm2MoeSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
intermediate_size = args.moe_intermediate_size
|
||||
|
||||
self.num_experts = num_experts = args.num_experts
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.use_expert_bias = args.use_expert_bias
|
||||
|
||||
self.gate = nn.Linear(dim, num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(dim, intermediate_size, num_experts)
|
||||
if self.use_expert_bias:
|
||||
self.expert_bias = mx.zeros((self.num_experts,))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
):
|
||||
gates = self.gate(x).astype(mx.float32)
|
||||
gates = mx.softmax(gates, axis=-1)
|
||||
|
||||
if self.use_expert_bias:
|
||||
gates += self.expert_bias
|
||||
|
||||
k = self.top_k
|
||||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||||
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
if self.norm_topk_prob:
|
||||
scores /= mx.sum(scores, axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores.astype(x.dtype)
|
||||
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Lfm2DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_attention_layer = layer_idx in args.full_attn_idxs
|
||||
|
||||
if self.is_attention_layer:
|
||||
self.self_attn = Attention(args)
|
||||
else:
|
||||
self.conv = ShortConv(args, layer_idx)
|
||||
self.feed_forward = (
|
||||
MLP(
|
||||
config=args,
|
||||
intermediate_size=args.intermediate_size,
|
||||
)
|
||||
if layer_idx < args.num_dense_layers
|
||||
else Lfm2MoeSparseMoeBlock(args)
|
||||
)
|
||||
|
||||
self.operator_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
self.ffn_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
|
||||
if self.is_attention_layer:
|
||||
r = self.self_attn(self.operator_norm(x), mask=mask, cache=cache)
|
||||
else:
|
||||
r = self.conv(
|
||||
self.operator_norm(x),
|
||||
mask=mask,
|
||||
cache=cache,
|
||||
)
|
||||
h = x + r
|
||||
out = h + self.feed_forward(self.ffn_norm(h))
|
||||
return out
|
||||
|
||||
|
||||
class Lfm2Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Lfm2DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
|
||||
self.embedding_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
self.fa_idx = args.full_attn_idxs[0]
|
||||
self.conv_idx = 0
|
||||
for i in range(args.num_hidden_layers):
|
||||
if i in args.full_attn_idxs:
|
||||
self.conv_idx += 1
|
||||
else:
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
conv_mask = create_ssm_mask(h, cache[self.conv_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attention_layer else conv_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.embedding_norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Lfm2Model(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
sanitized_weights = {}
|
||||
for name, param in weights.items():
|
||||
if "conv.weight" in name:
|
||||
if param.shape[-1] > param.shape[1]:
|
||||
param = param.transpose(0, 2, 1)
|
||||
replacements = {
|
||||
"w1.weight": "gate_proj.weight",
|
||||
"w2.weight": "down_proj.weight",
|
||||
"w3.weight": "up_proj.weight",
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
if old in name:
|
||||
name = name.replace(old, new)
|
||||
sanitized_weights[name] = param
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Only sanitize MoE layer weights
|
||||
for n in ["gate_proj", "down_proj", "up_proj"]:
|
||||
if f"{prefix}.feed_forward.experts.0.{n}.weight" in sanitized_weights:
|
||||
to_join = [
|
||||
sanitized_weights.pop(
|
||||
f"{prefix}.feed_forward.experts.{e}.{n}.weight"
|
||||
)
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
sanitized_weights[
|
||||
f"{prefix}.feed_forward.switch_mlp.{n}.weight"
|
||||
] = mx.stack(to_join)
|
||||
return sanitized_weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
KVCache() if l.is_attention_layer else ArraysCache(size=1)
|
||||
for l in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("feed_forward.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
@@ -0,0 +1,155 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@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(swiglu(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}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user