Compare commits

...

299 Commits

Author SHA1 Message Date
Anastasiia Filippova 83c408e1e6 log only on rank 0 2026-05-26 20:57:22 +02:00
Anastasiia Filippova 5b56e28af8 only light theme for now 2026-05-26 16:15:00 +02:00
Anastasiia Filippova c12cb31faa fix distibuted probing for the terminal theme 2026-05-26 15:55:57 +02:00
Anastasiia Filippova be13b3f735 UI for lora and chat 2026-05-26 13:49:35 +02:00
Arun Raj df1d3f3c9a Fix Gemma 4 sanitize() not stripping KV projections for shared layers (#1240) 2026-05-04 15:26:18 -07:00
Angelos Katharopoulos ed1fca4cef Thread local generation stream (#1090) 2026-04-22 00:34:09 -07:00
glyphVault 4f5cbd2a4f Fix Gemma 4 KV-shared layers creating unused projections (#1158) 2026-04-21 16:44:13 -07:00
Angelos Katharopoulos 3cd9a52df2 Fix ArraysCache extend (#1177) 2026-04-21 16:41:49 -07:00
Eyüp Can Akman 2f1ab85aec Fix Mistral empty tool_call_end flipping state machine to normal (#1151) 2026-04-21 01:36:56 -07:00
AkashKhamkar f3bb10c141 Fix Gemma4 tool parser: support hyphenated names and braces in strings (#1150) 2026-04-21 01:15:58 -07:00
Sherry Lo e1c24b3237 fix: handle NoneType check for think tokens in TokenizerWrapper (#1167) 2026-04-21 01:13:15 -07:00
Luis Molina f39cb8e934 Fix dwq: check for actual safetensors in target_dir (#1173) 2026-04-21 00:41:53 -07:00
TechToboggan a9856b485d Fix batch dimension mismatch in ArraysCache extend() (#1169)
Co-authored-by: Tristan Stahnke <tristan@melchior.lan>
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-04-20 23:42:03 -07:00
Andrei Panferov e92138cb01 Apertus tie_word_embeddings fix (#1143) 2026-04-20 23:47:40 +02:00
Siiea-ai a401730941 Fix missing tree_reduce import in models/cache.py (#1165) 2026-04-20 11:31:58 -07:00
Tarjei Mandt 6d114686e5 Fix MiniMax M2 parallel tool calling (#1171) 2026-04-20 10:57:20 -07:00
Tarjei Mandt aa4f880fb3 Fix parallel tool call handling in server (#1170) 2026-04-19 23:58:25 -07:00
razorback16 62f38aeb51 Fix batch dimension mismatch in BatchKVCache and BatchRotatingKVCache extend() (#1141) 2026-04-14 17:21:31 -07:00
Angelos Katharopoulos d9c63fff67 Bump the patch version (#1124) 2026-04-08 02:04:27 -07:00
Neil Mehta dcbf6e33d1 Align batch logits processor token contract (#1115) 2026-04-06 18:07:35 -07:00
Angelos Katharopoulos f26fddfd3b Gemma4 final fixes and multi-token think/tool start/end (#1114) 2026-04-06 17:40:48 -07:00
Tarjei Mandt f56d99712c Fix output corruption in speculative decoding (#1109) 2026-04-06 16:00:38 -07:00
spicyneuron c65c27b450 Fix Gemma 4 quantized per-layer projection loading (#1112) 2026-04-06 13:26:07 -07:00
Nic Davidson 3257c3df17 Add Gemma 4 tool call parser (#1105) 2026-04-04 17:21:45 -07:00
Angelos Katharopoulos d4eb136d44 Bring back max-kv-size to the batch generator (#1106) 2026-04-04 17:12:43 -07:00
Prince Canuma 4469ad4647 Add gemma 4 (#1093)
Co-authored-by: N8 <n8@n8programs.com>
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-04-04 04:47:03 -07:00
Matteo Celani f79dba7832 perf: use max instead of argsort in apply_min_p sampling (#1083) 2026-04-01 16:26:59 -07:00
Angelos Katharopoulos 3f9d179fd1 Batch generation refactoring and various fixes (#1072) 2026-04-01 15:07:50 -07:00
Lik Xun Yuan (Lx) 9dc023beed Fix PromptTrie.pop_prefixes() off-by-one when pruning immediate prefixes (#1078)
Signed-off-by: Yuan Lik Xun <lxyuan0420@gmail.com>
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-04-01 11:51:19 -07:00
Adam Durham 9dcefa5272 fix: break shared-buffer memory leak in GatedDeltaNet cache (#1077)
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-03-31 21:27:11 -07:00
Arthur Hjorth bdeac59767 Inserting logits processors into BatchGenerator in batch_generate (#1008)
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-03-31 16:48:13 -07:00
Tarjei Mandt 6ddfdda1ac Fix SSM dt clamp default for Nemotron-H (#1026) 2026-03-30 04:19:27 -07:00
Angelos Katharopoulos 4d3af3cebc Refactor LRUPromptCache (#1019) 2026-03-26 10:04:22 -07:00
Angelos Katharopoulos ed7884cb80 Fix missing cache advance from qwen 3.5 (#1024) 2026-03-19 17:20:38 -07:00
Angelos Katharopoulos f8019f7769 Fix flaky test (#1020) 2026-03-18 12:51:22 -07:00
AndreasPlt 564281f793 Supporting delay in mlx_lm benchmark (#1010) 2026-03-16 17:43:37 -07:00
Angelos Katharopoulos 73c8550478 Nemotron super support (#992) 2026-03-16 10:59:14 -07:00
mm65x ed69f837e6 fall back to ast.literal_eval for malformed JSON in qwen3_coder tool parser (#1004) 2026-03-15 23:00:51 -07:00
mm65x cc393b2862 Handle missing content-length header in server (#1001) 2026-03-15 19:40:29 -07:00
mm65x 2146e4ed18 avoid mutating input in SuScaledRoPE and YarnRoPE (#1003) 2026-03-15 18:13:47 -07:00
Angelos Katharopoulos 735a43b275 Delta net precision (#997) 2026-03-15 15:39:18 -07:00
Ryo Ota 332d94ca6f Add allowed-origins to the server (#987) 2026-03-13 19:22:23 -07:00
n8programs 480934402d Clear cache trainer memory (#986)
Co-authored-by: N8 <n8@n8programs.com>
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-03-13 18:19:53 -07:00
Angelos Katharopoulos ab157c2d18 Fix test after latest MLX update (#996) 2026-03-13 17:07:05 -07:00
Angelos Katharopoulos 5a8ced697e Bump the patch version (#981) 2026-03-10 23:27:59 -07:00
Eyüp Can Akman 760c5abcc8 Fix CompletionsDataset mask_prompt crash (#967) 2026-03-10 18:10:06 -07:00
Angelos Katharopoulos 43ee5455d3 Move to metal agnostic device_info (#979) 2026-03-10 17:41:33 -07:00
Angelos Katharopoulos 23af85703e Late binding caused incorrect cache checkpoint (#976) 2026-03-10 13:53:10 -07:00
rltakashige 89c430a9c2 Eval self.left_padding whenever it is updated in BatchRotatingKVCache (#960)
Co-authored-by: Angelos Katharopoulos <a_katharopoulos@apple.com>
2026-03-10 03:14:31 -07:00
Angelos Katharopoulos 4a21ffdf4b Presence and frequency penalties (#971) 2026-03-09 22:26:39 -07:00
Angelos Katharopoulos 852119b774 Bump the patch version (#959) 2026-03-08 18:04:52 -07:00
Angelos Katharopoulos 044474bc80 Adds tensor parallelism for Qwen 3.5 (#957) 2026-03-06 19:44:53 -08:00
Angelos Katharopoulos 2105aaf9c3 Better caching in the server (#911) 2026-03-06 13:42:56 -08:00
Angelos Katharopoulos cff7273a55 Ensure normalization does not promote to fp32 (#951) 2026-03-06 13:42:10 -08:00
Angelos Katharopoulos fc7d84448b Bump the version (#954) 2026-03-06 13:41:47 -08:00
spicyneuron 47be7150a6 fix: convert() uses incorrect defaults for quantization mode (#935) 2026-03-05 17:02:34 -08:00
Yongyue Sun 35fa620279 Add --prefill-step-size as cmd line argument (#943) 2026-03-04 17:40:01 -08:00
Noah Lyons 8162aaad56 step3p5: use rotating cache for sliding attention layers (#949) 2026-03-04 17:17:29 -08:00
Awni Hannun 834fac934c fix qwen3.5 sanitize (#928) 2026-02-24 17:04:43 -08:00
Awni Hannun 179da774b1 Clear the cache during batch generation (#926) 2026-02-23 19:50:35 -08:00
Awni Hannun 720f2369ba add tokens to eval to avoid large graphs when they are not used (#924) 2026-02-23 14:38:08 -08:00
Flynn 65725dcec2 Add filter guard to list comprehension (#918) 2026-02-23 14:22:54 -08:00
n8programs d4701ba513 clear cache on prompt ingestion in server (#917)
Co-authored-by: N8 <n8@n8programs.com>
2026-02-23 12:13:25 -08:00
Angelos Katharopoulos 321e764e0a Make the cache limits more friendly (#910) 2026-02-19 13:52:09 -08:00
Angelos Katharopoulos 83ff9c96d5 Improve the cache size limits (#906) 2026-02-19 10:13:48 -08:00
Yuri Khrustalev 9c113f7019 Allow reading LFM2 models nested rope params (#908)
Co-authored-by: yuri <yuri@liquid-macstudio-2.local>
2026-02-18 16:25:54 -08:00
Gökdeniz Gülmez 7d6c5e4af7 Add tie_word_embeddings modulars in mistral and qwen3 moe (#889)
* Add tie_word_embeddings option and update model call logic in Mixtral and Qwen3 models

* Update copyright year to 2026 and modify input handling in Mixtral and Qwen3 models
2026-02-18 11:23:40 -08:00
Awni Hannun ad067ea627 bump for next version (#904) 2026-02-17 07:39:58 -08:00
Angelos Katharopoulos d7b91e80f0 Fix sharded rms norm in MiniMax M2.5 (#898) 2026-02-16 17:20:07 -08:00
Awni Hannun 1fd521c3c7 fix qwen3.5 casting to fp32 (#902) 2026-02-16 10:23:31 -08:00
Ryan Goulden 572ada278c server: add usage.prompt_tokens_details.cached_tokens to json response (#849) 2026-02-16 08:37:35 -08:00
Ivan Fioravanti fb47f8fb99 Add the trust remote code option to mlx_lm perplexity (#896) 2026-02-15 20:43:23 -08:00
Tarjei Mandt 7a720882a7 Add JoyAI LLM Flash (#894) 2026-02-15 08:06:44 -08:00
spicyneuron 014ebc6a46 Fix mixed quant predicates for MLA models (#892) 2026-02-15 02:44:01 -08:00
Angelos Katharopoulos c6d9d3c9f5 Share model (#871) 2026-02-13 15:48:37 -08:00
Angelos Katharopoulos bcf630614f Fix save/load of CacheList (#886) 2026-02-12 18:41:48 -08:00
Gökdeniz Gülmez 1974376d70 Add GLM5 (#867)
* Add GLM4 MoE DSA model implementation with configurable parameters

* Update Acknowledgments to include GLM4 MoE DSA support

* format

* update ackn.

* Fixes

* Update acknowledgments to include contributions for GLM MoE DSA and additional architectures

* use dsv32 for glm5

* fix

* Fix rope theta

---------

Co-authored-by: Tarjei Mandt <kernelpool@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-12 10:11:16 -08:00
Tarjei Mandt 7e67225e1d Faster DSV32 generation (#885) 2026-02-12 07:35:20 -08:00
JJJYmmm 0fd3126496 [MODEL] support qwen3.5 series w/o vision (#869)
* support text-only qwen3.5 series

Co-authored-by: johnmai-dev <johnmai-dev@users.noreply.github.com>

* add test

* fix sanitize and add test

* make it more readable

* fix lint

---------

Co-authored-by: johnmai-dev <johnmai-dev@users.noreply.github.com>
2026-02-12 07:23:51 -08:00
Tarjei Mandt ca0d1c9630 LongCat MLA (#868)
* LongCat MLA

* Fix comment

* Remove workaround
2026-02-12 06:54:44 -08:00
Awni Hannun 82edd51a1e Devstral tool parser (#874) 2026-02-11 09:41:23 -08:00
Gökdeniz Gülmez aca4c149a1 Make validation set optional in training process (#857) 2026-02-10 15:24:44 -08:00
Tarjei Mandt 8f1c56ec83 Fix DeepSeek V3.2 indexer and weight loading (#866) 2026-02-10 12:15:12 -08:00
viktike 84ae19e675 Pythonic tool calling for LFM2 models (#864)
* Fix tool calling 404 Error: content with non-thinking (instruct) models.

* Add pythonic style tool call parser for LFM2

* test + format

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-09 15:23:07 -08:00
Awni Hannun 645a326a2e Bump version for next release (#865) 2026-02-09 14:40:57 -08:00
Tarjei Mandt fd6959dca7 Fix Kimi Linear (#853)
* Fix Kimi Linear

* Avoid concat/split

* Use fused rms_norm
2026-02-06 17:31:32 -08:00
Awni Hannun f18526f8d6 DSV3 MLA (#839)
* mla

* try to speed up prefill

* update dsv32 as well
2026-02-04 12:06:42 -08:00
Tarjei Mandt 25a4c8369e Fix sliding window mask during generation (#843)
* Fix sliding window mask during generation

* make window mask for regular cache

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-04 08:44:01 -08:00
Awni Hannun e08ec15b72 Fix batch mamba (#842)
* fix batch mamba

* remove mamba cache
2026-02-03 19:31:42 -08:00
Tarjei Mandt b77ec6b951 Fix Step 3.5 Flash model conversion (#840)
* Fix Step 3.5 Flash model conversion

* Detect converted norm weights

* Check layer names
2026-02-03 17:03:54 -08:00
Sebastian Jug ab050d1fac Deepseek V3.2 implementation fixes (#838)
* fix

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-03 07:58:29 -08:00
Josh Lehman 942b3ed4b6 fix: handle GLM 4.7 tool call fallbacks (#792)
* fix: handle glm47 tool call fallbacks

- add JSON/plain-text fallback parsing for GLM 4.7 tool calls
- normalize fallback argument values using tool schema
- add tests covering JSON/plain-text fallback paths
- Refs: n/a (pebbles not initialized)

Regeneration-Prompt: |
  Fix GLM 4.7 tool parser crash when the <arg_key> regex does not match. Keep the
  existing arg_key/arg_value parsing path intact, but add defensive fallbacks:
  first try JSON tool-call shapes (name+arguments, function+arguments, or nested
  tool objects), then a plain-text form like "name {json}" or "name key=value".
  If none parse, return a safe unknown tool with raw text in arguments. Use tool
  schema types to preserve string arguments and deserialize non-string values.
  Add tests in tests/test_tool_parsing.py that reproduce the crash with JSON
  input and verify the plain-text fallback behavior. Avoid dependency changes.

* simplify test

* rebase

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-03 06:48:22 -08:00
Ryan Goulden 11ebc98ada server: support chat_template_kwargs and top_logprobs (#829)
* server: support chat_template_kwargs and top_logprobs

* Adds support for clients sending "chat_template_kwargs",
  matching other open source LLM servers.
  This is gated behind `--trust-client-kwargs` because transformers
  does not provide any safe way to do this.

* changes the server's logprobs response to better match the OpenAI
  chat api & other open source servers.

* server: fix response when handling exceptions

* server: --client-chat-template-args whitelist

* simplify

* comment

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-02-02 19:51:38 -08:00
Tarjei Mandt 1630f9bf16 Add Step 3.5 Flash (#836)
* Add Step 3.5 Flash

* Shard model

* Feedback
2026-02-02 18:48:46 -08:00
christian-lms b7cc3aa5e5 allow creation of BatchRotatingKVCache instead of BatchKVCache when empty cache(s) are passed to BatchGenerator (#834) 2026-02-02 07:43:09 -08:00
Awni Hannun 7afcfac51a enable loading custom models (#830) 2026-01-30 19:17:14 -08:00
gaurav 1ecd27a31a fix cli (#827)
* fix cli

* nit

* fix typo in subpackages

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-30 11:36:43 -08:00
Angelos Katharopoulos 1fe1b3c901 Support distributed inference in the server (#741) 2026-01-30 11:03:55 -08:00
Awni Hannun 8a0f3781e9 fix mixed quant (#825) 2026-01-29 16:54:47 -08:00
Tarjei Mandt 56b8c0f383 Add LongCat Flash Lite (#819)
* Add LongCat Flash Lite

* Fix integer overflow

* Reuse LongCat Flash components

* proper cache

* use arrays cache

* add test for longcat

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-29 13:31:18 -08:00
Awni Hannun ad9434bef0 actually add cli (#823) 2026-01-29 13:30:54 -08:00
Tarjei Mandt 04fd7ccb97 Fix Kimi K2.5 tool call handling (#821)
* Fix Kimi K2 tool call handling

* Add unit test
2026-01-29 12:35:51 -08:00
Inferencer 7f1b7fe6bc Fix for Exception - MultiLinear.to_quantized() missing 'mode' (#809)
* Fix for Exception - MultiLinear.to_quantized() missing 'mode'

Add mode parameter to mixed_quant_predicate_builder as MLX now requires mode to be specified for nn.quantize class_predicate

* nit

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-29 07:23:35 -08:00
Luqman c645a55582 Fix NemotronH config compatibility with HuggingFace format (#820)
HuggingFace's NemotronH config uses separate `time_step_min` and
`time_step_max` fields, but mlx-lm expected a `time_step_limit` tuple.
This caused loading failures since `time_step_limit` was required but
never populated from the config.

- Make `time_step_limit` optional with default None
- Add `time_step_min` and `time_step_max` optional fields
- Add `__post_init__` to construct tuple from separate fields
2026-01-29 07:23:27 -08:00
Awni Hannun 1bdc8afca3 Bump mlx version and version (#816)
* bump mlx version and version

* add cli for mlx_lm -h
2026-01-28 17:10:49 -08:00
Tarjei Mandt 96699e6dad Add Kimi-K2.5 (#813)
* Add Kimi-K2.5

* Fix pipeline generation

* Sanitize config

* Remove workaround

* Address feedback
2026-01-27 10:23:39 -08:00
Tarjei Mandt b012e1a1e9 Add LongCat Flash tool parser (#810)
* Add LongCat Flash tool parser

* Add unit tests for both xml and json formats
2026-01-26 11:51:44 -08:00
Awni Hannun f53a9b0689 Transformers v5 (#811)
* transformers 5

* Add back tokenization space cleaning
2026-01-26 11:51:30 -08:00
lpalbou beceb5c16d Fix ArraysCache.from_state not initializing left_padding and lengths (#807)
* Fix ArraysCache.from_state not initializing left_padding and lengths

The base class from_state uses __new__ to bypass __init__, but
ArraysCache.state.setter only sets self.cache, not left_padding
or lengths. This causes AttributeError when using loaded caches
with models that use MambaCache (e.g., Qwen3-Next).

The fix overrides from_state in ArraysCache to properly initialize
these attributes before setting state.

* Add test

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-25 06:46:35 -08:00
Tarjei Mandt 12073b1db4 Sync random seed across ranks in distributed chat (#801)
* Sync random seed across ranks in distributed chat

* Use seed if provided

* Set default seed
2026-01-23 07:56:20 -08:00
Luqman d9846d37cb fix: use correct variable for logprobs in batch generation (#800) 2026-01-23 07:24:39 -08:00
Awni Hannun 1b76e3d580 Allow qq ops with activation quantization (#749)
* Allow qq ops with activation quantization

* Allow qq ops with activation quantization
2026-01-22 08:59:58 -08:00
Evan Quiney e5ddaff99b add kimi tool parser (#791)
* add kimi parser

* fix + test

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-22 07:46:07 -08:00
Gökdeniz Gülmez 14b9faf1bc Adding TeleChat3 (#773)
* in. com.

* Add Telechat3 MLP and Decoder Layer implementations

* Implement Telechat3 model and decoder layer with attention mechanism

* Add support for 'telechat3-yarn' in initialize_rope function

* Update Acknowledgments and add Telechat3 model tests

* format

* fix test

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-22 07:35:58 -08:00
n8programs 1423702019 Update glm4_moe_lite To Store KV Latent in Cache (#780)
* update glm4_moe_lite

* Fix GLM4 MoE Lite RoPE and MoE layer selection

* Apply PR #780 MLA KV latent cache

* Use full KV for prefill with MLA cache

* Remove unused GLM4 MoE lite config fields

* simplify

* updates

* try and load already quantized models

---------

Co-authored-by: N8 <n8@n8programs.com>
Co-authored-by: ivanfioravanti <ivan.fioravanti@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-21 13:55:14 -08:00
Awni Hannun 5ed1e48a3c bump transformers (#746)
* bump transformers

* bump
2026-01-21 13:45:15 -08:00
Gökdeniz Gülmez 4100703f1a move Xielu Activation in Apertus to activations.py (#772)
* feat: implement XieLU activation function and integrate into Apertus model

* format
2026-01-20 06:51:16 -08:00
Ryan Goulden 8c08a46da4 server: use OpenAI compatible finish_reason (#782) 2026-01-20 06:50:37 -08:00
Maanas Verma bc891dca4c import logging as it throws no logging error in place of actual error (#778)
* import logging as it throws no logging error in place of actual error

* Update mlx_lm/utils.py

---------

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2026-01-20 06:23:24 -08:00
Ivan Fioravanti 02228601cd Add glm4 moe lite model (#776)
* Add glm4 moe lite model

* Format glm4_moe_lite with black

* version

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-19 08:13:13 -08:00
Tarjei Mandt 8daabcc7c1 Shard LongCat Flash (#771) 2026-01-18 07:21:36 -08:00
Evan Quiney cd7d9a536e Add minimax tensor sharding (#760)
* add minimax sharding

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-17 08:45:42 -08:00
Tarjei Mandt 25246632cf Fix Longcat Flash extended context support (#768) 2026-01-17 07:42:22 -08:00
Evan Quiney 6651d2e0bf Add gpt-oss sharding (#761)
* add gpt-oss sharding

* fixes

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-17 07:42:11 -08:00
AndrewTan517 43dcf2f0c0 fix: unused batch_size parameter for mlx_lm.evaluate (#762) 2026-01-17 07:29:32 -08:00
Tarjei Mandt 769069d66b Fix CacheList batching (#769)
* Fix CacheList batching

* Simplify fix

* empty as method + change len to size

---------

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2026-01-17 07:29:21 -08:00
Tarjei Mandt 5261ab85ee Fix swiglu parameter order (#767) 2026-01-17 06:13:23 -08:00
gaurav c27c94a0ff fix(falcon_h1): support tied embeddings and correct muP scaling (#764)
* fix: handle tied embeddings and muP scaling mismatch

Support models with "tie_word_embeddings": true by conditionally initializing the lm_head and reusing embed_tokens weights.

Since falcon_h1 applies disparate muP multipliers to embeddings and the language head during sanitization, a scaling correction factor (lm_head_mult / embedding_mult) is applied in the forward pass when weights are tied. This ensures mathematical correctness without duplicating weights in memory. 

Fixes ValueError: Missing 1 parameters: lm_head.weight

* simplify sanitize logic

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>

---------

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2026-01-16 07:11:18 -08:00
Thomas Lazarus fd80ac89fb Adds support for Nemotron Super 49b v1.5 (#756)
* Adds make_cache to nemotron-nas

* Fixes from PR to not use _BaseCache

* Updates __call__
2026-01-16 07:03:05 -08:00
Awni Hannun edbf61dd8b Use compiled Swiglu (#753)
* swiglu

* swiglu

* rest of files, thx codexx

* nit

* Format
2026-01-14 11:58:41 -08:00
Awni Hannun c2a716c871 Update for latest mlx (#759) 2026-01-14 11:57:44 -08:00
Ivan Fioravanti 63c9873617 Handle empty caches during batch merge (#755)
* Handle empty caches during batch merge

* add test

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-14 08:47:47 -08:00
Julian Tibble 7585c142a6 fix: Fix type hint and pydoc for batch_generate (#745)
The 'prompts' argument to batch_generate has type List[List[int]].

However, previously the type hint was wrong, and the pydoc had the
correct type but the wrong argument name.
2026-01-12 07:36:48 -08:00
Tarjei Mandt 44d12e5d6f Fix batch generation for IQuestLoopCoder model (#748)
* Fix batch generation

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-09 16:50:51 -08:00
Awni Hannun 7a86c1289e Make cache list batchable (#743)
* Make cache list batchable

* comment
2026-01-09 10:47:05 -08:00
Gia Huy Vuong a20eefd7c2 Refactor tokenizer error handling to use warnings instead of exceptio… (#744)
* Refactor tokenizer error handling to use warnings instead of exceptions for missing tool call tokens

* disable tool calling if not in vocab

* add tool call warning

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-09 07:19:33 -08:00
Tarjei Mandt 3eb6ecf2b6 Fix tools parameter in apply_chat_template call (#747) 2026-01-09 07:08:01 -08:00
Awni Hannun 39a96ab18b Add a server benchmark for continuous batching (#728) 2026-01-08 14:35:40 -08:00
Nikhil Mitra 43082feafa Make MambaCache compatible with batch generation for nemotron-h (#690)
* Make MambaCache compatible with batch generation

* fix: Support right-padding masking in ArraysCache, add tests

* almost working

* test pass

* update models + gated delta

* rebase + fix

* fix

* allow batching in server

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-08 13:42:36 -08:00
Gökdeniz Gülmez 5cce1495e0 Add extract method to ArraysCache for item retrieval (#740) 2026-01-08 06:30:20 -08:00
Awni Hannun 509f5aef89 Fix sliding window batching (#738) 2026-01-07 10:07:56 -08:00
Tarjei Mandt 0f76343ea4 Add IQuest Coder V1 Loop variant (#716)
* Add IQuest Coder V1 Loop variant

* Minor tweaks

* Fix cache population

* Clean up nested for loop

* Simplify

* Bug fix in prefill

* Address feedback

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-07 07:39:24 -08:00
Eric Curtin 298b67c755 Add AWQ/GPTQ weight transformation utilities (#730)
* Add AWQ/GPTQ weight transformation utilities

Add functions to transform AutoAWQ and GPTQ packed weights to MLX
quantization format. This includes an unpacking function for AWQ/GPTQ
weights and a transformation function that converts the quantization
format from AutoAWQ/GPTQ to MLX. The transformation handles both
symmetric and asymmetric quantization by computing biases from zero
points. Also add a test case to verify the transformation works
correctly.

Signed-off-by: Eric Curtin <eric.curtin@docker.com>

* nits

* nits

---------

Signed-off-by: Eric Curtin <eric.curtin@docker.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-06 08:50:58 -08:00
Awni Hannun 94497d5255 fix release (#733) 2026-01-05 18:31:29 -08:00
Awni Hannun 4c80c68ea6 patch (#731) 2026-01-05 16:54:13 -08:00
Awni Hannun ac8ae2c05a Improve reasoning and tool call parsing in server (#711)
* Parse reasoning in server

* redesign and start to fix tool parsing

* add function gemma

* fix

* fix

* glm47 tools

* add minimax m2 tool parser

* tool_call finish reason

* Keep model wired in the server to reduce ttft

* infer tool parser
2026-01-05 14:18:52 -08:00
Tarjei Mandt 7a4d137df6 Add K-EXAONE MoE (#719)
* Add K-EXAONE MoE

* Shard model

* Cleanup

* Fix model test
2026-01-05 08:50:33 -08:00
Tarjei Mandt 90db1e6266 Add Solar Open (#721) 2026-01-05 08:38:53 -08:00
Thomas Lazarus d3dc2e3f33 Add logits_processors support to batch_generate (#635)
* Update generate.py

* add samplers and logits processors with per example option and server support

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-04 15:19:00 -08:00
John Mai 7423bf6752 Add YoutuLLM (#720)
* Add YoutuLLM

* nits + test

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2026-01-03 09:12:55 -08:00
Tarjei Mandt 5dec49a12f Add IQuest Coder V1 (#714)
* Add IQuest Coder V1

* Shard model

* Use remapping
2026-01-01 06:17:14 -08:00
cxl-git-hub 3727e01cd7 Fix empty /v1/models response for locally loaded models (#713)
* server: expose local --model in /v1/models.
use full local path as model id for /v1/models

在加载本地模型时,/v1/models返回空列表,导致 open webui获取不到模型,没办法使用。所以这里把本地模型的全路径作为 model_id返回。

* nits

* fix test

---------

Co-authored-by: chenxilong <cxl750529174@163.com>
Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2025-12-31 14:22:00 -08:00
Molly Sophia 09579644ac Add RWKV7 (#580)
* initial support for rwkv7

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* slight modification

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* fix batch inference

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* add metal wkv kernel and fix groupnorm calculation

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* quant_predicate

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* style and format changes

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* use pre-commit to format the code

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>

* nits

* add a test

---------

Signed-off-by: Molly Sophia <mollysophia379@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-31 14:21:42 -08:00
jaycoolslm 0081085a91 chore: add model-path param flag for convert API for better clarity (#702)
* chore: add model-path param flag for convert API for better clarity

Signed-off-by: Jake Hall <jaycoolslm@gmail.com>

* chore: refactor argparse for multiple string options

Signed-off-by: Jake Hall <jaycoolslm@gmail.com>

* Update mlx_lm/convert.py

* Update README.md

---------

Signed-off-by: Jake Hall <jaycoolslm@gmail.com>
Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2025-12-31 12:30:38 -08:00
Tarjei Mandt fed582eede Fix chat template detection for models with custom tokenizers (#712) 2025-12-31 06:43:34 -08:00
Awni Hannun 7973b8cfe8 allow mxfp8 and nvfp4 (#709) 2025-12-30 09:19:36 -08:00
will-lms 7096618d50 Ignore generation_config decode errors (#708) 2025-12-29 14:03:29 -08:00
Sebastian Jug 1e0c0f3985 Fix GIL starvation in _generate thread when batch is idle (#706)
* Fix GIL starvation in _generate thread when batch is idle

After a chat completion, the batch_generator stays alive but has no in-flight
requests (batch_results is empty). The original timeout logic used get_nowait()
when batch_generator existed, causing a tight loop that starved the GIL.

This fix uses a 0.1s timeout when the batch is idle (no in-flight requests),
preventing GIL starvation while preserving batching capability.

* format

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-29 11:52:58 -08:00
Viacheslav Ivanov 68f18bae14 batch_generate fails with Phi3 (LongRoPE) when prompts have different lengths (#707)
* fix(rope): handle batched offsets in SuScaledRoPE for batch_generate

When batch_generate processes variable-length prompts with left-padding,
cache.offset becomes an array. The seq_len comparison now extracts the
max value as a scalar to determine which frequencies to use.

* only use long rope

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-29 11:52:43 -08:00
Gia Huy Vuong f5ae09a807 Enhance load_config function to check for config file existence and i… (#701)
* Enhance load_config function to check for config file existence and incorporate eos_token_id from generation_config.json if available

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-27 06:59:38 -08:00
Awni Hannun 08c8c0a5ea support minimax m2 (#700) 2025-12-26 14:02:37 -08:00
Awni Hannun a9311cca23 shard glm (#698)
* shard glm

* angelos' fix

* nit
2025-12-24 12:05:22 -08:00
Awni Hannun 9fe5f43abf custom dsv32 chat template (#693)
* custom dsv32 chat template

* use has_chat_template
2025-12-22 13:52:58 -08:00
Angelos Katharopoulos 1b2d11b5c7 Bump the version (#692) 2025-12-18 13:43:49 -08:00
Awni Hannun 657a66c5c4 revert return dict and wrap apply_chat_template (#691) 2025-12-18 13:16:44 -08:00
Awni Hannun 595fb4bdbf bump to transformer v5 (#689) 2025-12-17 16:34:51 -08:00
Angelos Katharopoulos 79a0721c9a Model parallel generation (#676) 2025-12-17 13:35:28 -08:00
Awni Hannun cc3264c22e More useful error message for unsupported batching (#687) 2025-12-17 12:30:53 -08:00
Awni Hannun a227a9e9f3 Add mimo v2 flash (#685)
* add mimo v2 flash

* add test
2025-12-17 06:50:16 -08:00
Awni Hannun cd9ca9f068 fixes for transformers v5 (#684) 2025-12-17 06:08:08 -08:00
Jinhyeok Lee 7744d0f40b fix: server busy-waiting in request queue polling (#674) 2025-12-16 14:16:34 -08:00
Awni Hannun f3ed856610 support nemotron 3 (#678)
* support nemotron 3

* fix

* bump version
2025-12-16 08:50:44 -08:00
Inferencer ede65a1484 Fix for Devstral-2 (#671)
* Fix for Devstral-2

Convert cache offset to int for mx.arange compatibility in attention scale

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-11 08:40:12 -08:00
Awni Hannun 3d3e0751a3 fix (#669) 2025-12-09 17:11:51 -08:00
Anthony 085e36e6ab Fix SuScaledRoPE (#660) 2025-12-09 07:59:28 -08:00
Angelos Katharopoulos eea2e5f5de Fix server batching condition for SSMs (#655) 2025-12-08 23:18:53 -08:00
Awni Hannun cb763947ee Fix fusion and test (#668) 2025-12-08 16:39:46 -08:00
Awni Hannun b343a0556f fix dsv32 and gemma3 (#664) 2025-12-08 16:14:10 -08:00
Awni Hannun 82dfd39ef2 default repetition penalty to 0.0 in the server (#658) 2025-12-08 16:14:00 -08:00
Awni Hannun 84996808a2 Use test data zipfile in CI (#662)
* make fewer requests in tests

* token
2025-12-08 16:13:34 -08:00
Hritik Kumar 99f8fd6cc8 fix: calling correct dequantize function (#666) 2025-12-08 13:34:42 -08:00
Awni Hannun 7d042c6124 fix for rnj-1 (#657) 2025-12-08 07:16:57 -08:00
otarkhan 0fbff353db Fix slow batch generation in server by setting wired_limit (#652) 2025-12-05 11:28:17 -08:00
Angelos Katharopoulos 0ad37e2bbf version bump (#651)
Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-03 15:30:36 -08:00
Angelos Katharopoulos 454bf9a22b Fix the release action and revert version (#650) 2025-12-03 14:23:06 -08:00
Awni Hannun 133b5d3bd7 version bump (#649) 2025-12-03 14:04:00 -08:00
Awni Hannun abc52a0a48 Add deepseek v32 (#512)
* deepseek v32

* Fix sparse token selection in deepseek v3.2 (#531)

* Fix sparse token selection in deepseek v3.2

* Fix 4D mask input handling and remove unnecessary ones array

* simplify

* Update mlx_lm/models/deepseek_v32.py

Co-authored-by: Tarjei Mandt <kernelpool@gmail.com>

* comments

---------

Co-authored-by: Tarjei Mandt <kernelpool@gmail.com>
2025-12-03 14:03:21 -08:00
Angelos Katharopoulos 6b42901468 Batching in the server (#626) 2025-12-03 13:14:18 -08:00
Awni Hannun f353e0178b fix lora fusion for non affine quantization (#647) 2025-12-03 10:33:01 -08:00
Awni Hannun f940cf3a95 fix flaky test (#643) 2025-12-02 13:33:11 -08:00
Angelos Katharopoulos 34cbb8b51a Add a prompt cache that can hold multiple prompts (#625) 2025-12-02 13:29:55 -08:00
Awni Hannun 4bc21cc17b Ministral3 (#642)
* attempt ministral3, no tokenizer

* ministral3 works
2025-12-02 10:59:45 -08:00
Ivan Fioravanti 9fd3e419ec add support for Trinity/AfMoE model (#640)
* add support for Trinity/AfMoE model

- Implement AfMoE architecture with MoE (128 experts, 8 active per token)
- Dual normalization pattern (4 layer norms per decoder layer)
- Attention with Q/K normalization and learned sigmoid gating
- RoPE only for sliding window attention layers
- muP embedding scaling
- Shared experts support
- Custom quant_predicate for 4-bit quantization (keeps attention/embeddings at 8-bit)

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-12-02 07:34:54 -08:00
Awni Hannun 743f4f7710 fix olmo3 (#628) 2025-11-21 11:44:35 -08:00
Angelos Katharopoulos 088e7ad7ca Allow providing prompt caches in batched generation (#602) 2025-11-20 09:14:30 -08:00
Awni Hannun 1d01257d2e Fix for kimi k2 (#593)
* fix for kimi k2

* actually dequant

* use native int4
2025-11-18 06:16:42 -08:00
Awni Hannun 2959af09fb switch go github actions (#618) 2025-11-17 14:04:12 -08:00
Deekshith Reddy Dade 8f1f88e5af FIX: Add missing sentencepiece dependency for tokenizers (#611) 2025-11-17 07:55:54 -08:00
Gökdeniz Gülmez 606ff3ef06 ACKNOWLEDGMENTS.md House keeping (#594)
* typo

* add prince sections

* add ivan and more prince

* nits
2025-11-13 12:53:20 -08:00
Prince Canuma cd367819c7 Fix input_embeddings prefill bug in generate_step (#606)
* fix input_embeddings prefill bug in generate_step

* format
2025-11-13 12:52:57 -08:00
n8programs ba2cf3c0ee Fix Byte Decoder Lookup for Esoteric Single-Characters (#600)
* tokenizer single-character fix

* Update mlx_lm/tokenizer_utils.py

* Update mlx_lm/tokenizer_utils.py

---------

Co-authored-by: N8 <n8@n8programs.com>
Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2025-11-10 14:16:25 -08:00
Awni Hannun 6c1a459314 DWQ for very large models (#536)
* pipeline parallel mixin

* Refactor pipeline parallel, add optional target saving to DWQ

* preserve batch order

* Fixes

* fix glm4 pipeline

* event timeout hack

* use full targets for regular training
2025-11-07 06:43:40 -08:00
Prince Canuma 3833c205c1 [WIP] Add Kimi Linear (#577)
* add kimi linear

* fix config and naming

* refactor

* return array mask

* fix mask

* kimi linear fixes

# Conflicts:
#	mlx_lm/models/kimi_linear.py

* cleanup

* fix type casting (2 tok/s -> 70 tok/s)

* remove extra type casting

* remove upcasting from expert select

* nits

* format

* Simplify and remove fused_recurrent_kda

* Unify metal kernels

* Remove unnecessary chunking

* nits

---------

Co-authored-by: Tarjei Mandt <kernelpool@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-11-06 10:09:57 -08:00
Gökdeniz Gülmez 3356b0a017 Adding ring mini linear (#513)
* in. com.

* update

* better inference

* update

* updas

* upd.

* closer

* updates

* updates

* nits

* upd. ackn.

* format

* correct masking like the torch version

* add to test

* format

* optimization + format

* nits

* Fast path for generation

* remove linear attetnion cache

* adding it back

* speedbump + format

* clean up ackn.

* Store GLA state as float32 in metal kernel

* Fix operation order in Simple GLA recurrence

* nits

* fix

---------

Co-authored-by: Tarjei Mandt <kernelpool@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-11-06 09:57:29 -08:00
Josh York f6c94659d8 Fixed JSON parse error handling so it does not reference self.stream before it gets initialized (#592) 2025-11-06 06:24:36 -08:00
tnadav d3bf847e6f Make mlx-lm more type-checker friendly (#573)
* Fix type annotation for `load` parameter

* Add type annotations to all `load` parameters

* Avoid using mutable types for `load` default parameters

* Add return type annotation to `load_tokenizer`

* Export public module attributes
2025-11-05 11:25:00 -08:00
Josh York df6434185c Fix: Remove call to deleted method [_apply_chat_template_safe] and replace it with the standard method [self.tokenizer.apply_chat_template] (#591) 2025-11-05 11:23:19 -08:00
Jiaren Cai 974e17b43a add MiniMax-M2 in supported models (#575)
* add MiniMax-M2 in supported models

* update

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-11-05 08:39:38 -08:00
Josh York a82790a141 Fixed/improved behavior of the mask_prompt feature. (#584)
* Fixed/improved behavior of the mask_prompt feature.

Without setting add_generation_prompt to True, the model/assistant turn header can be included, which forces loss to be calculated over more than just the model's output that we care about.

Introduced _apply_chat_template_safe to centralize defensive calls to apply_chat_template to account for some environemnts that don't support tools (added defensive measures for add_generation_prompt too just in case).

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-11-05 08:38:41 -08:00
Alexander Schiwjow 2aa31f95a7 add parallel_residual setting to gptneox (#586)
Co-authored-by: Alexander Schwirjow <alexander.schwirjow@iis.fraunhofer.de>
2025-11-05 07:52:08 -08:00
Awni Hannun 4decc4d381 Add gen options and CoT removal (#587)
* add gen options and CoT removal

* comment
2025-11-05 06:16:59 -08:00
Tarjei Mandt 0d8272483b Remove leftover call to removed function (#590) 2025-11-05 06:13:45 -08:00
Josh York 663b822de5 Fixed typo in load_adapters that broke adapter loading after a regression in a recent commit. (#583)
load_adapeters -> load_adapters

Simple fix, but important.
2025-11-01 13:13:35 -07:00
Awni Hannun f36977385f fix eval thinking (#578) 2025-10-31 07:36:20 -07:00
Awni Hannun 1e8fca4e0b fix dequant + minor refactor (#572) 2025-10-30 14:30:10 -07:00
Gökdeniz Gülmez 61669b270f Align checkpoint loading with Jamba Mini and Large (#555)
* updates

* nits + format

* fix + format

* fix

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-29 21:04:10 -07:00
Prince Canuma f2b0262824 Add Minimax-M2 (#568)
* add minimax m2

* fix dequant and decoder

* remove unused

* remove unused

* normalize scores

* refactor

* fix minimax

* fix

---------

Co-authored-by: awni <awni@apple.com>
2025-10-27 14:39:25 -07:00
Awni Hannun 367d6d7686 version (#559) 2025-10-17 14:44:06 -07:00
Awni Hannun 5e6a7f6895 version (#558) 2025-10-17 14:41:44 -07:00
Daniel Nakov edc656a85c Add support for nanochat (#554)
* Add support for nanochat

* format

* compile softcap

* add test

---------

Co-authored-by: dnakov <3777433+dnakov@users.noreply.github.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-15 12:59:54 -07:00
Awni Hannun a4c6470390 benchmarks (#552) 2025-10-15 12:24:12 -07:00
Vincent Amato 1d114498f2 Add Qwen3-VL (Dense) language model implementation (#553)
* Added Qwen3-VL dense language model

* Added Qwen3-VL dense language model test
2025-10-14 12:28:46 -07:00
Gökdeniz Gülmez b1fc49a9f2 Adding jamba (#544)
* add modelargs

* adding mlp and sdp attentino

* updates

* adding the rest

* updates

* finish

* format

* upd. ackn.

* nits + format

* speedup

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-14 08:44:50 -07:00
Tarjei Mandt 4781244aaf Optimize Bailing MoE (#550)
* Optimize Bailing MoE

* Fix formatting
2025-10-14 07:45:07 -07:00
Awni Hannun 49ba6dff08 Support data parallel eval for generation tasks (#549)
* Support data parallel eval for generation tasks

* comment
2025-10-13 13:49:58 -07:00
Esakkivel Esakkiraja c3b4a15851 Added gradient accumulation to training loop (#511) 2025-10-13 11:27:15 -07:00
Awni Hannun a4e32ef5a5 Fix mask for batched SSM (#546) 2025-10-10 14:30:27 -07:00
Vincent Amato cceb45d6b6 Add Qwen3-VL language model implementation (#547)
* Added Qwen3-VL language model implementation.

* Formatted code

* Removed redundent test and added quant_predicate propoerty
2025-10-09 21:42:46 -07:00
Awni Hannun 0b8c1668d2 fix cuda install (#542) 2025-10-08 16:03:43 -07:00
Mauricio Barba Da Costa f876b3f775 minor typing issues (#540) 2025-10-08 06:07:35 -07:00
Victor Nogueira 373c63c08f Fix example command to quantize a model using GPTQ (#539) 2025-10-08 06:07:18 -07:00
Prince Canuma 344755a1f6 Add lfm2 moe (#537)
* add lfm2 moe

* fix config, loading and expert bias

* add test

* nits

---------

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2025-10-07 16:18:47 -07:00
mzfive abb185cb66 Fix: Add __future__ annotations import to qwen3_next.py for Python 3.9 compatibility (#533)
* Fix Python 3.8/3.9 compatibility in qwen3_next.py

Add missing `from __future__ import annotations` import to fix
Python 3.8/3.9 compatibility.

The `|` union syntax (PEP 604) requires Python 3.10+ or the
__future__ import. This change maintains the declared
python_requires>=3.8 compatibility.

Fixes compatibility with macOS system Python (3.9.6).

* format

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-07 11:28:36 -07:00
Awni Hannun 49919e8563 Cleanup and simplify model I/O (#532)
* Cleanup and simplify model I/O

* fix test
2025-10-07 06:57:51 -07:00
Awni Hannun 7168556aaa Remove act loss and add temp in DWQ (#500) 2025-10-07 06:06:23 -07:00
shepardxia 1e71de53e3 GPT2 Batching Fix (#529) 2025-10-07 00:36:48 -07:00
Awni Hannun f318741784 fix bailing moe (#521) 2025-10-02 15:15:14 -07:00
Awni Hannun 0edd0a0cd1 Fix lora MoEs (#522) 2025-10-02 15:15:00 -07:00
Awni Hannun 44c74e1d04 memory efficient ssm (#525) 2025-10-02 15:14:48 -07:00
Gökdeniz Gülmez 81c3c193cf removings (#524) 2025-10-02 11:49:39 -07:00
Gabe Goodhart b264da7602 feat: Refactor granitemoehybrid to support dense and non-hybrid variants (#518)
* feat: Refactor granitemoehybrid to support dense and non-hybrid variants

Written with Claude Code. Initial prompt:

I need to modify the model support implemented in `mlx_lm/models/granitemoehybrid.py` in two ways:

* Support optionally using a dense block in place of MoE. The dense block should look like `mlx_lm/models/granite.py` instead of `mlx_lm/models/granitemoe.py`.

* Support the case where there are no `mamba` layers (ie non-hybrid). This should devolve to exactly `granite.py` or `granitemoe.py` depending on whether the block after attention is dense or MoE.

You can test this using the following two models:

* Dense w/ hybrid: /Users/ghart/models/dmf_models/granite-4.0-h-micro-r250918a
* Dense w/ non-hybrid: /Users/ghart/models/dmf_models/granite-4.0-micro-r250918a

Branch: GraniteFourDense

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* refact: Clean up Claude's code a bit

Branch: GraniteFourDense

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* style: pre-commit format

Branch: GraniteFourDense

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* version bump

---------

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-02 07:21:08 -07:00
Ivan Fioravanti cf8cfd0a1b Add Apriel 1.5 (#520) 2025-10-01 21:49:16 -07:00
Nathan Sashihara f96344dfe6 Mixed quantization affects attention in DeepSeek V3, others (#506) 2025-10-01 21:20:37 -07:00
Gökdeniz Gülmez 9a4039a518 Add Olmo3 (#445)
* in. com.

* done

* making it trainable

* upd. ackn.

* format

* make tie_word_embeddings false

* fix index_id number

* default layer_types

* nits

* working inference

* finish

* finish

* format

* nits

* comment

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-10-01 16:03:13 -07:00
Awni Hannun 16ca0b3a17 simplify to_lora (#515) 2025-09-30 10:23:57 -07:00
Tarjei Mandt e1e66c0d64 Fix: Correct weight masking for zero-computation experts in LongCat Flash MoE (#508) 2025-09-30 08:45:07 -07:00
Tarjei Mandt 380789d067 Fix batching for models with nested cache structures (#510) 2025-09-30 07:59:57 -07:00
Awni Hannun a1d079e930 fix bailing moe (#514) 2025-09-29 13:42:18 -07:00
Awni Hannun 0c0b72221f Use depends in pipeline parallel (#483) 2025-09-26 16:42:51 -07:00
Daniel Nakov dcb4b9ba6d Add Code World Model support (#505)
* Add sliding-window support to LLaMA

* nits

* version

---------

Co-authored-by: dnakov <3777433+dnakov@users.noreply.github.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-26 15:22:12 -07:00
Awni Hannun 358b4d2ab5 fix (#503) 2025-09-26 08:48:24 -07:00
Prince Canuma 1a4d24ed5f Add Falcon H1 (#231)
* working inference

* minor refactor

* update rope

* add multipliers

* add gated rms

* temp fix

* fix all issues

* Empty commit message

Co-authored-by: Hamza Yous <HamzaYousLM@users.noreply.github.com>

* creds

Co-authored-by: Hamza Yous <HamzaYousLM@users.noreply.github.com>

* fix conv weight sanitize

* add tests

* rename config to args

* refactor RMSNormGated

* remove unused

* fix  multi-turn chat

* format

* replace at and set

* optimize infer: 42 -> 45 tok/s

* generate mup vector in Model

* remove comment

* refactor cache

* update mamba mask

* remove cache pos

* cleanup and speedup

* more cleanup

* more cleanup

* use mamba op + big speedup

* Fix batching with cache list

---------

Co-authored-by: younesbelkada <younes.belkada@tii.ae>
Co-authored-by: Hamza Yous <HamzaYousLM@users.noreply.github.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-24 07:58:23 -07:00
Awni Hannun 47e1710f23 qwen3 next batching (#478)
* qwen3 next batching

* fix None mask
2025-09-23 20:59:11 -07:00
Awni Hannun 50012d153d Add batch support for sliding window cache (#487)
* add batch support for sliding window cache

* fix

* fix masks

* fix cache

* another test

* comment
2025-09-23 20:56:47 -07:00
Awni Hannun eaf1748ea5 enable training for qwen3 next (#496) 2025-09-23 15:50:38 -07:00
Awni Hannun ffc0ecc1ca fix loading for qwen2 VL (#491) 2025-09-23 13:12:37 -07:00
Awni Hannun 4096aabdba fix for LFM2 (#493) 2025-09-23 13:12:28 -07:00
Awni Hannun 36963eec80 Fix KV cache quantization for hybrid models (#495) 2025-09-23 13:12:17 -07:00
Aria Wong f22120ef83 Fixing missing parameter passing for model_config in utils.load() (#494) 2025-09-23 13:02:35 -07:00
Awni Hannun c991106dbf fix quant predicate (#485) 2025-09-18 13:56:37 -07:00
Ivan Fioravanti a7f534c3f5 Gated-Delta Fused Kernel (Qwen3Next) (#454)
* apply gating in recurrent_gated_delta_rule

* update cache with new state

* prealocate outputs in recurrent_gated_delta_rule

* feat(kernel): gated-delta kernel scaffolding with CPU fallbacks and tests; integrate in Qwen3Next behind flag

* feat(kernel): implement Metal kernel for gated delta prefill with time iteration to optimize performance

* faster single time step kernel

* use kernel for prefill

* version bump

---------

Co-authored-by: Goekdeniz-Guelmez <gulmezdeniz1999@gmail.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-17 14:23:40 -07:00
Robert Collins 04d6d926f3 Extends quantization predicate with config (#476)
Adds config parameter to quantization predicate
Enables fine-grained quantization control
Supports per-parameter quantization strategies
Improves flexibility in model quantization configuration
2025-09-17 14:02:47 -07:00
Gökdeniz Gülmez 38dc092e1f Fix llama4 text and make trainable (#474)
* in. com.

* format

* add copyright

* nits + removings
2025-09-17 13:52:22 -07:00
Gökdeniz Gülmez de47734510 Adding support for mamba2 (#392)
* initial commit

* update tuner/utils.py

* update ACKNOWLEDGMENTS.md

* update

* nits

* movinf mamba2 cache over to cache.py + clean up

* clean up

* fix residual_in_fp32

* updates

* adding default args

* updates

* first working inference with codestral mamba

* clean up

* adding 1b mamba 2

* udpdates

* updates

* updates

* finish

* clean up

* clean ups

* format

* nits

* adding some einsums

* format again

* optimize + format

* nits

* nits

* more speed

* use custom kernel

* update

* format

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-16 15:33:03 -07:00
n8programs 9e89bb5c25 smol bugfix (#473)
Co-authored-by: N8 <n8@n8programs.com>
2025-09-16 15:32:56 -07:00
Robert Collins b2564b5226 Adds LLaMA 4 text model implementation in MLX (#469)
* Adds LLaMA 4 text model implementation in MLX

Implements full LLaMA 4 text model architecture in MLX
Supports configurable attention and MLP variants
Adds RoPE, RMSNorm, and attention temperature tuning
Enables dual-branch MLP and per-layer RoPE control
Integrates weight loading from safetensors with config mapping
Handles token embedding and layer normalization properly

* nits + fixes

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-16 14:48:22 -07:00
Ivan Fioravanti 502554646d fix: handle cache offset safely for mamba error (#472)
* fix: handle cache offset safely for mamba error

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-16 14:48:16 -07:00
Awni Hannun 40dd25d7b7 Batch support for mamba-style models (#468)
* support mamba in batch inference

* works with nemotron

* granite

* add to plamo2

* more models + fixes

* fix
2025-09-16 08:01:45 -07:00
Awni Hannun 55bb9471b8 Batch generation (#443)
* initial batch generation

* more in batch generate

* concatenation

* use batch API in eval

* unique max tokens per prompt

* basic continuous batching

* simplify

* better perf by ensuring everything in same stream

* use data class for response

* check cache type
2025-09-15 16:02:45 -07:00
Awni Hannun 9a11a81add fix gemma3 window (#465) 2025-09-15 11:01:21 -07:00
Awni Hannun 4aaac2072d Add groups to ssm kernel and update more models (#456)
* Add groups to ssm kernel and update more models

* faster prompt processing
2025-09-15 09:37:24 -07:00
Neil Mehta 469461f463 fix VL models (#464) 2025-09-15 09:37:05 -07:00
Zhedong Cen 714157be6d Add an introduction to the default LLM in README.md (#461)
* Add introduction of default LLM in README.md

* Update README.md

---------

Co-authored-by: Awni Hannun <awni.hannun@gmail.com>
2025-09-15 06:14:38 -07:00
Gökdeniz Gülmez 06a9fdc5ad Adding GLM (#457)
* in. com.

* upd.

* format

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-13 21:41:02 -07:00
Awni Hannun 420418ca4e Update README.md 2025-09-13 14:29:54 -07:00
Gökdeniz Gülmez d6c45998f0 fix qwen3 next (#453)
* apply gating in recurrent_gated_delta_rule

* update cache with new state

* prealocate outputs in recurrent_gated_delta_rule

* format

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-13 07:03:03 -07:00
Gökdeniz Gülmez 42828df17a Update bitnet, nemotron h to use build in relu2 from MLX (#446)
* upd. bitnet, nemotron h

* format
2025-09-13 07:02:29 -07:00
Awni Hannun 80c217ff96 Faster ssm (#451)
* faster ssm

* comment + support batching
2025-09-12 17:40:08 -07:00
Gökdeniz Gülmez cf8e59ef76 Adding Qwen3 Next (#441)
* in. com.

* adding attention + gated rms norm

* adding Qwen3NextDecoderLayer

* adding Qwen3NextModel

* adding Model

* adding MLP

* adding Qwen3NextGatedDeltaNet

* updates

* updates

* upd. ackn.

* nits

* making it trainable

* inference fix

* gibberish inference

* fix training

* fix for batching

* nits

* optimize

* updates

* closer

* upd.

* fix inference

* fix

* optimization

* nits

* minimize

* clean ups

* format

* nits

* format again

* set some defaults

* alternateing layer defaults

* remove MTP layers

* add head dim but optional

* nits + format

* some nits

* some fixes

* fixes

* move f to innit

* optimized recurrent_gated_delta_rule

* optmize and shorten recurrent_gated_delta_rule a lot + moving g = mx.exp(g) up to fix gibberish output

* make train better

* nits

* nits + fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-12 17:29:48 -07:00
Gökdeniz Gülmez 7b84eb6bd1 Adding Ling Mini (#450)
* in. com.

* fixes

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-12 11:57:59 -07:00
squaredice 547b6b7e26 fix(quantization): parameterize hardcoded group_size in mixed_quant_predicate_builder (#449)
- Added group_size parameter to mixed_quant_predicate_builder with default of 64 for backward compatibility.
- Propagated q_group_size from convert function to the builder call.
- Improves flexibility for custom quantization schemes without altering core logic.
2025-09-12 06:16:36 -07:00
Awni Hannun 319c453828 sdpa with sinks (#418)
* sdpa with sinks

* bump mlx

* fix
2025-09-11 20:00:52 -07:00
Awni Hannun 64b3c51a58 fix server paths (#448)
* fix server paths

* remove relative condition
2025-09-11 14:05:38 -07:00
Gabe Goodhart 1537efd29a model: GraniteMoeHybrid (#442)
* feat(models): Add initial implementation of GraniteMoeHybrid generated by Claude Code

This commit was entirely generated using Claude Code and the following
prompt:

---
I've got an in-depth feature request for you to add. I need you to add support for the GraniteMoeHybrid architecture to the `mlx-lm` project. The task is to extend the existing set of model architecture implementations in `mlx_lm/models` by adding a new module named `granitemoehybrid.py`. Here are a few key pointers on this model architecture:

* It is a hybrid-recurrent model that uses `mamba2` for some layers (recurrent) and `granitemoe` for some layers (attention)
* It is very similar to the `nemotron_h` architecture implemented in `mlx_lm/models/nemotron_h.py`, but with a few key differences
    * In `GraniteMoeHybrid`, each layer has either a `mamba2` block or a `granitemoe` attention block AND a MoE block, whereas in `nemotron_h`, each "layer" is a single block that is either `mamba2`, `attention` (llama), or `ffn` (not MoE).
    * The config for `GraniteMoeHybrid` uses the `layer_types` field to determine whether to use `mamba2` or `granitemoe` attention for each layer
* The `transformers` implementation can be found at https://github.com/huggingface/transformers/blob/main/src/transformers/models/granitemoehybrid/modeling_granitemoehybrid.py
    * The config can be found at https://github.com/huggingface/transformers/blob/main/src/transformers/models/granitemoehybrid/configuration_granitemoehybrid.py
* The PR adding support in `llama.cpp` is: https://github.com/ggml-org/llama.cpp/pull/13550
    * NOTE: In `llama.cpp`, I made the architecture slightly more flexible such that each layer could use either a MoE block OR a fully-connected FFN block after the recurrent/attention block
* For the `granitemoe` attention, the architecture is very similar to standard `llama` attention, but it includes 4 additional scalar multipliers that are pulled from config:
    * `embedding_multiplier`:
        * Multiply the input embeddings by this scalar before the first layer
        * Used here in `transformers` https://github.com/huggingface/transformers/blob/main/src/transformers/models/granitemoehybrid/modeling_granitemoehybrid.py#L1347
    * `attention_multiplier`:
        * Used as the scaling factor in standard attention in place of the default 1/sqrt(n_embed_head)
        * Used here in `transformers`: https://github.com/huggingface/transformers/blob/main/src/transformers/models/granitemoehybrid/modeling_granitemoehybrid.py#L217

The goal of this project is to create a fully working local implementation of the model in `mlx_lm`. You can find a local model to test with at /Users/ghart/models/granite-4.0-tiny-preview/. You can find a version of the `nemotron_h` model to test with at /Users/ghart/models/nvidia/NVIDIA-Nemotron-Nano-9B-v2/. To accomplish this project, you'll need to take the following steps:

1. Get a development environment working (you can use `uv` to manage your virtual env) and install the necessary dependencies
2. Run a sample inference with a model that is already known to work (eg `/Users/ghart/models/nvidia/NVIDIA-Nemotron-Nano-9B-v2/`)
3. Create the new module at `mlx_lm/models/granitemoehybrid.py`
4. Implement the model architecture, test, and iterate until you've got things working locally

Once you've got it working, let me know and I'll review and commit
---

Branch: GraniteHybrid

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* fix(models): Claude Code fixes to architecture bugs

Inference now matches transormers. Further refinement by me comming next.

Branch: GraniteHybrid

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* fix: Cleanup trailing whitespace and unused imports / config params

Branch: GraniteHybrid

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* refactor: Refactor implementations to more closely resemble related models

This keeps the implementation of the attention block closer to GraniteMoe
for an easier diff view in the future. The functionality is identical.

Branch: GraniteHybrid

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>

* nits + rebase

---------

Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>
Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-10 08:37:32 -07:00
Gökdeniz Gülmez 4a085c7618 Add lille 130m (#429)
* in. com.

* inference works

* rebase

* cpyrgt

* upd. ackn

* clean up residuals

* format

* rebase + nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-09 21:21:24 -07:00
Gökdeniz Gülmez 87961a743a adding Kwai-Klear/Klear-46B-A2.5B-Instruct (#437)
* in. com.

* clean up

* sanitize

* fix

* nits

* making it trainable

* format

* upd. ackn

* rebase + nits

* rebase + nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-09 20:10:16 -07:00
Gökdeniz Gülmez d79f3cd612 remove manual conv class in mamba1 (#436)
* remove manual conv class

* remove slice

* add sanitize

* format
2025-09-09 20:09:43 -07:00
Awni Hannun 103877ea3e some cleanup + tests towards batching (#430) 2025-09-09 13:05:11 -07:00
Awni Hannun 64574e19b8 fix hunyuan v1 dense (#440) 2025-09-09 12:56:30 -07:00
Nathan Sashihara 1b08ef199b Avoid cache-trimming crash in server for longcat chat and baichuan_m1 (#434)
* Avoid cache-trimming crash in server for longcat chat and baichuan_m1

* fix

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-08 10:09:41 -07:00
Awni Hannun 4ce11c6e50 allow fp8 (#431) 2025-09-08 06:35:47 -07:00
Gökdeniz Gülmez 0f268680c8 Fix Nemotron H loading error (#426)
* fix

* format
2025-09-04 09:03:00 -07:00
Gökdeniz Gülmez 3ae6583393 Adding longcat flash (#423)
* in. com.

* udpates

* working

* fix rope

* import rope from deepseek file

* nits

* making it trainable

* adding to lora

* update ackn

* fixes

* fixes

* bump

* bump

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-04 08:51:34 -07:00
Gökdeniz Gülmez dccde59664 Add nemotron h (#407)
* init commit

* updates

* working

* updates

* format

* working

* updates

* format

* making it trainable

* clean up

* clean up

* updates

* clean up

* format

* nits

* final format

* nits + format

* fix mamba

* perf + nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-03 21:22:01 -07:00
Gökdeniz Gülmez 30c30a2a7e add Apertus from Swiss AI (#421)
* innit. com.

* fix Xielu

* update ackn.

* making it trainable

* nits

* format

* compile nonlinearity

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-03 07:25:23 -07:00
Gökdeniz Gülmez 6dd9d48bdc had to add self.args and self.model_type into the model class for mlx-lm-lore (#422)
* fix

* nits

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-03 06:30:22 -07:00
Gökdeniz Gülmez f5741cae33 Adding ibm Granite MoE (#413)
* init comm

* upd ackn

* upd train

* training working

* format and testing training

* use switch layer

---------

Co-authored-by: Awni Hannun <awni@apple.com>
2025-09-02 07:51:22 -07:00
Nathan Sashihara 3b9074f48a server allow specifying seed (#414)
* server allow specifying seed

* seed test only require immediate consistency
2025-09-02 06:13:21 -07:00
n8programs e797abf49b np random seed (#415)
Co-authored-by: N8 <n8@n8programs.com>
2025-08-30 06:04:40 -07:00
195 changed files with 27714 additions and 3400 deletions
-100
View File
@@ -1,100 +0,0 @@
version: 2.1
orbs:
apple: ml-explore/pr-approval@0.1.0
jobs:
linux_build_and_test:
docker:
- image: cimg/python:3.9
steps:
- checkout
- run:
name: Run style checks
command: |
pip install pre-commit
pre-commit run --all
if ! git diff --quiet; then echo 'Style checks failed, please install pre-commit and run pre-commit run --all and push the change'; exit 1; fi
mlx_lm_build_and_test:
macos:
xcode: "15.2.0"
resource_class: m2pro.medium
steps:
- checkout
- run:
name: Install dependencies
command: |
brew install python@3.9
python3.9 -m venv env
source env/bin/activate
pip install --upgrade pip
pip install sentencepiece
pip install unittest-xml-reporting
pip install -e ".[test]"
- run:
name: Run Python tests
command: |
source env/bin/activate
python -m xmlrunner discover -v tests -o test-results/
- store_test_results:
path: test-results
build_release:
macos:
xcode: "15.2.0"
resource_class: m2pro.medium
steps:
- checkout
- run:
name: Install dependencies
command: |
brew install python@3.9
python3.9 -m venv env
source env/bin/activate
pip install --upgrade pip
pip install build
pip install twine
- run:
name: Build and upload
command: |
source env/bin/activate
python -m build
twine upload dist/*
- store_artifacts:
path: dist/
workflows:
build_and_test:
when:
matches:
pattern: "^(?!pull/)[-\\w]+$"
value: << pipeline.git.branch >>
jobs:
- mlx_lm_build_and_test
- linux_build_and_test
build_pypi_release:
jobs:
- build_release:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
prb:
when:
matches:
pattern: "^pull/\\d+(/head)?$"
value: << pipeline.git.branch >>
jobs:
- hold:
type: approval
- apple/authenticate:
context: pr-approval
- mlx_lm_build_and_test:
requires: [ hold ]
- linux_build_and_test:
requires: [ hold ]
+16
View File
@@ -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 }}
+44
View File
@@ -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
+41
View File
@@ -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/
+23 -2
View File
@@ -8,5 +8,26 @@ with a short description of your contribution(s) below. For example:
MLX LM was developed with contributions from the following individuals:
- Shunta Saito: Added support for PLaMo models.
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's`Mamba v1`, Z.ai & THUKEG's `GLM4`, Rednote `dots.llm1`, Baisu's `Ernie4.5 MoE`, inclusionAI's `Ling-lite, Ling-Coder-lite, Ling-plus`, and Allenai's `OLMoE`; Added support for the following training algorithms: `Full Weight Fine-Tuning`, and the `Muon` optimizer; Added support for the following other features: `Multiple Optimizers to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
- Prince Canuma: Helped add support for the following model architectures: HuggingFace's `Starcoder2`, Cohere's `Cohere (1 and 2)`, Alibaba Qwen's `Qwen (2, 3 and MoE)`, Microsoft's `Phi (3 and 3.5 MoE)`, `BitNet1.58`, Meta's `Llama (3 and 4)`, Google DeepMind's `Gemma 3`, and InterLM's `InternLM 2.5`.
- 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)`
+24 -43
View File
@@ -12,7 +12,6 @@ Some key features include:
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`
* Supports experiment tracking using SwanLab and W&B.
The easiest way to get started is to install the `mlx-lm` package:
@@ -53,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:
@@ -66,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)
@@ -80,7 +85,9 @@ To see a description of all the arguments you can do:
Check out the [generation
example](https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/examples/generate_response.py)
to see how to use the API in more detail.
to see how to use the API in more detail. Check out the [batch generation
example](https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/examples/batch_generate_response.py)
to see how to efficiently generate continuations for a batch of prompts.
The `mlx-lm` package also comes with functionality to quantize and optionally
upload models to the Hugging Face Hub.
@@ -123,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):
@@ -163,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:
@@ -178,7 +185,7 @@ You can upload new models to Hugging Face by specifying `--upload-repo` to
```
mlx_lm.convert \
--hf-path mistralai/Mistral-7B-Instruct-v0.3 \
--model mistralai/Mistral-7B-Instruct-v0.3 \
-q \
--upload-repo mlx-community/my-4bit-mistral
```
@@ -229,45 +236,19 @@ for more usage details.
### Supported Models
`mlx-lm` supports thousands of Hugging Face format LLMs. If the model you want to
run is not supported, file an
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet,
submit a pull request.
`mlx-lm` supports thousands of LLMs available on the Hugging Face Hub. If the
model you want to run is not supported, file an
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet, submit
a pull request. Many supported models are available in various quantization
formats in the [MLX Community](https://huggingface.co/mlx-community) Hugging
Face organization.
Here are a few examples of Hugging Face models that work with this example:
For some models the tokenizer may require you to enable the `trust_remote_code`
option. You can do this by passing `--trust-remote-code` in the command line.
If you don't specify the flag explicitly, you will be prompted to trust remote
code in the terminal when running the model.
- [mistralai/Mistral-7B-v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1)
- [meta-llama/Llama-2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf)
- [deepseek-ai/deepseek-coder-6.7b-instruct](https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-instruct)
- [01-ai/Yi-6B-Chat](https://huggingface.co/01-ai/Yi-6B-Chat)
- [microsoft/phi-2](https://huggingface.co/microsoft/phi-2)
- [mistralai/Mixtral-8x7B-Instruct-v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1)
- [Qwen/Qwen-7B](https://huggingface.co/Qwen/Qwen-7B)
- [pfnet/plamo-13b](https://huggingface.co/pfnet/plamo-13b)
- [pfnet/plamo-13b-instruct](https://huggingface.co/pfnet/plamo-13b-instruct)
- [stabilityai/stablelm-2-zephyr-1_6b](https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b)
- [internlm/internlm2-7b](https://huggingface.co/internlm/internlm2-7b)
- [tiiuae/falcon-mamba-7b-instruct](https://huggingface.co/tiiuae/falcon-mamba-7b-instruct)
Most
[Mistral](https://huggingface.co/models?library=transformers,safetensors&other=mistral&sort=trending),
[Llama](https://huggingface.co/models?library=transformers,safetensors&other=llama&sort=trending),
[Phi-2](https://huggingface.co/models?library=transformers,safetensors&other=phi&sort=trending),
and
[Mixtral](https://huggingface.co/models?library=transformers,safetensors&other=mixtral&sort=trending)
style models should work out of the box.
For some models (such as `Qwen` and `plamo`) the tokenizer requires you to
enable the `trust_remote_code` option. You can do this by passing
`--trust-remote-code` in the command line. If you don't specify the flag
explicitly, you will be prompted to trust remote code in the terminal when
running the model.
For `Qwen` models you must also specify the `eos_token`. You can do this by
passing `--eos-token "<|endoftext|>"` in the command
line.
These options can also be set in the Python API. For example:
Tokenizer options can also be set in the Python API. For example:
```python
model, tokenizer = load(
+348
View File
@@ -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()
+63
View File
@@ -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>
+1 -1
View File
@@ -129,7 +129,7 @@ mlx_lm.awq --help
Use `mlx_lm.gptq` to run GPTQ on a given model. For example:
```bash
mlx_lm.awq --model Qwen/Qwen3-0.6B
mlx_lm.gptq --model Qwen/Qwen3-0.6B
```
The script can take anywhere from a few minutes to several hours depending on
+12 -7
View File
@@ -66,9 +66,10 @@ mlx_lm.lora \
To fine-tune the full model weights, add the `--fine-tune-type full` flag.
Currently supported fine-tuning types are `lora` (default), `dora`, and `full`.
The `--data` argument must specify a path to a `train.jsonl`, `valid.jsonl`
when using `--train` and a path to a `test.jsonl` when using `--test`. For more
details on the data format see the section on [Data](#Data).
The `--data` argument must specify a path to a `train.jsonl` when using
`--train` and a path to a `test.jsonl` when using `--test`. A `valid.jsonl` is
optional; if provided, validation loss will be reported during training. For
more details on the data format see the section on [Data](#Data).
For example, to fine-tune a Mistral 7B you can use `--model
mistralai/Mistral-7B-v0.1`.
@@ -184,9 +185,10 @@ Face.
### Local Datasets
For fine-tuning (`--train`), the data loader expects a `train.jsonl` and a
`valid.jsonl` to be in the data directory. For evaluation (`--test`), the data
loader expects a `test.jsonl` in the data directory.
For fine-tuning (`--train`), the data loader expects a `train.jsonl` to be in
the data directory. A `valid.jsonl` is optional; if present, validation loss
will be reported periodically during training. For evaluation (`--test`), the
data loader expects a `test.jsonl` in the data directory.
Currently, `*.jsonl` files support `chat`, `tools`, `completions`, and `text`
data formats. Here are examples of these formats:
@@ -371,7 +373,10 @@ of memory. Here are some tips to reduce memory use should you need to do so:
2. Try using a smaller batch size with `--batch-size`. The default is `4` so
setting this to `2` or `1` will reduce memory consumption. This may slow
things down a little, but will also reduce the memory use.
things down a little, but will also reduce the memory use. You can increase
the effective batch size without increasing the memory use by accumulating
gradients using `--grad-accumulation-steps <N>` which will accumulate the
gradient of `<N>` batches before updating the parameters.
3. Reduce the number of layers to fine-tune with `--num-layers`. The default
is `16`, so you can try `8` or `4`. This reduces the amount of memory
+14 -2
View File
@@ -72,12 +72,24 @@ curl localhost:8080/v1/chat/completions \
- `min_p`: (Optional) A float specifying the min-p sampling parameter.
Defaults to `0.0` (disabled).
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
Defaults to `1.0`.
- `repetition_penalty`: (Optional) Applies a multiplicative penalty to repeated
tokens. Defaults to `0.0` (disabled).
- `repetition_context_size`: (Optional) The size of the context window for
applying repetition penalty. Defaults to `20`.
- `presence_penalty`: (Optional) Applies an additive penalty to tokens
that appeared before. Defaults to `0.0` (disabled).
- `presence_context_size`: (Optional) The size of the context window for
applying presence penalty. Defaults to `20`.
- `frequency_penalty`: (Optional) Applies an additive penalty proportional to
how many times a token appeared previously. Defaults to `0.0` (disabled).
- `frequency_context_size`: (Optional) The size of the context window for
applying frequency penalty. Defaults to `20`.
- `logit_bias`: (Optional) A dictionary mapping token IDs to their bias
values. Defaults to `None`.
+10 -1
View File
@@ -7,5 +7,14 @@ from ._version import __version__
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
from .convert import convert
from .generate import generate, stream_generate
from .generate import batch_generate, generate, stream_generate
from .utils import load
__all__ = [
"__version__",
"convert",
"batch_generate",
"generate",
"stream_generate",
"load",
]
+3 -28
View File
@@ -1,31 +1,6 @@
# Copyright © 2025 Apple Inc.
import importlib
import sys
if __name__ == "__main__":
subcommands = {
"quant.awq",
"quant.dwq",
"quant.dynamic_quant",
"quant.gptq",
"benchmark",
"cache_prompt",
"chat",
"convert",
"evaluate",
"fuse",
"generate",
"lora",
"perplexity",
"server",
"manage",
"upload",
}
if len(sys.argv) < 2:
raise ValueError(f"CLI requires a subcommand in {subcommands}")
subcommand = sys.argv.pop(1)
if subcommand not in subcommands:
raise ValueError(f"CLI requires a subcommand in {subcommands}")
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
submodule.main()
from . import cli
cli.main()
+1 -1
View File
@@ -1,3 +1,3 @@
# Copyright © 2023-2025 Apple Inc.
__version__ = "0.27.0"
__version__ = "0.31.3"
+89 -25
View File
@@ -1,16 +1,13 @@
# Copyright © 2025 Apple Inc.
import argparse
import time
import mlx.core as mx
from mlx_lm import stream_generate
from mlx_lm import batch_generate, load, stream_generate
from mlx_lm.generate import DEFAULT_MODEL
from mlx_lm.tokenizer_utils import load_tokenizer
from mlx_lm.utils import (
fetch_from_hub,
get_model_path,
)
from mlx_lm.utils import pipeline_load, sharded_load
def setup_arg_parser():
@@ -25,11 +22,6 @@ def setup_arg_parser():
),
default=None,
)
parser.add_argument(
"--trust-remote-code",
action="store_true",
help="Enable trusting remote code for tokenizer",
)
parser.add_argument(
"--prompt-tokens",
"-p",
@@ -44,6 +36,13 @@ def setup_arg_parser():
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",
@@ -51,6 +50,29 @@ def setup_arg_parser():
help="Number of timing trials",
type=int,
)
parser.add_argument(
"--pipeline",
action="store_true",
help="Use pipelining instead of tensor parallelism",
)
parser.add_argument(
"--quantize-activations",
"-qa",
action="store_true",
help="Quantize activations using the same quantization config as the corresponding layer.",
)
parser.add_argument(
"--prefill-step-size",
type=int,
default=2048,
help="Step size for prefill processing (default: 2048)",
)
parser.add_argument(
"--delay",
type=int,
default=0,
help="Delay between each test in seconds (default: 0)",
)
return parser
@@ -59,39 +81,81 @@ def main():
args = parser.parse_args()
mx.random.seed(0)
group = mx.distributed.init()
rank = group.rank()
pipeline_group = group if args.pipeline else None
tensor_group = group if not args.pipeline else None
def rprint(*args, **kwargs):
if rank == 0:
print(*args, **kwargs)
model_path = args.model or DEFAULT_MODEL
model_path, _ = get_model_path(model_path, revision=None)
model, config, _ = fetch_from_hub(model_path, trust_remote_code=True)
tokenizer = load_tokenizer(
model_path,
eos_token_ids=[], # Empty to avoid early stopping
tokenizer_config_extra={"trust_remote_code": True},
)
if group.size() > 1:
model, tokenizer, config = sharded_load(
model_path, pipeline_group, tensor_group, return_config=True
)
else:
model, tokenizer, config = load(
model_path,
return_config=True,
tokenizer_config={"trust_remote_code": True},
model_config={"quantize_activations": args.quantize_activations},
)
# Empty to avoid early stopping
tokenizer._eos_token_ids = {}
prompt_tokens = args.prompt_tokens
generation_tokens = args.generation_tokens
prompt = mx.random.randint(0, config["vocab_size"], (prompt_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 _bench():
def single_bench():
for response in stream_generate(
model, tokenizer, prompt, max_tokens=generation_tokens
model,
tokenizer,
prompt,
max_tokens=generation_tokens,
prefill_step_size=args.prefill_step_size,
):
pass
return response
print("Running warmup..")
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"]
print(f"Timing with {prompt_tokens=} and {generation_tokens=}.")
rprint(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
responses = []
for i in range(args.num_trials):
if args.delay > 0:
time.sleep(args.delay)
tic = time.perf_counter()
response = _bench()
toc = time.perf_counter()
responses.append(response)
results = [(k, getattr(response, k)) for k in report_keys]
results = [f"{k}={v:.3f}" for k, v in results]
print(f"Trial {i+1}: " + ", ".join(results))
results.append(f"total_time={toc - tic:.3f}")
rprint(f"Trial {i+1}: " + ", ".join(results))
def avg(k):
vals = (getattr(response, k) for response in responses)
@@ -99,7 +163,7 @@ def main():
results = [(k, avg(k)) for k in report_keys]
results = [f"{k}={v:.3f}" for k, v in results]
print(f"Averages: " + ", ".join(results))
rprint(f"Averages: " + ", ".join(results))
if __name__ == "__main__":
+4 -17
View File
@@ -41,16 +41,6 @@ def setup_arg_parser():
default=None,
help="End of sequence token for tokenizer",
)
parser.add_argument(
"--ignore-chat-template",
action="store_true",
help="Use the raw prompt without the tokenizer's chat template.",
)
parser.add_argument(
"--use-default-chat-template",
action="store_true",
help="Use the default chat template",
)
parser.add_argument(
"--max-kv-size",
type=int,
@@ -107,14 +97,12 @@ def main():
args.prompt = sys.stdin.read() if args.prompt == "-" else args.prompt
if args.use_default_chat_template:
if tokenizer.chat_template is None:
tokenizer.chat_template = tokenizer.default_chat_template
if not args.ignore_chat_template and tokenizer.chat_template is not None:
if tokenizer.has_chat_template:
messages = [{"role": "user", "content": args.prompt}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=False, continue_final_message=True
messages,
add_generation_prompt=False,
continue_final_message=True,
)
else:
@@ -153,7 +141,6 @@ def main():
print("Saving...")
metadata = {}
metadata["model"] = args.model
metadata["chat_template"] = json.dumps(tokenizer.chat_template)
metadata["tokenizer_config"] = json.dumps(tokenizer_config)
save_prompt_cache(args.prompt_cache_file, cache, metadata)
+89 -23
View File
@@ -1,23 +1,43 @@
# Copyright © 2023-2024 Apple Inc.
import argparse
import sys
import mlx.core as mx
from .cli_ui import (
make_console,
make_corridor_prompt,
print_chat_help,
print_header_panel,
)
from .generate import stream_generate
from .models.cache import make_prompt_cache
from .sample_utils import make_sampler
from .utils import load
from .utils import load, sharded_load
DEFAULT_TEMP = 0.0
DEFAULT_TOP_P = 1.0
DEFAULT_XTC_PROBABILITY = 0.0
DEFAULT_XTC_THRESHOLD = 0.0
DEFAULT_SEED = None
DEFAULT_SEED = 0
DEFAULT_MAX_TOKENS = 256
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
def _print_chat_header(args, console):
rows = [("model", str(args.model))]
if args.adapter_path:
rows.append(("adapter", str(args.adapter_path)))
rows.append(("max tokens", f"{args.max_tokens:,}"))
if args.system_prompt:
sp = args.system_prompt
if len(sp) > 60:
sp = sp[:57] + "..."
rows.append(("system", sp))
print_header_panel(console, "mlx_lm.chat", rows)
def setup_arg_parser():
"""Set up and return the argument parser."""
parser = argparse.ArgumentParser(description="Chat with an LLM")
@@ -79,6 +99,11 @@ def setup_arg_parser():
default=None,
help="System prompt to be used for the chat template",
)
parser.add_argument(
"--pipeline",
action="store_true",
help="Use pipelining instead of tensor parallelism",
)
return parser
@@ -86,41 +111,74 @@ def main():
parser = setup_arg_parser()
args = parser.parse_args()
if args.seed is not None:
mx.random.seed(args.seed)
group = mx.distributed.init()
rank = group.rank()
pipeline_group = group if args.pipeline else None
tensor_group = group if not args.pipeline else None
model, tokenizer = load(
args.model,
adapter_path=args.adapter_path,
tokenizer_config={
"trust_remote_code": True if args.trust_remote_code else None
},
)
console = make_console()
def print_help():
print("The command list:")
print("- 'q' to exit")
print("- 'r' to reset the chat")
print("- 'h' to display these commands")
def rprint(*args, **kwargs):
if rank == 0:
print(*args, **kwargs)
mx.random.seed(args.seed)
if group.size() > 1:
if args.adapter_path:
parser.error("Adapters not supported in distributed mode")
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
else:
model, tokenizer = load(
args.model,
adapter_path=args.adapter_path,
tokenizer_config={
"trust_remote_code": True if args.trust_remote_code else None
},
)
if rank == 0:
_print_chat_header(args, console)
print_chat_help(console)
if rank == 0:
prompt_console = make_console(force_terminal=True, color_system="truecolor")
_draw_corridor_prompt = make_corridor_prompt(prompt_console)
else:
_draw_corridor_prompt = lambda: ""
print(f"[INFO] Starting chat session with {args.model}.")
print_help()
prompt_cache = make_prompt_cache(model, args.max_kv_size)
while True:
query = input(">> ")
prompt = _draw_corridor_prompt()
query = input(prompt)
if rank == 0:
# Cursor is now on the bottom-rule row; advance past it.
sys.stdout.write("\n")
sys.stdout.flush()
if query == "q":
if rank == 0:
console.print("[ui.muted]bye[/ui.muted]")
break
if query == "r":
prompt_cache = make_prompt_cache(model, args.max_kv_size)
if rank == 0:
console.print(
" [ui.good]reset[/ui.good] [ui.muted]context cleared[/ui.muted]"
)
continue
if query == "h":
print_help()
if rank == 0:
print_chat_help(console)
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)
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
)
last_response = None
for response in stream_generate(
model,
tokenizer,
@@ -137,8 +195,16 @@ def main():
),
prompt_cache=prompt_cache,
):
print(response.text, flush=True, end="")
print()
rprint(response.text, flush=True, end="")
last_response = response
rprint()
if rank == 0 and last_response is not None:
console.print(
f" [ui.muted]{last_response.generation_tokens} tokens · "
f"{last_response.generation_tps:.1f} tok/s · "
f"prompt {last_response.prompt_tps:.1f} tok/s · "
f"peak {last_response.peak_memory:.2f} GB[/ui.muted]"
)
if __name__ == "__main__":
View File
+345
View File
@@ -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
+53
View File
@@ -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}")
+172
View File
@@ -0,0 +1,172 @@
# Copyright © 2024 Apple Inc.
"""Shared UI helpers for the mlx_lm command-line tools.
Centralizes the rich-based panel/progress/prompt rendering used by the chat
and training entry points. The theme is hardcoded for a light terminal
background.
"""
import os
import re
import shutil
import sys
from rich.box import ROUNDED
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
Progress,
ProgressColumn,
TextColumn,
)
from rich.text import Text
from rich.theme import Theme
def _terminal_width(default: int = 120) -> int:
"""Best-effort terminal width.
Under launchers like ``mlx.launch`` the worker's stdout is a pipe, so
Rich's auto-detection falls back to 80 columns. Honor an explicit
``MLX_LM_WIDTH`` override, then ``COLUMNS``, then a real TTY query, and
finally a generous default that's nicer than 80 on modern terminals.
"""
for var in ("MLX_LM_WIDTH", "COLUMNS"):
value = os.environ.get(var)
if value and value.isdigit():
return int(value)
width = shutil.get_terminal_size(fallback=(0, 0)).columns
return width if width > 0 else default
def _make_theme() -> Theme:
return Theme(
{
"ui.strong": "bold #000000",
"ui.label": "#2a2a2a",
"ui.muted": "grey42",
"ui.heading": "bold #1a1a1a",
"ui.dim": "grey62",
"ui.accent": "bold purple",
"ui.border": "blue",
"ui.good": "bold green",
"ui.warn": "yellow",
"progress.percentage": "bold blue",
}
)
def make_console(**kwargs) -> Console:
"""Return a rich Console pre-loaded with the mlx_lm theme."""
kwargs.setdefault("highlight", False)
kwargs.setdefault("color_system", "truecolor")
kwargs.setdefault("width", _terminal_width())
return Console(theme=_make_theme(), **kwargs)
def print_header_panel(
console: Console, title: str, rows: list[tuple[str, str]]
) -> None:
"""Render the boxed header used by the chat and training entry points."""
label_w = max(len(k) for k, _ in rows)
body = "\n".join(
f" [ui.label]{k.ljust(label_w)}[/ui.label] [ui.strong]{v}[/ui.strong]"
for k, v in rows
)
console.print(
Panel(
body,
title=f"[ui.accent]{title}[/ui.accent]",
title_align="left",
border_style="ui.border",
box=ROUNDED,
padding=(0, 2),
)
)
def print_chat_help(console: Console) -> None:
console.print(
" [ui.label]commands[/ui.label] "
"[ui.strong]q[/ui.strong] [ui.muted]exit[/ui.muted] "
"[ui.strong]r[/ui.strong] [ui.muted]reset[/ui.muted] "
"[ui.strong]h[/ui.strong] [ui.muted]help[/ui.muted]"
)
def make_corridor_prompt(console: Console):
"""Return a callable that draws the chat input corridor.
The returned callable draws the top/bottom rules around the input line,
repositions the cursor onto the middle line, and returns the styled
"" prompt string. Pass that string to ``input()`` so readline treats
the marker as part of the prompt — otherwise backspace will erase it.
"""
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
def _readline_safe(text: str) -> str:
# Wrap escape sequences in \x01..\x02 so readline doesn't count
# them when computing the prompt's visible width.
return _ANSI_RE.sub(lambda m: f"\x01{m.group(0)}\x02", text)
def _draw() -> str:
width = console.width
dashes = "" * max(width - 1, 10)
with console.capture() as cap:
console.print(f"[ui.muted]{dashes}[/ui.muted]")
console.print()
console.print(f"[ui.muted]{dashes}[/ui.muted]")
sys.stdout.write(cap.get())
# Move the cursor up two rows back onto the blank middle line.
sys.stdout.write("\033[2A\r")
sys.stdout.flush()
with console.capture() as cap2:
console.print("[ui.accent][/ui.accent] ", end="")
return _readline_safe(cap2.get())
return _draw
class SquareBar(ProgressColumn):
"""Progress bar rendered with █/░ blocks plus eighth-block sub-precision."""
_EIGHTHS = "▏▎▍▌▋▊▉" # 1/8 .. 7/8
def __init__(self, bar_width: int = 40, complete_style: str = "blue"):
super().__init__()
self.bar_width = bar_width
self.complete_style = complete_style
def render(self, task):
if not task.total:
return Text("" * self.bar_width, style="ui.dim")
pct = min(max(task.completed / task.total, 0.0), 1.0)
total_eighths = int(pct * self.bar_width * 8)
full = total_eighths // 8
rem = total_eighths % 8
text = Text()
text.append("" * full, style=self.complete_style)
used = full
if rem > 0 and full < self.bar_width:
text.append(self._EIGHTHS[rem - 1], style=self.complete_style)
used += 1
text.append("" * (self.bar_width - used), style="ui.dim")
return text
def make_train_progress(console: Console, *, disable: bool = False) -> Progress:
return Progress(
TextColumn("[bold blue]train[/bold blue]"),
SquareBar(bar_width=30, complete_style="blue"),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TextColumn("[ui.muted]·[/ui.muted]"),
TextColumn(
"[bold blue]{task.completed:>5,}[/bold blue]"
"[ui.muted]/{task.total:,}[/ui.muted]"
),
console=console,
transient=False,
disable=disable,
)
+44 -22
View File
@@ -10,8 +10,7 @@ from mlx.utils import tree_map_with_path
from .utils import (
dequantize_model,
fetch_from_hub,
get_model_path,
load,
quantize_model,
save,
upload_to_hub,
@@ -19,10 +18,10 @@ from .utils import (
def mixed_quant_predicate_builder(
recipe: str, model: nn.Module
recipe: str, model: nn.Module, group_size: int = 64
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
mode = "affine"
high_bits = 6
group_size = 64
if recipe == "mixed_2_6":
low_bits = 2
@@ -34,7 +33,7 @@ def mixed_quant_predicate_builder(
elif recipe == "mixed_4_6":
low_bits = 4
else:
raise ValueError("Invalid quant recipe {recipe}")
raise ValueError(f"Invalid quant recipe {recipe}")
down_keys = [k for k, _ in model.named_modules() if "down_proj" in k]
if len(down_keys) == 0:
@@ -64,14 +63,16 @@ def mixed_quant_predicate_builder(
or index >= 7 * num_layers // 8
or (index - num_layers // 8) % 3 == 2
)
if "v_proj" in path and use_more_bits:
return {"group_size": group_size, "bits": high_bits}
if (
"v_proj" in path or "v_a_proj" in path or "v_b_proj" in path
) and use_more_bits:
return {"group_size": group_size, "bits": high_bits, "mode": mode}
if "down_proj" in path and use_more_bits:
return {"group_size": group_size, "bits": high_bits}
return {"group_size": group_size, "bits": high_bits, "mode": mode}
if "lm_head" in path:
return {"group_size": group_size, "bits": high_bits}
return {"group_size": group_size, "bits": high_bits, "mode": mode}
return {"group_size": group_size, "bits": low_bits}
return {"group_size": group_size, "bits": low_bits, "mode": mode}
return mixed_quant_predicate
@@ -85,8 +86,8 @@ def convert(
hf_path: str,
mlx_path: str = "mlx_model",
quantize: bool = False,
q_group_size: int = 64,
q_bits: int = 4,
q_group_size: Optional[int] = None,
q_bits: Optional[int] = None,
q_mode: str = "affine",
dtype: Optional[str] = None,
upload_repo: str = None,
@@ -108,16 +109,27 @@ def convert(
)
print("[INFO] Loading")
model_path, hf_path = get_model_path(hf_path, revision=revision)
model, config, tokenizer = fetch_from_hub(
model_path, lazy=True, trust_remote_code=trust_remote_code
model, tokenizer, config = load(
hf_path,
revision=revision,
return_config=True,
tokenizer_config={"trust_remote_code": trust_remote_code},
lazy=True,
)
if isinstance(quant_predicate, str):
quant_predicate = mixed_quant_predicate_builder(quant_predicate, model)
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)
@@ -153,11 +165,10 @@ def convert(
save(
mlx_path,
model_path,
hf_path,
model,
tokenizer,
config,
hf_repo=hf_path,
)
if upload_repo is not None:
@@ -175,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."
)
@@ -183,17 +199,23 @@ def configure_parser() -> argparse.ArgumentParser:
"-q", "--quantize", help="Generate a quantized model.", action="store_true"
)
parser.add_argument(
"--q-group-size", help="Group size for quantization.", type=int, default=64
"--q-group-size",
help="Group size for quantization.",
type=int,
default=None,
)
parser.add_argument(
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
"--q-bits",
help="Bits per weight for quantization.",
type=int,
default=None,
)
parser.add_argument(
"--q-mode",
help="The quantization mode.",
type=str,
default="affine",
choices=["affine", "mxfp4"],
choices=["affine", "mxfp4", "nvfp4", "mxfp8"],
)
parser.add_argument(
"--quant-predicate",
+107 -38
View File
@@ -12,7 +12,7 @@ import logging
import os
from importlib.metadata import version
from pathlib import Path
from typing import Any, Optional
from typing import Any, Callable, Optional
import lm_eval
import mlx.core as mx
@@ -20,13 +20,14 @@ import mlx.nn as nn
import numpy as np
from lm_eval.api.model import LM
from lm_eval.api.registry import register_model
from lm_eval.models import huggingface
from tqdm import tqdm
from .generate import stream_generate
from .models.base import create_causal_mask
from .generate import batch_generate
from .models.cache import make_prompt_cache
from .utils import common_prefix_len, load
from .sample_utils import make_sampler
from .utils import load
DEFAULT_MAX_TOKENS = 8192
def _rstrip_until(s, untils):
@@ -37,6 +38,13 @@ def _rstrip_until(s, untils):
return s[: min(f)]
def _lstrip(s, pattern):
"""Truncate the prefix of the string after the first occurrence of pattern."""
if (idx := s.find(pattern)) != -1:
return s[idx + len(pattern) :]
return s
def _pad_inputs(inputs):
lengths = np.array([len(x) for x in inputs])
maxlen = lengths.max()
@@ -63,26 +71,28 @@ def chat_template_fn(**extra_kwargs):
@register_model("mlxlm")
class MLXLM(LM):
tokenizer_name = huggingface.HFLM.tokenizer_name
apply_chat_template = chat_template_fn()
def __init__(
self,
path_or_hf_repo: str,
max_tokens: Optional[int] = None,
batch_size: int = 8,
use_chat_template: Optional[bool] = None,
trust_remote_code: bool = False,
sampler: Optional[Callable[[mx.array], mx.array]] = None,
) -> None:
super().__init__()
tokenizer_config = {"trust_remote_code": True if trust_remote_code else None}
self._model, self.tokenizer = load(
path_or_hf_repo, tokenizer_config=tokenizer_config
)
self._max_tokens = max_tokens or self.tokenizer.model_max_length
self._batch_size = 8
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 _process_prompt(self, prompt, step_size: int = 2048):
prompt = mx.array(prompt)[None]
@@ -99,30 +109,28 @@ class MLXLM(LM):
inputs, targets = inputs[..., :-1], inputs[..., 1:]
cache = cache or make_prompt_cache(self._model)
lengths += cache[0].offset
offset = 0
scores, is_greedy = [], []
for i in range(0, inputs.shape[1], step_size):
inp = inputs[:, i : i + step_size]
T = inp.shape[1]
offset = cache[0].offset
mask = create_causal_mask(T, offset, lengths=lengths)
logits = self._model(inp, cache=cache, mask=mask)
logits = self._model(inp, cache=cache)
log_probs = nn.log_softmax(logits.astype(mx.float32))
score = mx.take_along_axis(
log_probs, targets[:, i : i + step_size, mx.newaxis], axis=-1
)[..., 0]
ig = targets[:, i : i + step_size] == mx.argmax(logits, axis=-1)
ig = mx.where(mx.arange(T) + offset < lengths[:, None], ig, False)
ig = mx.where(mx.arange(offset, T + offset) < lengths[:, None], ig, False)
mx.eval(score, ig)
mx.clear_cache()
is_greedy.append(ig)
scores.append(score)
offset += T
scores = mx.concatenate(scores, axis=1)
is_greedy = mx.concatenate(is_greedy, axis=1)
@@ -137,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
@@ -185,7 +197,8 @@ class MLXLM(LM):
max_completed_l = max(len(s) for s in full_sequences)
# compute truncation length
truncation = max(0, max_completed_l - self._max_tokens - 1)
max_tokens = self._max_tokens or DEFAULT_MAX_TOKENS
truncation = max(0, max_completed_l - max_tokens - 1)
orig_prefix_l = len(prefix)
prefix_l = max(len(prefix) - truncation, 0)
prefix = prefix[len(prefix) - prefix_l :]
@@ -307,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}
completions = []
for context, opt in tqdm(zip(contexts, options), total=len(contexts)):
until = opt["until"]
context = self.tokenizer.encode(
# Tokenize all contexts
contexts = [
self.tokenizer.encode(
context, add_special_tokens=not self.use_chat_template
)
max_tokens = min(
opt.get("max_gen_tokens", self._max_tokens),
self.tokenizer.model_max_length - len(context),
)
text = ""
for response in stream_generate(
self._model, self.tokenizer, prompt=context, max_tokens=max_tokens
):
text += response.text
if any(u in text for u in until):
text = _rstrip_until(text, until)
completions.append(text)
break
else:
completions.append(text)
for context in contexts
]
# TODO consider multi-token, per-prompt stop conditions
max_tokens = [
self._max_tokens or opt.get("max_gen_tokens", DEFAULT_MAX_TOKENS)
for opt in options
]
completions = batch_generate(
model=self._model,
tokenizer=self.tokenizer,
prompts=contexts,
max_tokens=max_tokens,
verbose=True,
sampler=self._sampler,
).texts
for e, (text, opt) in enumerate(zip(completions, options)):
completions[e] = _rstrip_until(text, opt["until"])
if self.tokenizer.has_thinking:
completions[e] = _lstrip(text, self.tokenizer.think_end)
# Gather the completions
if group.size() > 1:
with mx.stream(mx.cpu):
pad_to = (total_requests + group.size() - 1) // group.size()
pad = pad_to - len(completions)
completions = [list(c.encode("utf-8")) for c in completions]
max_len = mx.array(max(len(c) for c in completions))
max_len = mx.distributed.all_max(max_len).item()
lengths = mx.array([len(c) for c in completions] + [0] * pad)
completions = mx.array(
[c + [0] * (max_len - len(c)) for c in completions]
+ [[0] * max_len] * pad,
mx.uint8,
)
completions = (
mx.distributed.all_gather(completions[None])
.swapaxes(0, 1)
.flatten(0, 1)
.tolist()
)
lengths = (
mx.distributed.all_gather(lengths[None])
.swapaxes(0, 1)
.flatten(0, 1)
.tolist()
)
completions = completions[:total_requests]
lengths = lengths[:total_requests]
completions = [
bytearray(c[:l]).decode() for c, l in zip(completions, lengths)
]
return completions
@@ -350,7 +408,9 @@ def main():
parser.add_argument(
"--max-tokens",
type=int,
help="Maximum nunber of tokens to generate. Defaults to the model's max context length.",
help="Maximum number of tokens to generate. When set, this value takes"
" precedence over task specific defaults.",
default=None,
)
parser.add_argument(
"--limit",
@@ -392,7 +452,9 @@ def main():
action="store_true",
help="Enable trusting remote code for tokenizer",
)
parser.add_argument("--temp", type=float, default=0.0, help="Sampling temperature")
parser.add_argument("--top-p", type=float, default=1.0, help="Sampling top-p")
parser.add_argument("--top-k", type=int, default=0, help="Sampling top-k")
args = parser.parse_args()
output_dir = Path(args.output_dir)
@@ -409,11 +471,18 @@ def main():
if world.size() > 1 and world.rank() == 0:
print(f"Evaluating with {world.size()} nodes")
sampler = make_sampler(
temp=args.temp,
top_p=args.top_p,
top_k=args.top_k,
)
lm = MLXLM(
args.model,
max_tokens=args.max_tokens,
batch_size=args.batch_size,
use_chat_template=args.apply_chat_template,
trust_remote_code=args.trust_remote_code,
sampler=sampler,
)
MLXLM.apply_chat_template = chat_template_fn(**args.chat_template_args)
@@ -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])
+8 -2
View File
@@ -15,7 +15,10 @@ prompt_cache = make_prompt_cache(model)
# User turn
prompt = "Hi my name is <Name>."
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
)
# Assistant response
response = generate(
@@ -29,7 +32,10 @@ response = generate(
# User turn
prompt = "What's my name?"
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
)
# Assistant response
response = generate(
+2 -1
View File
@@ -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
+4 -2
View File
@@ -1,5 +1,5 @@
# The path to the local model directory or Hugging Face repo.
model: "mlx-community/Llama-3.2-1B-Instruct"
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
# Whether or not to train (boolean)
train: true
@@ -47,6 +47,9 @@ steps_per_report: 10
# Number of training steps between validations.
steps_per_eval: 200
# Number of micro-steps to accumulate before each optimizer update.
grad_accumulation_steps: 1
# Load path to resume training with the given adapter weights.
resume_adapter_file: null
@@ -90,4 +93,3 @@ lora_parameters:
# valid_split: "train[-100:]"
# prompt_feature: "text"
# completion_feature: "summary"
@@ -0,0 +1,40 @@
from openai import OpenAI
client = OpenAI(
api_key="not-needed",
base_url="http://localhost:8080/v1",
)
model = "mlx-community/Qwen3-4B-Thinking-2507-4bit"
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
# Non-streaming example
response = client.chat.completions.create(
model=model, messages=messages, max_tokens=2048
)
reasoning = response.choices[0].message.reasoning
content = response.choices[0].message.content
print("=== reasoning ===\n")
print(f"\033[37m{reasoning}\033[0m")
print("=== content ===\n")
print(content)
# Streaming example
stream = client.chat.completions.create(
model=model,
messages=messages,
stream=True,
max_tokens=2048,
)
for chunk in stream:
if (reasoning := chunk.choices[0].delta.reasoning) is not None:
print(f"\033[37m{reasoning}\033[0m", end="")
if (content := chunk.choices[0].delta.content) is not None:
print(f"{content}", end="")
print()
+3 -1
View File
@@ -8,11 +8,13 @@ To run, first start the server:
Then run this script.
"""
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
model = "mlx-community/qwen3-4b-4bit-DWQ"
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
tools = [
-135
View File
@@ -1,135 +0,0 @@
# Copyright © 2024 Apple Inc.
"""
Run with:
```
mlx.launch \
--hostfile /path/to/hosts.json \
/path/to/pipeline_generate.py \
--prompt "hello world"
```
Make sure you can run MLX over MPI on two hosts. For more information see the
documentation:
https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
"""
import argparse
import json
import resource
from pathlib import Path
import mlx.core as mx
from huggingface_hub import snapshot_download
from mlx.utils import tree_flatten
from mlx_lm import load, stream_generate
from mlx_lm.utils import load_model, load_tokenizer
# Needed for 8 bit model
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
def download(repo: str, allow_patterns: list[str]) -> Path:
return Path(
snapshot_download(
repo,
allow_patterns=allow_patterns,
)
)
def shard_and_load(repo):
# Get model path with everything but weight safetensors
model_path = download(
args.model,
allow_patterns=["*.json", "*.py", "tokenizer.model", "*.tiktoken", "*.txt"],
)
# Lazy load and shard model to figure out
# which weights we need
model, config = load_model(model_path, lazy=True, strict=False)
group = mx.distributed.init()
rank = group.rank()
model.model.pipeline(group)
# Figure out which files we need for the local shard
with open(model_path / "model.safetensors.index.json", "r") as fid:
weight_index = json.load(fid)["weight_map"]
local_files = set()
for k, _ in tree_flatten(model.parameters()):
local_files.add(weight_index[k])
# Download weights for local shard
download(args.model, allow_patterns=local_files)
# Load and shard the model, and load the weights
tokenizer = load_tokenizer(
model_path,
{"trust_remote_code": True},
eos_token_ids=config.get("eos_token_id", None),
)
model, _ = load_model(model_path, lazy=True, strict=False)
model.model.pipeline(group)
mx.eval(model.parameters())
# Synchronize processes before generation to avoid timeout if downloading
# model for the first time.
mx.eval(mx.distributed.all_sum(mx.array(1.0), stream=mx.cpu))
return model, tokenizer
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
parser.add_argument(
"--model",
default="mlx-community/DeepSeek-R1-3bit",
help="HF repo or path to local model.",
)
parser.add_argument(
"--prompt",
"-p",
default="Write a quicksort in C++.",
help="Message to be processed by the model ('-' reads from stdin)",
)
parser.add_argument(
"--max-tokens",
"-m",
type=int,
default=256,
help="Maximum number of tokens to generate",
)
args = parser.parse_args()
group = mx.distributed.init()
rank = group.rank()
def rprint(*args, **kwargs):
if rank == 0:
print(*args, **kwargs)
model, tokenizer = shard_and_load(args.model)
messages = [{"role": "user", "content": args.prompt}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
for response in stream_generate(
model, tokenizer, prompt, max_tokens=args.max_tokens
):
rprint(response.text, end="", flush=True)
rprint()
rprint("=" * 10)
rprint(
f"Prompt: {response.prompt_tokens} tokens, "
f"{response.prompt_tps:.3f} tokens-per-sec"
)
rprint(
f"Generation: {response.generation_tokens} tokens, "
f"{response.generation_tps:.3f} tokens-per-sec"
)
rprint(f"Peak memory: {response.peak_memory:.3f} GB")
+86
View File
@@ -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")
+9 -8
View File
@@ -6,7 +6,7 @@ from mlx_lm import generate, load
from mlx_lm.models.cache import make_prompt_cache
# Specify the checkpoint
checkpoint = "mlx-community/Qwen2.5-32B-Instruct-4bit"
checkpoint = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
# Load the corresponding model and tokenizer
model, tokenizer = load(path_or_hf_repo=checkpoint)
@@ -31,7 +31,9 @@ prompt = "Multiply 12234585 and 48838483920."
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, tools=list(tools.values())
messages,
add_generation_prompt=True,
tools=list(tools.values()),
)
prompt_cache = make_prompt_cache(model)
@@ -47,12 +49,11 @@ response = generate(
)
# Parse the tool call:
# (Note, the tool call format is model specific)
tool_open = "<tool_call>"
tool_close = "</tool_call>"
start_tool = response.find(tool_open) + len(tool_open)
end_tool = response.find(tool_close)
tool_call = json.loads(response[start_tool:end_tool].strip())
# - The tool call format is model specific.
# - The tokenizer's tool parser expects tool call text to be already extracted.
start_tool = response.find(tokenizer.tool_call_start) + len(tokenizer.tool_call_start)
end_tool = response.find(tokenizer.tool_call_end)
tool_call = tokenizer.tool_parser(response[start_tool:end_tool].strip())
tool_result = tools[tool_call["name"]](**tool_call["arguments"])
# Put the tool result in the prompt
+14 -21
View File
@@ -4,10 +4,9 @@ from pathlib import Path
from mlx.utils import tree_flatten, tree_unflatten
from .gguf import convert_to_gguf
from .tuner.utils import dequantize, load_adapters
from .utils import (
fetch_from_hub,
get_model_path,
dequantize_model,
load,
save,
upload_to_hub,
)
@@ -40,8 +39,8 @@ def parse_arguments() -> argparse.Namespace:
default=None,
)
parser.add_argument(
"--de-quantize",
help="Generate a de-quantized model.",
"--dequantize",
help="Generate a dequantized model.",
action="store_true",
)
parser.add_argument(
@@ -62,14 +61,12 @@ def main() -> None:
print("Loading pretrained model")
args = parse_arguments()
model_path, hf_path = get_model_path(args.model)
model, config, tokenizer = fetch_from_hub(model_path)
model.freeze()
model = load_adapters(model, args.adapter_path)
model, tokenizer, config = load(
args.model, adapter_path=args.adapter_path, return_config=True
)
fused_linears = [
(n, m.fuse(de_quantize=args.de_quantize))
(n, m.fuse(dequantize=args.dequantize))
for n, m in model.named_modules()
if hasattr(m, "fuse")
]
@@ -77,19 +74,19 @@ def main() -> None:
if fused_linears:
model.update_modules(tree_unflatten(fused_linears))
if args.de_quantize:
print("De-quantizing model")
model = dequantize(model)
if args.dequantize:
print("Dequantizing model")
model = dequantize_model(model)
config.pop("quantization", None)
config.pop("quantization_config", None)
save_path = Path(args.save_path)
save(
save_path,
model_path,
args.model,
model,
tokenizer,
config,
hf_repo=hf_path,
donate_model=False,
)
@@ -100,13 +97,9 @@ def main() -> None:
f"Model type {model_type} not supported for GGUF conversion."
)
weights = dict(tree_flatten(model.parameters()))
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
if args.upload_repo is not None:
if hf_path is None:
raise ValueError(
"Must provide original Hugging Face repo to upload local model."
)
upload_to_hub(args.save_path, args.upload_repo)
+1219 -39
View File
File diff suppressed because it is too large Load Diff
+65 -27
View File
@@ -12,6 +12,7 @@ import mlx.optimizers as optim
import numpy as np
import yaml
from .cli_ui import make_console, print_header_panel
from .tuner.callbacks import get_reporting_callbacks
from .tuner.datasets import CacheDataset, load_dataset
from .tuner.trainer import TrainingArgs, TrainingCallback, evaluate, train
@@ -21,7 +22,13 @@ from .tuner.utils import (
load_adapters,
print_trainable_parameters,
)
from .utils import load, save_config
from .utils import _parse_size, load, save_config
def printf(*args, **kwargs):
if mx.distributed.init().rank() == 0:
print(*args, **kwargs)
yaml_loader = yaml.SafeLoader
yaml_loader.add_implicit_resolver(
@@ -40,7 +47,7 @@ yaml_loader.add_implicit_resolver(
)
CONFIG_DEFAULTS = {
"model": "mlx_model",
"model": "Qwen/Qwen3-0.6b",
"train": False,
"fine_tune_type": "lora",
"optimizer": "adam",
@@ -51,7 +58,7 @@ CONFIG_DEFAULTS = {
"sgd": {},
"adafactor": {},
},
"data": "data/",
"data": "mlx-community/WikiSQL",
"seed": 0,
"num_layers": 16,
"batch_size": 4,
@@ -68,10 +75,11 @@ CONFIG_DEFAULTS = {
"max_seq_length": 2048,
"config": None,
"grad_checkpoint": False,
"grad_accumulation_steps": 1,
"clear_cache_threshold": 0,
"lr_schedule": None,
"lora_parameters": {"rank": 8, "dropout": 0.0, "scale": 20.0},
"mask_prompt": False,
"wandb": None, # will be deprecated in a future release
"report_to": None,
"project_name": None,
}
@@ -142,6 +150,11 @@ def build_parser():
type=int,
help="Number of training steps between validations.",
)
parser.add_argument(
"--grad-accumulation-steps",
type=int,
help="Number of steps to accumulate before each optimizer update.",
)
parser.add_argument(
"--resume-adapter-file",
type=str,
@@ -185,14 +198,11 @@ def build_parser():
help="Use gradient checkpointing to reduce memory use.",
default=None,
)
parser.add_argument( # will be deprecated in a future release
"--wandb",
type=str,
default=None,
help=(
"The 'wandb' argument is deprecated and will be removed in a future release. "
"Use 'report_to: wandb' and 'project_name' in the configuration instead."
),
parser.add_argument(
"--clear-cache-threshold",
type=_parse_size,
default=0,
help="Clear the allocator cache between steps if it grows too large.",
)
parser.add_argument(
"--report-to",
@@ -243,7 +253,7 @@ def train_model(
# Resume from weights if provided
if args.resume_adapter_file is not None:
print(f"Loading fine-tuned weights from {args.resume_adapter_file}")
printf(f"Loading fine-tuned weights from {args.resume_adapter_file}")
model.load_weights(args.resume_adapter_file, strict=False)
print_trainable_parameters(model)
@@ -265,6 +275,7 @@ def train_model(
adapter_file=adapter_file,
max_seq_length=args.max_seq_length,
grad_checkpoint=args.grad_checkpoint,
grad_accumulation_steps=args.grad_accumulation_steps,
)
# Initialize the selected optimizer
@@ -287,6 +298,8 @@ def train_model(
opt = opt_class(learning_rate=lr, **optimizer_config)
_print_run_header(args)
# Train model
train(
model=model,
@@ -298,30 +311,54 @@ def train_model(
)
def _print_run_header(args):
rank = mx.distributed.init().rank()
if rank != 0:
return
type_label = args.fine_tune_type
if args.fine_tune_type in ("lora", "dora"):
rank = args.lora_parameters.get("rank", "?")
type_label = f"{args.fine_tune_type} · {args.num_layers} layers · rank {rank}"
elif args.fine_tune_type == "full":
type_label = f"full · {args.num_layers} layers"
lr = (
args.learning_rate
if isinstance(args.learning_rate, (int, float))
else "schedule"
)
lr_str = f"{lr:.1e}" if isinstance(lr, (int, float)) else lr
rows = [
("model", str(args.model)),
("type", type_label),
("dataset", str(args.data)),
("optimizer", f"{args.optimizer} · lr {lr_str}"),
("batch · iters", f"{args.batch_size} · {args.iters:,}"),
("max seq", f"{args.max_seq_length:,}"),
]
print_header_panel(make_console(), "mlx_lm.lora", rows)
def evaluate_model(args, model: nn.Module, test_set):
rank = mx.distributed.init().rank()
test_loss = evaluate(
model=model,
dataset=CacheDataset(test_set),
batch_size=args.batch_size,
num_batches=args.test_batches,
max_seq_length=args.max_seq_length,
progress=(rank == 0),
)
test_ppl = math.exp(test_loss)
print(f"Test loss {test_loss:.3f}, Test ppl {test_ppl:.3f}.")
printf(f"Test loss {test_loss:.3f}, Test ppl {test_ppl:.3f}.")
def run(args, training_callback: TrainingCallback = None):
np.random.seed(args.seed)
if args.wandb is not None:
warnings.warn(
"The 'wandb' argument is deprecated and will be removed in a future release. "
"Use 'report_to: wandb' and 'project_name' in the configuration instead.",
DeprecationWarning,
)
args.report_to = "wandb"
args.project_name = args.wandb
training_callback = get_reporting_callbacks(
args.report_to,
project_name=args.project_name,
@@ -329,10 +366,10 @@ def run(args, training_callback: TrainingCallback = None):
config=vars(args),
)
print("Loading pretrained model")
printf("Loading pretrained model")
model, tokenizer = load(args.model, tokenizer_config={"trust_remote_code": True})
print("Loading datasets")
printf("Loading datasets")
train_set, valid_set, test_set = load_dataset(args, tokenizer)
if args.test and not args.train:
@@ -341,13 +378,13 @@ def run(args, training_callback: TrainingCallback = None):
load_adapters(model, args.adapter_path)
elif args.train:
print("Training")
printf("Training")
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")
printf("Testing")
evaluate_model(args, model, test_set)
@@ -355,10 +392,11 @@ def main():
os.environ["TOKENIZERS_PARALLELISM"] = "true"
parser = build_parser()
args = parser.parse_args()
config = args.config
args = vars(args)
if config:
print("Loading configuration file", config)
printf("Loading configuration file", config)
with open(config, "r") as file:
config = yaml.load(file, yaml_loader)
# Prefer parameters from command-line arguments
+263
View File
@@ -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
+43
View File
@@ -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)
+7 -14
View File
@@ -9,6 +9,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import ConcatenateKVCache, KVCache
from .rope_utils import initialize_rope
@@ -50,7 +51,7 @@ class FusedLoRALinear(nn.Module):
]
self.lora_b = [mx.zeros((r, od)) for od in output_dims]
def fuse(self, de_quantize: bool = False):
def fuse(self, dequantize: bool = False):
linear = self.linear
weight = linear.weight
is_quantized = isinstance(linear, FusedQuantizedLinear)
@@ -79,7 +80,7 @@ class FusedLoRALinear(nn.Module):
delta = mx.concatenate(deltas, axis=0)
fused_linear.weight = weight + delta
if is_quantized and not de_quantize:
if is_quantized and not dequantize:
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
return fused_linear
@@ -262,11 +263,6 @@ class KVReuseAttention(nn.Module):
return self.out_proj(output)
@partial(mx.compile, shapeless=True)
def _swiglu(g, x):
return nn.silu(g) * x
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
@@ -281,7 +277,7 @@ class MLP(nn.Module):
def __call__(self, x) -> mx.array:
g = self.gate_proj(x)
x = self.up_proj(x)
return self.down_proj(_swiglu(g, x))
return self.down_proj(swiglu(g, x))
class TransformerBlock(nn.Module):
@@ -350,18 +346,16 @@ class AFMModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embedding(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
cache[-1] = ConcatenateKVCache()
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -382,10 +376,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
out = self.model.embedding.as_linear(out)
return out
+405
View File
@@ -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
+195
View File
@@ -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
+43 -18
View File
@@ -6,8 +6,9 @@ from typing import Any, List, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import CacheList, KVCache, MambaCache, RotatingKVCache
from .cache import ArraysCache, CacheList, KVCache, RotatingKVCache
@dataclass
@@ -96,7 +97,10 @@ class Attention(nn.Module):
k = k.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
v = v.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
if cache is not None:
if cache is None:
cache = (None, None)
if cache[0] is not None:
offset = cache[1].offset
last_k, last_v = cache[0][0], cache[0][1]
else:
@@ -110,7 +114,7 @@ class Attention(nn.Module):
q = self.rope(q, offset=offset)
k = self.rope(k, offset=offset)
if cache is not None:
if cache[0] is not None:
k, v = cache[1].update_and_fetch(k, v)
if L > 0:
cache[0][0] = k_init[:, :, -1:, :]
@@ -137,7 +141,7 @@ class MLP(nn.Module):
)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class DecoderLayer(nn.Module):
@@ -167,17 +171,40 @@ class BaichuanModel(nn.Module):
self.layers = [DecoderLayer(config, i) for i in range(config.num_hidden_layers)]
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def __call__(
self, inputs: mx.array, mask: mx.array = None, cache: Any = None
) -> mx.array:
self.sliding_window = config.sliding_window
self.first_swa_idx = None
if config.sliding_window_layers:
self.first_swa_idx = config.sliding_window_layers[0]
self.first_global_idx = None
self.swa_layers = set(config.sliding_window_layers)
for i in range(config.num_hidden_layers):
if i in self.swa_layers:
continue
self.first_global_idx = i
break
def __call__(self, inputs: mx.array, cache: Any = None) -> mx.array:
x = self.embed_tokens(inputs)
if mask is None:
if cache is not None:
c = [cache[0][1]]
mask = create_attention_mask(x, c)
if cache is None:
cache = [None] * len(self.layers)
for layer, c in zip(self.layers, cache):
cache = [(None, None)] * len(self.layers)
if self.first_global_idx is None:
c_global = None
else:
c_global = cache[self.first_global_idx][1]
if self.first_swa_idx is None:
c_swa = None
else:
c_swa = cache[self.first_swa_idx][1]
global_mask = create_attention_mask(x, c_global)
swa_mask = create_attention_mask(x, c_swa, window_size=self.sliding_window)
for l, (layer, c) in enumerate(zip(self.layers, cache)):
mask = swa_mask if l in self.swa_layers else global_mask
x = layer(x, mask, c)
return self.norm(x)
@@ -196,7 +223,7 @@ class Model(nn.Module):
caches = []
for i, layer in enumerate(self.model.layers):
is_swa = i in self.config.sliding_window_layers
conv_cache = MambaCache()
conv_cache = ArraysCache(size=2)
if is_swa:
kv_cache = RotatingKVCache(max_size=self.config.sliding_window)
else:
@@ -215,10 +242,8 @@ class Model(nn.Module):
weights["lm_head.weight"] = w
return weights
def __call__(
self, inputs: mx.array, mask: mx.array = None, cache: Any = None
) -> mx.array:
outputs = self.model(inputs, mask, cache)
def __call__(self, inputs: mx.array, cache: Any = None) -> mx.array:
outputs = self.model(inputs, cache)
return self.lm_head(outputs)
@property
+137 -58
View File
@@ -1,11 +1,13 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@@ -34,6 +36,25 @@ class ModelArgs(BaseModelArgs):
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):
@@ -56,12 +77,13 @@ class BailingMoeMLP(nn.Module):
)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class BailingMoeAttention(nn.Module):
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
@@ -78,8 +100,14 @@ class BailingMoeAttention(nn.Module):
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(
self.head_dim,
rope_dim,
args.rope_theta,
traditional=False,
scaling_config=args.rope_scaling,
@@ -110,6 +138,10 @@ class BailingMoeAttention(nn.Module):
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)
@@ -126,32 +158,77 @@ class BailingMoeAttention(nn.Module):
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, config):
def __init__(self, args: ModelArgs):
super().__init__()
self.config = config
self.top_k = config.num_experts_per_tok
self.num_experts = config.num_experts
self.norm_topk_prob = config.norm_topk_prob
self.gating_dim = config.hidden_size
self.norm_topk_prob = args.norm_topk_prob
self.gate_proj = nn.Linear(self.gating_dim, self.num_experts, bias=False)
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
def __call__(self, hidden_states):
B, L, D = hidden_states.shape
x = hidden_states.reshape(-1, D)
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
logits = self.gate_proj(x)
scores = mx.softmax(logits, axis=-1, precise=True)
topk_idx = mx.argpartition(scores, kth=-self.top_k, axis=-1)[..., -self.top_k :]
topk_scores = mx.take_along_axis(scores, topk_idx, axis=-1)
if self.top_k > 1 and self.norm_topk_prob:
denom = mx.sum(topk_scores, axis=-1, keepdims=True)
topk_scores = topk_scores / mx.maximum(denom, 1e-9)
return topk_idx, topk_scores
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):
@@ -159,42 +236,32 @@ class BailingMoeSparseMoeBlock(nn.Module):
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(config=args)
if args.num_shared_experts > 0:
self.shared_experts = BailingMoeMLP(
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=args.moe_intermediate_size * args.num_shared_experts,
intermediate_size=shared_dim * args.num_shared_experts,
)
else:
self.shared_experts = None
def __call__(self, hidden_states):
batch_size, seq_len, hidden_dim = hidden_states.shape
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:
identity = hidden_states
x = hidden_states.reshape(-1, hidden_dim)
expert_indices, expert_weights = self.gate(hidden_states)
expert_outputs = self.switch_mlp(x, expert_indices)
weighted_output = mx.sum(expert_outputs * expert_weights[..., None], axis=-2)
output = weighted_output.reshape(batch_size, seq_len, hidden_dim)
if self.shared_experts is not None:
output = output + self.shared_experts(hidden_states)
return output
out = out + self.shared_experts(x)
return out
class BailingMoeDecoderLayer(nn.Module):
@@ -239,17 +306,15 @@ class BailingMoeModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
h = self.word_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, c)
@@ -263,18 +328,25 @@ class Model(nn.Module):
self.norm_head = args.norm_head
self.model_type = args.model_type
self.model = BailingMoeModel(args)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
if not args.tie_word_embeddings:
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.model(inputs, mask, cache)
return self.lm_head(h)
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
@@ -311,12 +383,19 @@ class Model(nn.Module):
@property
def quant_predicate(self):
def predicate(path, _):
if path.endswith("mlp.gate"):
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
+595
View File
@@ -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
+31 -27
View File
@@ -7,8 +7,6 @@ from typing import Any, Optional
import mlx.core as mx
from mlx.utils import tree_map
from .cache import QuantizedKVCache
@dataclass
class BaseModelArgs:
@@ -27,7 +25,8 @@ def create_causal_mask(
N: int,
offset: int = 0,
window_size: Optional[int] = None,
lengths: Optional[mx.array] = None,
right_padding: Optional[mx.array] = None,
left_padding: Optional[mx.array] = None,
):
rinds = mx.arange(offset + N)
linds = mx.arange(offset, offset + N) if offset else rinds
@@ -36,33 +35,30 @@ def create_causal_mask(
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)
if right_padding is not None:
mask = mask & (rinds < mx.expand_dims((offset + N) - right_padding, (1, 2, 3)))
if left_padding is not None:
mask = mask & (mx.expand_dims(left_padding, (1, 2, 3)) <= rinds)
return mask
def create_attention_mask(
h: mx.array, cache: Optional[Any] = None, return_array: bool = False
h, cache=None, window_size: Optional[int] = None, return_array: bool = False
):
T = h.shape[1]
if T > 1:
offset = 0
window_size = None
if cache is not None and cache[0] is not None:
c = cache[0]
offset = c.offset
if hasattr(c, "max_size"):
window_size = c.max_size
offset = min(window_size, offset)
return_array = return_array or offset + T > window_size
if return_array:
return create_causal_mask(T, offset, window_size=window_size)
else:
return "causal"
else:
mask = None
return mask
N = h.shape[1]
if cache and hasattr(cache, "make_mask"):
return cache.make_mask(N, return_array=return_array, window_size=window_size)
if N == 1:
return None
if return_array or (window_size and N > window_size):
return create_causal_mask(N, window_size=window_size)
return "causal"
def create_ssm_mask(h, cache=None):
if cache and hasattr(cache, "make_mask"):
return cache.make_mask(h.shape[1])
return None
def quantized_scaled_dot_product_attention(
@@ -116,8 +112,11 @@ def scaled_dot_product_attention(
cache,
scale: float,
mask: Optional[mx.array],
sinks: Optional[mx.array] = None,
) -> mx.array:
if isinstance(cache, QuantizedKVCache):
if hasattr(cache, "bits"):
if sinks is not None:
raise ValueError("Quantized SDPA does not support attention sinks.")
return quantized_scaled_dot_product_attention(
queries,
keys,
@@ -129,5 +128,10 @@ def scaled_dot_product_attention(
)
else:
return mx.fast.scaled_dot_product_attention(
queries, keys, values, scale=scale, mask=mask
queries,
keys,
values,
scale=scale,
mask=mask,
sinks=sinks,
)
+4 -12
View File
@@ -93,11 +93,6 @@ class Attention(nn.Module):
return output
@partial(mx.compile, shapeless=True)
def relu2(x):
return mx.square(nn.relu(x))
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
@@ -116,7 +111,7 @@ class MLP(nn.Module):
self.ffn_sub_norm = nn.RMSNorm(args.intermediate_size, eps=args.rms_norm_eps)
def __call__(self, x) -> mx.array:
x = relu2(self.gate_proj(x)) * self.up_proj(x)
x = nn.relu2(self.gate_proj(x)) * self.up_proj(x)
x = self.ffn_sub_norm(x)
x = self.down_proj(x)
return x
@@ -163,17 +158,15 @@ class LlamaModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -192,10 +185,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+1244 -31
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -109,7 +110,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
def __call__(self, x):
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
@@ -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
+9 -19
View File
@@ -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,11 +84,6 @@ class Attention(nn.Module):
if cache is not None:
keys, values = cache.update_and_fetch(keys, values)
if self.use_sliding_window and isinstance(mask, mx.array):
key_len = keys.shape[-2]
if mask.shape[-1] != key_len:
mask = mask[..., -key_len:]
# TODO: maybe remove cast once fused mask is supported since attention
# may be in higher precision
sdpa_type = mx.float32 if queries.dtype == mx.float16 else queries.dtype
@@ -111,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):
@@ -148,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)
@@ -160,7 +157,6 @@ class CohereModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
@@ -168,10 +164,9 @@ class CohereModel(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
if mask is None:
j = self.args.sliding_window_pattern
full_mask = create_attention_mask(h, cache[j - 1 : j])
sliding_window_mask = create_attention_mask(h, cache)
j = self.args.sliding_window_pattern
full_mask = create_attention_mask(h, cache[j - 1])
swa_mask = create_attention_mask(h, cache[0], window_size=self.window_size)
for i, (layer, c) in enumerate(zip(self.layers, cache)):
is_global = (
@@ -179,13 +174,9 @@ class CohereModel(nn.Module):
== self.args.sliding_window_pattern - 1
)
local_mask = mask
if mask is None and is_global:
local_mask = full_mask
elif mask is None:
local_mask = sliding_window_mask
mask = full_mask if is_global else swa_mask
h = layer(h, local_mask, c)
h = layer(h, mask, c)
return self.norm(h)
@@ -200,10 +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
+5 -7
View File
@@ -7,6 +7,7 @@ import mlx.core as mx
import mlx.nn as nn
import numpy as np
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -107,7 +108,7 @@ class MLP(nn.Module):
self.w2 = nn.Linear(ffn_dim, d_model, bias=False)
def __call__(self, x: mx.array) -> mx.array:
current_hidden_states = nn.silu(self.w1(x)) * self.v1(x)
current_hidden_states = swiglu(self.w1(x), self.v1(x))
current_hidden_states = self.w2(current_hidden_states)
return current_hidden_states
@@ -196,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)
@@ -224,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
+5 -6
View File
@@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .switch_layers import SwitchGLU
@@ -120,7 +121,7 @@ class DeepseekMLP(nn.Module):
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class MoEGate(nn.Module):
@@ -210,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)
@@ -237,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):
+79 -36
View File
@@ -6,8 +6,11 @@ from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .pipeline import PipelineMixin
from .switch_layers import SwitchGLU
@@ -258,7 +261,7 @@ class DeepseekV2MLP(nn.Module):
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
def __call__(self, x):
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return down_proj
@@ -314,13 +317,21 @@ class DeepseekV2MoE(nn.Module):
config=config, intermediate_size=intermediate_size
)
self.sharding_group = None
def __call__(self, x):
if self.sharding_group is not None:
x = sum_gradients(self.sharding_group)(x)
inds, scores = self.gate(x)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2)
if self.config.n_shared_experts is not None:
y = y + self.shared_experts(x)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
@@ -355,7 +366,7 @@ class DeepseekV2DecoderLayer(nn.Module):
return out
class DeepseekV2Model(nn.Module):
class DeepseekV2Model(PipelineMixin, nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -364,61 +375,38 @@ class DeepseekV2Model(nn.Module):
DeepseekV2DecoderLayer(config, idx)
for idx in range(config.num_hidden_layers)
]
self.start_idx = 0
self.end_idx = len(self.layers)
self.num_layers = self.end_idx
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.pipeline_rank = 0
self.pipeline_size = 1
def pipeline(self, group):
# Split layers in reverse so rank=0 gets the last layers and
# rank=pipeline_size-1 gets the first
self.pipeline_rank = group.rank()
self.pipeline_size = group.size()
layers_per_rank = len(self.layers) // self.pipeline_size
extra = len(self.layers) - layers_per_rank * self.pipeline_size
if self.pipeline_rank < extra:
layers_per_rank += 1
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
self.end_idx = self.start_idx + layers_per_rank
self.num_layers = layers_per_rank
self.layers = self.layers[: self.end_idx]
self.layers[: self.start_idx] = [None] * self.start_idx
self.num_layers = len(self.layers) - self.start_idx
def __call__(
self,
x: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
) -> mx.array:
h = self.embed_tokens(x)
pipeline_rank = self.pipeline_rank
pipeline_size = self.pipeline_size
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * self.num_layers
cache = [None] * len(self.pipeline_layers)
mask = create_attention_mask(h, cache[0])
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
h = mx.distributed.all_gather(h)[: h.shape[0]]
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
@@ -435,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):
@@ -453,6 +440,62 @@ class Model(nn.Module):
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
return weights
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
for layer in self.model.layers:
# Shard the self attention
if layer.self_attn.q_lora_rank is None:
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
else:
layer.self_attn.q_b_proj = shard_linear(
layer.self_attn.q_b_proj, "all-to-sharded", group=group
)
layer.self_attn.kv_b_proj = shard_linear(
layer.self_attn.kv_b_proj, "all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
layer.self_attn.num_heads //= N
# Shard the MLP
if isinstance(layer.mlp, DeepseekV2MLP):
layer.mlp.gate_proj = shard_linear(
layer.mlp.gate_proj, "all-to-sharded", group=group
)
layer.mlp.down_proj = shard_linear(
layer.mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.up_proj = shard_linear(
layer.mlp.up_proj, "all-to-sharded", group=group
)
# Shard the MoE. Shard in place since the MoE should be responsible
# for aggregating the results.
else:
layer.mlp.sharding_group = group
shard_inplace(
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
)
@property
def layers(self):
return self.model.layers[self.model.start_idx : self.model.end_idx]
return self.model.pipeline_layers
+196 -175
View File
@@ -7,8 +7,13 @@ from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .mla import MultiLinear
from .pipeline import PipelineMixin
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@@ -33,9 +38,9 @@ class ModelArgs(BaseModelArgs):
topk_method: str = "noaux_tc"
scoring_func: str = "sigmoid"
norm_topk_prob: bool = True
n_group: Optional[int] = None
topk_group: Optional[int] = None
num_experts_per_tok: Optional[int] = None
n_group: int = 1
topk_group: int = 1
num_experts_per_tok: int = 1
moe_layer_freq: int = 1
first_k_dense_replace: int = 0
max_position_embeddings: int = 2048
@@ -45,85 +50,6 @@ class ModelArgs(BaseModelArgs):
attention_bias: bool = False
def yarn_find_correction_dim(
num_rotations, dim, base=10000, max_position_embeddings=2048
):
return (dim * math.log(max_position_embeddings / (num_rotations * 2 * math.pi))) / (
2 * math.log(base)
)
def yarn_find_correction_range(
low_rot, high_rot, dim, base=10000, max_position_embeddings=2048
):
low = math.floor(
yarn_find_correction_dim(low_rot, dim, base, max_position_embeddings)
)
high = math.ceil(
yarn_find_correction_dim(high_rot, dim, base, max_position_embeddings)
)
return max(low, 0), min(high, dim - 1)
def yarn_get_mscale(scale=1, mscale=1):
if scale <= 1:
return 1.0
return 0.1 * mscale * math.log(scale) + 1.0
def yarn_linear_ramp_mask(min_val, max_val, dim):
if min_val == max_val:
max_val += 0.001 # Prevent singularity
linear_func = (mx.arange(dim, dtype=mx.float32) - min_val) / (max_val - min_val)
return mx.clip(linear_func, 0, 1)
class DeepseekV3YarnRotaryEmbedding(nn.Module):
def __init__(
self,
dim,
max_position_embeddings=2048,
base=10000,
scaling_factor=1.0,
original_max_position_embeddings=4096,
beta_fast=32,
beta_slow=1,
mscale=1,
mscale_all_dim=0,
):
super().__init__()
self.mscale = yarn_get_mscale(scaling_factor, mscale) / yarn_get_mscale(
scaling_factor, mscale_all_dim
)
freq_extra = base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)
freq_inter = scaling_factor * freq_extra
low, high = yarn_find_correction_range(
beta_fast,
beta_slow,
dim,
base,
original_max_position_embeddings,
)
freq_mask = 1.0 - yarn_linear_ramp_mask(low, high, dim // 2)
self._freqs = (freq_inter * freq_extra) / (
freq_inter * freq_mask + freq_extra * (1 - freq_mask)
)
def __call__(self, x, offset=0):
if self.mscale != 1.0:
x = self.mscale * x
return mx.fast.rope(
x,
x.shape[-1],
traditional=True,
base=None,
scale=1.0,
offset=offset,
freqs=self._freqs,
)
class DeepseekV3Attention(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
@@ -160,11 +86,11 @@ class DeepseekV3Attention(nn.Module):
bias=config.attention_bias,
)
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6)
self.kv_b_proj = nn.Linear(
self.kv_lora_rank,
self.num_heads
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
bias=False,
self.embed_q = MultiLinear(
self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads
)
self.unembed_out = MultiLinear(
self.kv_lora_rank, self.v_head_dim, self.num_heads
)
self.o_proj = nn.Linear(
@@ -175,35 +101,19 @@ class DeepseekV3Attention(nn.Module):
if self.config.rope_scaling is not None:
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
scaling_factor = self.config.rope_scaling["factor"]
if mscale_all_dim:
mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
self.scale = self.scale * mscale * mscale
scaling_factor = self.config.rope_scaling["factor"]
if scaling_factor > 1:
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
self.scale = self.scale * s * s
rope_kwargs = {
key: self.config.rope_scaling[key]
for key in [
"original_max_position_embeddings",
"beta_fast",
"beta_slow",
"mscale",
"mscale_all_dim",
]
if key in self.config.rope_scaling
}
self.rope = DeepseekV3YarnRotaryEmbedding(
dim=self.qk_rope_head_dim,
max_position_embeddings=self.max_position_embeddings,
scaling_factor=scaling_factor,
base=self.rope_theta,
**rope_kwargs,
)
else:
self.rope = nn.RoPE(
dims=self.qk_rope_head_dim,
base=self.rope_theta,
traditional=True,
)
self.rope = initialize_rope(
dims=self.qk_rope_head_dim,
base=self.rope_theta,
traditional=True,
max_position_embeddings=self.max_position_embeddings,
scaling_config=self.config.rope_scaling,
)
def __call__(
self,
@@ -223,29 +133,38 @@ class DeepseekV3Attention(nn.Module):
compressed_kv = self.kv_a_proj_with_mqa(x)
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
kv = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
kv = kv.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
kv_latent = self.kv_a_layernorm(compressed_kv)
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
offset = cache.offset if cache is not None else 0
q_pe = self.rope(q_pe, offset)
k_pe = self.rope(k_pe, offset)
kv_latent = mx.expand_dims(kv_latent, axis=1)
if cache is not None:
q_pe = self.rope(q_pe, cache.offset)
k_pe = self.rope(k_pe, cache.offset)
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
keys, values = cache.update_and_fetch(
mx.concatenate([k_nope, k_pe], axis=-1), values
)
else:
q_pe = self.rope(q_pe)
k_pe = self.rope(k_pe)
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
keys = mx.concatenate([k_nope, k_pe], axis=-1)
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
queries = mx.concatenate([q_nope, q_pe], axis=-1)
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
if mask is not None:
pe_scores = mx.where(
mask,
pe_scores,
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
)
if L == 1:
q_nope = self.embed_q(q_nope)
k = v = kv_latent
else:
k = self.embed_q(kv_latent, transpose=False)
v = self.unembed_out(kv_latent)
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
)
if L == 1:
output = self.unembed_out(output)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
@@ -266,7 +185,7 @@ class DeepseekV3MLP(nn.Module):
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
def __call__(self, x):
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return down_proj
@@ -281,18 +200,18 @@ def group_expert_select(
norm_topk_prob,
):
k = top_k
scores = mx.sigmoid(gates.astype(mx.float32))
orig_scores = scores
scores = scores + e_score_correction_bias
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
k = n_group - topk_group
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
scores = mx.put_along_axis(
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
)
scores = mx.flatten(scores, -2, -1)
if n_group > 1:
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
k = n_group - topk_group
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
scores = mx.put_along_axis(
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
)
scores = mx.flatten(scores, -2, -1)
k = top_k
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
@@ -349,13 +268,21 @@ class DeepseekV3MoE(nn.Module):
config=config, intermediate_size=intermediate_size
)
self.sharding_group = None
def __call__(self, x):
if self.sharding_group is not None:
x = sum_gradients(self.sharding_group)(x)
inds, scores = self.gate(x)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
if self.config.n_shared_experts is not None:
y = y + self.shared_experts(x)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
@@ -389,7 +316,7 @@ class DeepseekV3DecoderLayer(nn.Module):
return h + r
class DeepseekV3Model(nn.Module):
class DeepseekV3Model(PipelineMixin, nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -398,59 +325,38 @@ class DeepseekV3Model(nn.Module):
DeepseekV3DecoderLayer(config, idx)
for idx in range(config.num_hidden_layers)
]
self.start_idx = 0
self.end_idx = len(self.layers)
self.num_layers = self.end_idx
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.pipeline_rank = 0
self.pipeline_size = 1
def pipeline(self, group):
# Split layers in reverse so rank=0 gets the last layers and
# rank=pipeline_size-1 gets the first
self.pipeline_rank = group.rank()
self.pipeline_size = group.size()
layers_per_rank = len(self.layers) // self.pipeline_size
extra = len(self.layers) - layers_per_rank * self.pipeline_size
if self.pipeline_rank < extra:
layers_per_rank += 1
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
self.end_idx = self.start_idx + layers_per_rank
self.layers = self.layers[: self.end_idx]
self.layers[: self.start_idx] = [None] * self.start_idx
self.num_layers = len(self.layers) - self.start_idx
def __call__(
self,
x: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
) -> mx.array:
h = self.embed_tokens(x)
pipeline_rank = self.pipeline_rank
pipeline_size = self.pipeline_size
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * self.num_layers
cache = [None] * len(self.pipeline_layers)
mask = create_attention_mask(h, cache[0], return_array=True)
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
h = mx.distributed.all_gather(h)[: h.shape[0]]
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
@@ -467,14 +373,14 @@ 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):
def dequant(weight, scale_inv):
dtype = weight.dtype
dtype = mx.bfloat16
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
bs = 128 # block size
m, n = weight.shape
pad_bottom = (-m) % bs
@@ -488,7 +394,22 @@ class Model(nn.Module):
)
return weight[:m, :n].astype(dtype)
# Dequantize
# Remap for int4
new_weights = {}
for k, v in weights.items():
if k.endswith("weight_shape"):
base = k.replace("weight_shape", "")
new_weights[base + "weight"] = weights[base + "weight_packed"].view(
mx.uint32
)
s = weights[base + "weight_scale"]
new_weights[base + "scales"] = s
new_weights[base + "biases"] = -8 * s
elif not (k.endswith("weight_scale") or k.endswith("weight_packed")):
new_weights[k] = v
weights = new_weights
# Dequantize fp8
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
@@ -512,6 +433,42 @@ class Model(nn.Module):
for e in range(self.args.n_routed_experts)
]
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
prefix = f"model.layers.{l}.self_attn"
if f"{prefix}.kv_b_proj.weight" in weights:
layer = self.model.layers[l].self_attn.embed_q
quantized = f"{prefix}.kv_b_proj.scales" in weights
v = weights.pop(f"{prefix}.kv_b_proj.weight")
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
if quantized:
dims = self.args.kv_lora_rank
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
# Try to infer bits and group size
bits = (v.shape[-1] * 32) // dims
group_size = dims // scales.shape[-1]
v = mx.dequantize(
v, scales, biases, bits=bits, group_size=group_size
)
num_heads = self.args.num_attention_heads
v = v.reshape(num_heads, head_dim, -1)
wk = mx.contiguous(
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
)
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
if quantized:
wk, wk_scales, wk_biases = mx.quantize(
wk, bits=bits, group_size=group_size
)
wv, wv_scales, wv_biases = mx.quantize(
wv, bits=bits, group_size=group_size
)
weights[f"{prefix}.embed_q.scales"] = wk_scales
weights[f"{prefix}.unembed_out.scales"] = wv_scales
weights[f"{prefix}.embed_q.biases"] = wk_biases
weights[f"{prefix}.unembed_out.biases"] = wv_biases
weights[f"{prefix}.embed_q.weight"] = wk
weights[f"{prefix}.unembed_out.weight"] = wv
# Remove multi-token prediction layer and any unused precomputed rotary freqs
return {
@@ -520,9 +477,73 @@ class Model(nn.Module):
if not k.startswith("model.layers.61") and "rotary_emb.inv_freq" not in k
}
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
rank = group.rank()
for layer in self.model.layers:
# Shard the self attention
if layer.self_attn.q_lora_rank is None:
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
else:
layer.self_attn.q_b_proj = shard_linear(
layer.self_attn.q_b_proj, "all-to-sharded", group=group
)
layer.self_attn.num_heads //= N
num_heads = layer.self_attn.num_heads
sh = rank * num_heads
eh = sh + num_heads
def shard_heads(w):
return w[sh:eh]
layer.self_attn.embed_q.apply(shard_heads)
layer.self_attn.unembed_out.apply(shard_heads)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
# Shard the MLP
if isinstance(layer.mlp, DeepseekV3MLP):
layer.mlp.gate_proj = shard_linear(
layer.mlp.gate_proj, "all-to-sharded", group=group
)
layer.mlp.down_proj = shard_linear(
layer.mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.up_proj = shard_linear(
layer.mlp.up_proj, "all-to-sharded", group=group
)
# Shard the MoE. Shard in place since the MoE should be responsible
# for aggregating the results.
else:
layer.mlp.sharding_group = group
shard_inplace(
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
)
@property
def layers(self):
return self.model.layers[self.model.start_idx : self.model.end_idx]
return self.model.pipeline_layers
@property
def cast_predicate(self):
+654
View File
@@ -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]
+6 -10
View File
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@@ -22,10 +23,9 @@ class ModelArgs(BaseModelArgs):
rms_norm_eps: float
vocab_size: int
max_position_embeddings: Optional[int]
num_key_value_heads: Optional[int]
num_key_value_heads: int
first_k_dense_replace: int
moe_intermediate_size: int
moe_layer_freq: int
n_routed_experts: int
n_shared_experts: int
norm_topk_prob: bool
@@ -48,7 +48,6 @@ class Dots1Attention(nn.Module):
dim = args.hidden_size
self.n_heads = n_heads = args.num_attention_heads
assert args.num_key_value_heads is not None
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
head_dim = args.head_dim or args.hidden_size // n_heads
@@ -182,7 +181,7 @@ class Dots1MLP(nn.Module):
)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class Dots1MoE(nn.Module):
@@ -254,17 +253,15 @@ class Dots1Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
) -> mx.array:
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
@@ -283,10 +280,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import initialize_rope
@@ -87,7 +88,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class DecoderLayer(nn.Module):
@@ -123,17 +124,15 @@ class Ernie45Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
@@ -152,10 +151,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@@ -98,7 +99,7 @@ class Ernie4_5_MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class Ernie4_5_MoeMLP(nn.Module):
@@ -219,17 +220,15 @@ class Ernie45Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
@@ -248,10 +247,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+5 -6
View File
@@ -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:
+19 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@@ -102,7 +103,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
@@ -149,23 +150,35 @@ class ExaoneModel(nn.Module):
)
for i in range(args.num_hidden_layers)
]
if pattern:
self.swa_idx = pattern.index("L")
self.full_idx = pattern.index("G")
else:
self.swa_idx = None
self.full_idx = 0
self.window_size = args.sliding_window
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
global_mask = create_attention_mask(h, cache[self.full_idx])
if self.swa_idx is not None:
swa_mask = create_attention_mask(
h, cache[self.swa_idx], window_size=self.window_size
)
else:
swa_mask = None
for layer, c in zip(self.layers, cache):
mask = swa_mask if layer.self_attn.is_local else global_mask
h = layer(h, mask, c)
return self.norm(h)
@@ -183,10 +196,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+439
View File
@@ -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
)
+504
View File
@@ -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
+283
View File
@@ -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)
+3 -6
View File
@@ -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
+3 -6
View File
@@ -165,18 +165,16 @@ class GemmaModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
h = h * (self.args.hidden_size**0.5)
if mask is None:
mask = create_attention_mask(h, cache, return_array=True)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0], return_array=True)
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
@@ -194,10 +192,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
out = self.model.embed_tokens.as_linear(out)
out = mx.tanh(out / self.final_logit_softcapping)
out = out * self.final_logit_softcapping
+1 -2
View File
@@ -40,11 +40,10 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache=None,
mask: Optional[mx.array] = None,
input_embeddings: Optional[mx.array] = None,
):
return self.language_model(
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
inputs, cache=cache, input_embeddings=input_embeddings
)
def sanitize(self, weights):
+35 -34
View File
@@ -2,13 +2,14 @@
from dataclasses import dataclass
from functools import partial
from typing import Any, Optional
from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@dataclass
@@ -22,12 +23,13 @@ class ModelArgs(BaseModelArgs):
rms_norm_eps: float = 1.0e-6
vocab_size: int = 262144
num_key_value_heads: int = 1
rope_global_base_freq: float = 1_000_000.0
rope_theta: float = 1_000_000.0
rope_local_base_freq: float = 10_000.0
rope_traditional: bool = False
query_pre_attn_scalar: float = 256
sliding_window: int = 512
sliding_window_pattern: int = 6
max_position_embeddings: int = 32768
rope_scaling: Dict = None
class Attention(nn.Module):
@@ -52,15 +54,20 @@ class Attention(nn.Module):
self.k_norm = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
self.is_sliding = (layer_idx + 1) % args.sliding_window_pattern != 0
self.rope = nn.RoPE(
head_dim,
traditional=args.rope_traditional,
base=(
args.rope_local_base_freq
if self.is_sliding
else args.rope_global_base_freq
),
)
if self.is_sliding:
self.rope = initialize_rope(
dims=head_dim,
base=args.rope_local_base_freq,
traditional=False,
)
else:
self.rope = initialize_rope(
dims=head_dim,
base=args.rope_theta,
traditional=False,
max_position_embeddings=args.max_position_embeddings,
scaling_config=args.rope_scaling,
)
def __call__(
self,
@@ -87,8 +94,6 @@ class Attention(nn.Module):
keys = self.rope(keys)
# Sliding window
if isinstance(mask, mx.array) and mask.shape[-1] != keys.shape[-2]:
mask = mask[..., -keys.shape[-2] :]
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
@@ -160,6 +165,8 @@ class Gemma3Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.window_size = args.sliding_window
self.sliding_window_pattern = args.sliding_window_pattern
self.vocab_size = args.vocab_size
self.num_hidden_layers = args.num_hidden_layers
assert self.vocab_size > 0
@@ -173,7 +180,6 @@ class Gemma3Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
@@ -186,24 +192,22 @@ class Gemma3Model(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
if mask is None:
j = self.args.sliding_window_pattern
full_mask = create_attention_mask(h, cache[j - 1 : j])
sliding_window_mask = create_attention_mask(h, cache)
global_mask = create_attention_mask(h, cache[self.sliding_window_pattern - 1])
if self.sliding_window_pattern > 1:
sliding_window_mask = create_attention_mask(
h,
cache[0],
window_size=self.window_size,
)
else:
sliding_window_mask = None
for i, (layer, c) in enumerate(zip(self.layers, cache)):
is_global = (
i % self.args.sliding_window_pattern
== self.args.sliding_window_pattern - 1
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
)
local_mask = mask
if mask is None and is_global:
local_mask = full_mask
elif mask is None:
local_mask = sliding_window_mask
h = layer(h, local_mask, c)
mask = global_mask if is_global else sliding_window_mask
h = layer(h, mask, c)
return self.norm(h)
@@ -221,10 +225,9 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache=None,
mask: Optional[mx.array] = None,
input_embeddings: Optional[mx.array] = None,
):
out = self.model(inputs, mask, cache, input_embeddings)
out = self.model(inputs, cache, input_embeddings)
if self.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
@@ -250,7 +253,5 @@ class Model(nn.Module):
):
caches.append(KVCache())
else:
caches.append(
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
)
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
return caches
+24 -35
View File
@@ -151,9 +151,6 @@ class Gemma3nAttention(nn.Module):
queries = queries.transpose(0, 2, 1, 3)
queries = self.rope(queries, offset=offset)
if isinstance(mask, mx.array) and mask.shape[-1] != keys.shape[-2]:
mask = mask[:, : keys.shape[-2]]
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
@@ -273,12 +270,11 @@ class Gemma3nAltUp(nn.Module):
)
all_coefs = self.correction_coefs(modalities) + 1.0
active_x = predictions[self.config.altup_active_idx]
innovation = activated - active_x
all_coefs = all_coefs.transpose(2, 1, 0)
corrected = innovation[None] * all_coefs[:, None]
all_coefs = all_coefs.moveaxis(2, 0)
corrected = innovation[None] * all_coefs[..., None]
corrected += predictions
return corrected.astype(activated.dtype)
@@ -310,7 +306,6 @@ class Gemma3nDecoderLayer(nn.Module):
eps=config.rms_norm_eps,
)
self.is_sliding = self.self_attn.is_sliding
self.sliding_window = config.sliding_window
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
@@ -355,7 +350,6 @@ class Gemma3nDecoderLayer(nn.Module):
attn_ffw = self.mlp(attn_norm)
attn_ffw_norm = self.post_feedforward_layernorm(attn_ffw)
attn_ffw_laurel_gated = attn_laurel + attn_ffw_norm
corrected_predictions = self.altup.correct(predictions, attn_ffw_laurel_gated)
first_prediction = corrected_predictions[self.config.altup_active_idx]
@@ -437,10 +431,11 @@ class LanguageModel(nn.Module):
eps=config.rms_norm_eps,
)
self.first_sliding_idx = self.config.layer_types.index("sliding_attention")
self.first_full_idx = self.config.layer_types.index("full_attention")
self.first_sliding_idx = config.layer_types.index("sliding_attention")
self.first_full_idx = config.layer_types.index("full_attention")
self.sliding_window = config.sliding_window
concrete_layers = self.config.layer_types[: self.first_kv_shared_layer_idx]
concrete_layers = config.layer_types[: self.first_kv_shared_layer_idx]
shared_full_idx = (
len(concrete_layers) - 1 - concrete_layers[::-1].index("full_attention")
)
@@ -463,7 +458,6 @@ class LanguageModel(nn.Module):
def __call__(
self,
inputs: mx.array = None,
mask: mx.array = None,
cache=None,
input_embeddings: mx.array = None,
):
@@ -476,17 +470,17 @@ class LanguageModel(nn.Module):
per_layer_inputs = self.project_per_layer_inputs(h, per_layer_inputs)
if cache is None:
cache = self.make_cache()
cache = [None] * len(self.layers)
if mask is None:
full_mask = create_attention_mask(
h,
cache[self.first_full_idx :],
)
sliding_window_mask = create_attention_mask(
h,
cache[self.first_sliding_idx :],
)
global_mask = create_attention_mask(
h,
cache[self.first_full_idx],
)
sliding_window_mask = create_attention_mask(
h,
cache[self.first_sliding_idx],
window_size=self.sliding_window,
)
h0 = h
# Expand hidden_states to support per-layer inputs
@@ -497,21 +491,19 @@ class LanguageModel(nn.Module):
h = mx.stack(h_list, axis=0)
mags = mx.mean(h[1:] ** 2, axis=-1, keepdims=True) ** 0.5
h[1:] = h[1:] * (target_magnitude / mx.maximum(mags, mx.finfo(h0.dtype).min))
for i, layer in enumerate(self.layers):
per_layer_input = per_layer_inputs[:, :, i, :]
is_global = self.config.layer_types[i] == "full_attention"
local_mask = mask
if mask is None and is_global:
local_mask = full_mask
elif mask is None:
local_mask = sliding_window_mask
if is_global:
mask = global_mask
else:
mask = sliding_window_mask
h = layer(
h,
local_mask,
mask,
cache[self.layer_idx_to_cache_idx[i]],
per_layer_input,
)
@@ -582,11 +574,10 @@ class Gemma3n(nn.Module):
self,
inputs: mx.array,
cache=None,
mask: Optional[mx.array] = None,
input_embeddings: Optional[mx.array] = None,
):
return self.language_model(
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
inputs, cache=cache, input_embeddings=input_embeddings
)
def make_cache(self):
@@ -598,17 +589,15 @@ class Model(nn.Module):
super().__init__()
self.args = args
self.model = Gemma3n(args)
self.model_type = args.model_type
def __call__(
self,
inputs: mx.array,
cache=None,
mask: Optional[mx.array] = None,
input_embeddings: Optional[mx.array] = None,
):
return self.model(
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
)
return self.model(inputs, cache=cache, input_embeddings=input_embeddings)
def sanitize(self, weights):
weights = tree_unflatten(list(weights.items()))
+92
View File
@@ -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()
+688
View File
@@ -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
+188
View File
@@ -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
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -38,7 +39,7 @@ class Glm4MLP(nn.Module):
def __call__(self, x) -> mx.array:
x = self.gate_up_proj(x)
gate, up_states = mx.split(x, 2, axis=-1)
return self.down_proj(nn.silu(gate) * up_states)
return self.down_proj(swiglu(gate, up_states))
class Glm4Attention(nn.Module):
@@ -144,17 +145,15 @@ class Glm4Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -172,10 +171,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
return self.lm_head(out)
@property
+90 -16
View File
@@ -7,8 +7,11 @@ from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .pipeline import PipelineMixin
from .switch_layers import SwitchGLU
@@ -121,7 +124,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
def __call__(self, x):
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return down_proj
@@ -140,7 +143,6 @@ def group_expert_select(
orig_scores = scores
scores = scores + e_score_correction_bias
if n_group > 1:
k = top_k
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
@@ -205,13 +207,21 @@ class MoE(nn.Module):
config=config, intermediate_size=intermediate_size
)
self.sharding_group = None
def __call__(self, x):
if self.sharding_group is not None:
x = sum_gradients(self.sharding_group)(x)
inds, scores = self.gate(x)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
if self.config.n_shared_experts is not None:
y = y + self.shared_experts(x)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
@@ -244,7 +254,7 @@ class DecoderLayer(nn.Module):
return h + r
class LanguageModel(nn.Module):
class LanguageModel(PipelineMixin, nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -252,28 +262,38 @@ class LanguageModel(nn.Module):
self.layers = [
DecoderLayer(config, idx) for idx in range(config.num_hidden_layers)
]
self.start_idx = 0
self.end_idx = len(self.layers)
self.num_layers = self.end_idx
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def __call__(
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] * self.num_layers
cache = [None] * len(self.pipeline_layers)
mask = create_attention_mask(h, cache[0])
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
@@ -290,9 +310,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):
@@ -317,9 +336,64 @@ class Model(nn.Module):
if not k.startswith(f"model.layers.{mpt_layer}")
}
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
for layer in self.model.layers:
# Shard the self attention
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
layer.self_attn.k_proj = shard_linear(
layer.self_attn.k_proj, "all-to-sharded", group=group
)
layer.self_attn.v_proj = shard_linear(
layer.self_attn.v_proj, "all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
layer.self_attn.n_heads //= N
layer.self_attn.n_kv_heads //= N
# Shard the MLP
if isinstance(layer.mlp, MLP):
layer.mlp.gate_proj = shard_linear(
layer.mlp.gate_proj, "all-to-sharded", group=group
)
layer.mlp.down_proj = shard_linear(
layer.mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.up_proj = shard_linear(
layer.mlp.up_proj, "all-to-sharded", group=group
)
# Shard the MoE. Shard in place since the MoE should be responsible
# for aggregating the results.
else:
layer.mlp.sharding_group = group
shard_inplace(
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
)
shard_inplace(
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
)
shard_inplace(
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
)
@property
def layers(self):
return self.model.layers
return self.model.pipeline_layers
@property
def cast_predicate(self):
+531
View File
@@ -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
+53
View File
@@ -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)
+13 -14
View File
@@ -1,4 +1,4 @@
# Copyright © 2023-2024 Apple Inc.
# Copyright © 2023 - 2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Optional
@@ -125,26 +125,26 @@ class GPT2Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
_, L = inputs.shape
hidden_states = self.wte(inputs)
offset = 0
if cache is not None and len(cache) > 0 and cache[0] is not None:
offset = cache[0].offset
position_ids = mx.arange(offset, offset + L)
hidden_states += self.wpe(position_ids)
if mask is None:
mask = create_attention_mask(hidden_states, cache)
if cache is None:
cache = [None] * len(self.h)
offset = 0
if cache[0] is not None:
offset = cache[0].offset
offset = mx.array(offset)
position_ids = mx.arange(L) + offset[..., None]
hidden_states += self.wpe(position_ids)
mask = create_attention_mask(hidden_states, cache[0])
for layer, c in zip(self.h, cache):
hidden_states = layer(hidden_states, mask, cache=c)
@@ -161,10 +161,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
out = self.model.wte.as_linear(out)
return out
+3 -7
View File
@@ -137,23 +137,20 @@ class GPTBigCodeModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
B, L = inputs.shape
hidden_states = self.wte(inputs)
mask = None
if mask is not None and hidden_states.shape[1] > 1:
mask = create_attention_mask(hidden_states, cache)
if cache is None:
cache = [None] * len(self.h)
position_ids = mx.array(np.arange(L))
else:
position_ids = mx.array(np.arange(cache[0].offset, cache[0].offset + L))
mask = create_attention_mask(hidden_states, cache[0])
hidden_states += self.wpe(position_ids)
for layer, c in zip(self.h, cache):
@@ -174,10 +171,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.transformer(inputs, mask, cache)
out = self.transformer(inputs, cache)
if self.args.tie_word_embeddings:
out = self.transformer.wte.as_linear(out)
else:
+19 -12
View File
@@ -23,6 +23,7 @@ class ModelArgs(BaseModelArgs):
vocab_size: int
rotary_emb_base: int
rotary_pct: float
use_parallel_residual: bool = True
num_key_value_heads: int = None
def __post_init__(self):
@@ -107,6 +108,7 @@ class TransformerBlock(nn.Module):
self.layer_norm_eps = args.layer_norm_eps
self.attention = Attention(args)
self.mlp = MLP(args)
self.use_parallel_residual = args.use_parallel_residual
self.input_layernorm = nn.LayerNorm(
self.hidden_size,
eps=self.layer_norm_eps,
@@ -121,12 +123,20 @@ class TransformerBlock(nn.Module):
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
residual = x
# NeoX runs attention and feedforward network in parallel.
attn = self.attention(self.input_layernorm(x), mask, cache)
ffn = self.mlp(self.post_attention_layernorm(x))
out = attn + ffn + residual
return out
if self.use_parallel_residual:
residual = x
# Run attention and feedforward network in parallel.
attn = self.attention(self.input_layernorm(x), mask, cache)
ffn = self.mlp(self.post_attention_layernorm(x))
out = attn + ffn + residual
return out
else:
# Run attention and feedforward network sequentially.
attn_output = self.attention(self.input_layernorm(x), mask, cache)
x = x + attn_output
ffn_output = self.mlp(self.post_attention_layernorm(x))
x = x + ffn_output
return x
class GPTNeoXModel(nn.Module):
@@ -145,19 +155,17 @@ class GPTNeoXModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
_, L = inputs.shape
hidden_states = self.embed_in(inputs)
if mask is None:
mask = create_attention_mask(hidden_states, cache)
if cache is None:
cache = [None] * len(self.h)
mask = create_attention_mask(hidden_states, cache[0])
for layer, c in zip(self.h, cache):
hidden_states = layer(hidden_states, mask, cache=c)
@@ -177,10 +185,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
return out
def sanitize(self, weights):
+74 -110
View File
@@ -7,8 +7,9 @@ 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_causal_mask, scaled_dot_product_attention
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
@@ -102,80 +103,6 @@ class AttentionBlock(nn.Module):
scaling_config=config.rope_scaling,
)
# Cache the mask so we don't have to create it every time
self._previous_mask = None
def get_causal_mask(self, x, cache):
_, L, _ = x.shape
offset = cache.offset if cache is not None else 0
offset = max(1, offset)
def _make_mask(L, offset):
zero = mx.array(0, dtype=x.dtype)
neginf = mx.array(-mx.inf, dtype=x.dtype)
mask = mx.where(create_causal_mask(L, offset - 1), zero, neginf)
mask = mask.reshape(1, 1, L, -1)
mask = mx.tile(mask, (1, self.num_attention_heads, 1, 1))
sinks = mx.tile(self.sinks.reshape(1, -1, 1, 1), (1, 1, L, 1))
mask = mx.concatenate([sinks, mask], axis=-1)
return mask
# When training re-create the mask so that gradients flow to the sinks.
# When L is large then recreate the mask because otherwise it will take
# a pretty significant chunk of memory.
if self.training or L > 8:
self._previous_mask = None
return _make_mask(L, offset)
# Create the mask once and try to reuse it. For this reason we round up
# to the closest multiple of 512 so we can reuse the mask several times.
length = ((L + offset + 511) // 512) * 512
if (
self._previous_mask is None
or self._previous_mask.shape[-1] < length
or self._previous_mask.shape[-2] != L
):
self._previous_mask = _make_mask(L, length - L)
return self._previous_mask[..., : L + offset]
def get_sliding_window_mask(self, x, cache, window_size):
_, L, _ = x.shape
offset = cache.offset if cache is not None else 0
offset = max(1, offset)
def _make_mask(L, offset):
zero = mx.array(0, dtype=x.dtype)
neginf = mx.array(-mx.inf, dtype=x.dtype)
mask = create_causal_mask(L, offset - 1, window_size)
mask = mx.where(mask, zero, neginf)
mask = mask.reshape(1, 1, L, -1)
mask = mx.tile(mask, (1, self.num_attention_heads, 1, 1))
sinks = mx.tile(self.sinks.reshape(1, -1, 1, 1), (1, 1, L, 1))
mask = mx.concatenate([sinks, mask], axis=-1)
return mask
# If we are training then simply re-create the mask every time to make
# sure gradients flow to the sinks.
#
# For simplicity also re-create the mask if we have more than 1 query
# for now.
if self.training or L > 1:
self._previous_mask = None
return _make_mask(L, min(window_size + 1, offset))
# We are in inference so cache the mask and try to reuse it
if self._previous_mask is None:
self._previous_mask = _make_mask(L, window_size)
return self._previous_mask[..., : min(L + offset, window_size + 1)]
def get_mask(self, x, cache, window_size):
if window_size is not None:
return self.get_sliding_window_mask(x, cache, window_size)
else:
return self.get_causal_mask(x, cache)
def __call__(self, x: mx.array, mask: mx.array, cache=None) -> mx.array:
B, L, _ = x.shape
D = self.head_dim
@@ -185,26 +112,17 @@ class AttentionBlock(nn.Module):
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 None or the cache offset is 0 then we need to add a 0 key
# and value to make some space for the sink
if cache is None or cache.offset == 0:
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)
zeros = mx.zeros((B, Hk, 1, D), dtype=k.dtype)
k = mx.concatenate([zeros, k], axis=2)
v = mx.concatenate([zeros, v], axis=2)
if cache is not None:
k, v = cache.update_and_fetch(k, v)
# We have already put the 0 in the cache no need to do anything special
else:
q = self.rope(q, offset=cache.offset - 1)
k = self.rope(k, offset=cache.offset - 1)
k, v = cache.update_and_fetch(k, v)
# NOTE: mask should contain the sink weights already
v_hat = scaled_dot_product_attention(q, k, v, cache, self.sm_scale, mask=mask)
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))
@@ -225,8 +143,12 @@ class MLPBlock(nn.Module):
bias=True,
)
self.router = nn.Linear(config.hidden_size, config.num_local_experts, bias=True)
self.sharding_group = None
def __call__(self, x: mx.array) -> mx.array:
if self.sharding_group is not None:
x = sum_gradients(self.sharding_group)(x)
g = self.router(x)
experts, indices = mlx_topk(g, k=self.num_experts_per_tok, axis=-1)
expert_weights = mx.softmax(experts, axis=-1, precise=True)
@@ -235,7 +157,13 @@ class MLPBlock(nn.Module):
x = self.experts(x, indices)
x = x * mx.expand_dims(expert_weights, axis=-1)
return x.sum(axis=-2)
y = x.sum(axis=-2)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
class TransformerBlock(nn.Module):
@@ -272,11 +200,12 @@ class GptOssMoeModel(nn.Module):
] * (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,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
@@ -288,18 +217,14 @@ class GptOssMoeModel(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
if mask is None:
masks = [
l.self_attn.get_mask(
x, c, self.window_size if lt == "sliding_attention" else None
)
for (l, c, lt) in zip(self.layers, cache, self.layer_types)
]
else:
masks = [mask] * 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 i, (layer, c, m) in enumerate(zip(self.layers, cache, masks)):
x = layer(x, m, c)
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
@@ -312,8 +237,8 @@ class Model(nn.Module):
self.model = GptOssMoeModel(args)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(self, inputs: mx.array, mask: mx.array = None, cache=None):
return self.lm_head(self.model(inputs, mask, cache))
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()):
@@ -354,6 +279,47 @@ class Model(nn.Module):
return new_weights
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
R = group.rank()
for layer in self.model.layers:
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, sharding="all-to-sharded", group=group
)
layer.self_attn.k_proj = shard_linear(
layer.self_attn.k_proj, sharding="all-to-sharded", group=group
)
layer.self_attn.v_proj = shard_linear(
layer.self_attn.v_proj, sharding="all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, sharding="sharded-to-all", group=group
)
layer.self_attn.num_attention_heads //= N
layer.self_attn.num_key_value_heads //= N
layer.self_attn.num_key_value_groups = (
layer.self_attn.num_attention_heads
// layer.self_attn.num_key_value_heads
)
layer.self_attn.sinks = layer.self_attn.sinks[
layer.self_attn.num_attention_heads
* R : layer.self_attn.num_attention_heads
* (R + 1)
]
shard_inplace(layer.mlp.experts.gate_proj, "all-to-sharded", group=group)
shard_inplace(layer.mlp.experts.down_proj, "sharded-to-all", group=group)
layer.mlp.experts.down_proj.bias /= N
shard_inplace(
layer.mlp.experts.up_proj, sharding="all-to-sharded", group=group
)
layer.mlp.sharding_group = group
@property
def layers(self):
return self.model.layers
@@ -373,7 +339,5 @@ class Model(nn.Module):
if lt == "full_attention":
caches.append(KVCache())
else:
caches.append(
RotatingKVCache(max_size=self.args.sliding_window + 1, keep=1)
)
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
return caches
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import initialize_rope
@@ -104,7 +105,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
@@ -150,17 +151,15 @@ class GraniteModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs) * self.embedding_multiplier
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -180,10 +179,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+235
View File
@@ -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
+559
View File
@@ -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
+5 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -92,7 +93,7 @@ class HeliumMLP(nn.Module):
)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class HeliumDecoderLayer(nn.Module):
@@ -136,17 +137,15 @@ class HeliumModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
) -> mx.array:
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
@@ -170,10 +169,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
) -> mx.array:
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
+4 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .switch_layers import SwitchGLU
@@ -148,7 +149,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class Gate(nn.Module):
@@ -259,17 +260,14 @@ class HunYuanModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for i, (layer, c) in enumerate(zip(self.layers, cache)):
if (not self.args.use_cla) or i % self.args.cla_share_factor == 0:
shared_kv_states = None
@@ -288,10 +286,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
return self.model.embed_tokens.as_linear(out)
def sanitize(self, weights):
+15 -8
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -144,7 +145,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
@@ -186,17 +187,14 @@ class HunyuanV1DenseModel(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)
@@ -209,16 +207,25 @@ class Model(nn.Module):
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,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
return self.model.embed_tokens.as_linear(out)
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
+4 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -156,7 +157,7 @@ class MLP(nn.Module):
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
return self.w2(swiglu(self.w1(x), self.w3(x)))
class TransformerBlock(nn.Module):
@@ -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:
+4 -7
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
@@ -154,7 +155,7 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
@@ -193,17 +194,14 @@ class InternLM2Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -222,10 +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.embed_tokens.as_linear(out)
else:
+286
View File
@@ -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
]
+385
View File
@@ -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
+83
View File
@@ -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
+611
View File
@@ -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
+5 -7
View File
@@ -30,9 +30,9 @@ class TextArgs(BaseModelArgs):
topk_method: str = "noaux_tc"
scoring_func: str = "sigmoid"
norm_topk_prob: bool = True
n_group: Optional[int] = None
topk_group: Optional[int] = None
num_experts_per_tok: Optional[int] = None
n_group: int = 1
topk_group: int = 1
num_experts_per_tok: int = 1
moe_layer_freq: int = 1
first_k_dense_replace: int = 0
max_position_embeddings: int = 2048
@@ -62,9 +62,8 @@ class LanguageModel(nn.Module):
self,
inputs: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
):
out = self.model(inputs, cache, mask)
out = self.model(inputs, cache)
return self.lm_head(out)
@@ -79,9 +78,8 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache: Optional[Any] = None,
mask: Optional[mx.array] = None,
):
return self.language_model(inputs, cache, mask)
return self.language_model(inputs, cache)
def sanitize(self, weights):
def keep(key):
+1 -7
View File
@@ -18,11 +18,6 @@ class ModelArgs(BaseModelArgs):
def __post_init__(self):
self.text_config["tie_word_embeddings"] = False
self.text_config["full_attn_idxs"] = [
i
for i, layer_type in enumerate(self.text_config["layer_types"])
if layer_type == "full_attention"
]
class Model(nn.Module):
@@ -36,11 +31,10 @@ class Model(nn.Module):
self,
inputs: mx.array,
cache=None,
mask: Optional[mx.array] = None,
input_embeddings: Optional[mx.array] = None,
):
return self.language_model(
inputs, cache=cache, mask=mask, input_embeddings=input_embeddings
inputs, cache=cache, input_embeddings=input_embeddings
)
def sanitize(self, weights):
+60 -22
View File
@@ -5,7 +5,13 @@ from typing import Any, List, Optional
import mlx.core as mx
import mlx.nn as nn
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .activations import swiglu
from .base import (
BaseModelArgs,
create_attention_mask,
create_ssm_mask,
scaled_dot_product_attention,
)
from .cache import ArraysCache, KVCache
@@ -26,8 +32,22 @@ class ModelArgs(BaseModelArgs):
block_multiple_of: int
block_ffn_dim_multiplier: float
block_auto_adjust_ff_dim: bool
full_attn_idxs: List[int]
rope_theta: float
rope_theta: float = 1000000.0
rope_parameters: Optional[dict] = None
full_attn_idxs: Optional[List[int]] = None
layer_types: Optional[List[str]] = None
def __post_init__(self):
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
self.rope_theta = self.rope_parameters["rope_theta"]
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.full_attn_idxs is None:
self.full_attn_idxs = [
i
for i, layer_type in enumerate(self.layer_types)
if layer_type == "full_attention"
]
class Attention(nn.Module):
@@ -114,24 +134,36 @@ class ShortConv(nn.Module):
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
seqlen = x.shape[1]
BCx = self.in_proj(x)
B, C, x = mx.split(BCx, 3, axis=-1)
Bx = B * x
if mask is not None:
Bx = mx.where(mask[..., None], Bx, 0)
state = None
if cache is not None:
state = cache[0]
if state is None:
state = mx.zeros(
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
)
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)])
Bx = mx.concatenate([state, Bx], axis=-2)
if cache is not None:
cache[0] = Bx[:, -(self.L_cache - 1) :]
conv_out = self.conv(Bx)
y = C * conv_out
@@ -159,7 +191,7 @@ class MLP(nn.Module):
self.w2 = nn.Linear(ff_dim, dim, bias=False)
def __call__(self, x) -> mx.array:
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
return self.w2(swiglu(self.w1(x), self.w3(x)))
class Lfm2DecoderLayer(nn.Module):
@@ -194,6 +226,7 @@ class Lfm2DecoderLayer(nn.Module):
else:
r = self.conv(
self.operator_norm(x),
mask=mask,
cache=cache,
)
h = x + r
@@ -214,10 +247,17 @@ class Lfm2Model(nn.Module):
self.embedding_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
self.fa_idx = args.full_attn_idxs[0]
self.conv_idx = 0
for i in range(args.num_hidden_layers):
if i in args.full_attn_idxs:
self.conv_idx += 1
else:
break
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
@@ -226,15 +266,14 @@ class Lfm2Model(nn.Module):
else:
h = self.embed_tokens(inputs)
if mask is None:
first_attn_idx = self.args.full_attn_idxs[0]
c = [cache[first_attn_idx]] if cache is not None else None
mask = create_attention_mask(h, c)
if cache is None:
cache = [None] * len(self.layers)
attn_mask = create_attention_mask(h, cache[self.fa_idx])
conv_mask = create_ssm_mask(h, cache[self.conv_idx])
for layer, c in zip(self.layers, cache):
mask = attn_mask if layer.is_attention_layer else conv_mask
h = layer(h, mask, cache=c)
return self.embedding_norm(h)
@@ -250,11 +289,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
out = self.model(inputs, mask, cache, input_embeddings)
out = self.model(inputs, cache, input_embeddings)
return self.model.embed_tokens.as_linear(out)
def sanitize(self, weights):
+387
View File
@@ -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
+155
View File
@@ -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}
+73 -12
View File
@@ -1,12 +1,15 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, List, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_linear
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@@ -28,11 +31,16 @@ class ModelArgs(BaseModelArgs):
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = True
layer_types: Optional[List[str]] = None
sliding_window: Optional[int] = None
def __post_init__(self):
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.layer_types is None:
self.layer_types = ["full_attention"] * self.num_hidden_layers
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
@@ -110,14 +118,15 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs):
def __init__(self, args: ModelArgs, use_sliding: bool = False):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.hidden_size = args.hidden_size
self.use_sliding = use_sliding
self.self_attn = Attention(args)
self.mlp = MLP(args)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
@@ -145,17 +154,25 @@ class LlamaModel(nn.Module):
self.args = args
self.vocab_size = args.vocab_size
self.num_hidden_layers = args.num_hidden_layers
self.layer_types = args.layer_types
self.sliding_window = args.sliding_window
assert self.vocab_size > 0
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
for layer_type in self.layer_types
]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.fa_idx = self.layer_types.index("full_attention")
self.swa_idx = None
for e, l in enumerate(self.layers):
if l.use_sliding:
self.swa_idx = e
break
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
@@ -164,14 +181,18 @@ class LlamaModel(nn.Module):
else:
h = self.embed_tokens(inputs)
if mask is None:
mask = create_attention_mask(h, cache)
if cache is None:
cache = [None] * len(self.layers)
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
fa_mask = create_attention_mask(h, cache[self.fa_idx])
if self.swa_idx is not None:
swa_mask = create_attention_mask(
h, cache[self.swa_idx], window_size=self.sliding_window
)
for layer, cache in zip(self.layers, cache):
mask = swa_mask if layer.use_sliding else fa_mask
h = layer(h, mask, cache=cache)
return self.norm(h)
@@ -188,11 +209,10 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
out = self.model(inputs, mask, cache, input_embeddings)
out = self.model(inputs, cache, input_embeddings)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
@@ -208,6 +228,47 @@ class Model(nn.Module):
weights.pop("lm_head.weight", None)
return weights
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
for layer in self.model.layers:
# Shard the self attention
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
layer.self_attn.k_proj = shard_linear(
layer.self_attn.k_proj, "all-to-sharded", group=group
)
layer.self_attn.v_proj = shard_linear(
layer.self_attn.v_proj, "all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
layer.self_attn.n_heads //= N
layer.self_attn.n_kv_heads //= N
# Shard the MLP
layer.mlp.gate_proj = shard_linear(
layer.mlp.gate_proj, "all-to-sharded", group=group
)
layer.mlp.down_proj = shard_linear(
layer.mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.up_proj = shard_linear(
layer.mlp.up_proj, "all-to-sharded", group=group
)
@property
def layers(self):
return self.model.layers
def make_cache(self):
return [
(
RotatingKVCache(max_size=self.model.sliding_window)
if layer.use_sliding
else KVCache()
)
for layer in self.layers
]
+9 -17
View File
@@ -6,6 +6,7 @@ from typing import Any, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import ChunkedKVCache, KVCache
from .rope_utils import initialize_rope
@@ -17,7 +18,6 @@ class TextArgs(BaseModelArgs):
attention_bias: bool
attention_chunk_size: int
head_dim: int
hidden_act: str
hidden_size: int
interleave_moe_layer_step: int
intermediate_size: int
@@ -146,13 +146,14 @@ class MLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class MoE(nn.Module):
def __init__(self, args):
super().__init__()
self.top_k = args.num_experts_per_tok
assert self.top_k == 1, "Only 1 expert per token supported"
self.num_experts = args.num_local_experts
self.experts = SwitchGLU(
args.hidden_size, args.intermediate_size, self.num_experts
@@ -219,7 +220,6 @@ class LlamaModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
h = self.embed_tokens(inputs)
@@ -242,21 +242,15 @@ class LlamaModel(nn.Module):
token_pos = linds <= rinds
chunk_mask = (block_pos == 0) & token_pos
if mask is None:
mask = create_attention_mask(h, cache)
else:
chunk_mask &= mask
if cache is None:
cache = [None] * len(self.layers)
global_mask = create_attention_mask(h, cache[3])
for idx, (layer, c) in enumerate(zip(self.layers, cache)):
use_chunked_attention = (idx + 1) % 4 != 0
if use_chunked_attention:
local_mask = chunk_mask
else:
local_mask = mask
h = layer(h, local_mask, cache=c)
mask = chunk_mask if use_chunked_attention else global_mask
h = layer(h, mask, cache=c)
return self.norm(h)
@@ -274,10 +268,9 @@ class LanguageModel(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
out = self.model(inputs, mask, cache)
out = self.model(inputs, cache)
return self.lm_head(out)
@@ -291,10 +284,9 @@ class Model(nn.Module):
def __call__(
self,
inputs: mx.array,
mask: mx.array = None,
cache=None,
):
return self.language_model(inputs, mask, cache)
return self.language_model(inputs, cache)
def sanitize(self, weights):
def to_remove(k):
+182
View File
@@ -0,0 +1,182 @@
# 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_attention_heads: int
num_hidden_layers: int
vocab_size: int
intermediate_size: int
intermediate_size_mlp: int
num_key_value_heads: int
rms_norm_eps: float
rope_theta: float
head_dim: int
tie_word_embeddings: bool
no_rope_layers: list
use_qk_norm: bool
class Attention(nn.Module):
def __init__(self, args: ModelArgs, use_rope):
super().__init__()
self.args = args
self.n_heads = args.num_attention_heads
self.n_kv_heads = args.num_key_value_heads
self.head_dim = args.head_dim
self.scale = self.head_dim**-0.5
self.q_proj = nn.Linear(
args.hidden_size, self.n_heads * self.head_dim, bias=False
)
self.k_proj = nn.Linear(
args.hidden_size, self.n_kv_heads * self.head_dim, bias=False
)
self.v_proj = nn.Linear(
args.hidden_size, self.n_kv_heads * self.head_dim, bias=False
)
self.o_proj = nn.Linear(
self.n_heads * self.head_dim, args.hidden_size, bias=False
)
self.use_rope = use_rope
if use_rope:
self.rope = nn.RoPE(self.head_dim, traditional=True, base=args.rope_theta)
self.use_qk_norm = args.use_qk_norm
self.rms_norm_eps = args.rms_norm_eps
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = queries.reshape(B, L, self.n_heads, -1)
keys = keys.reshape(B, L, self.n_kv_heads, -1)
if self.use_qk_norm:
queries = mx.fast.rms_norm(queries, None, self.rms_norm_eps)
keys = mx.fast.rms_norm(keys, None, self.rms_norm_eps)
queries = queries.transpose(0, 2, 1, 3)
keys = keys.transpose(0, 2, 1, 3)
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
if self.use_rope:
offset = cache.offset if cache is not None else 0
queries = self.rope(queries, offset=offset)
keys = self.rope(keys, offset=offset)
if cache is not None:
keys, values = cache.update_and_fetch(keys, values)
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class MLP(nn.Module):
def __init__(self, dim, intermediate_size, activation=nn.silu):
super().__init__()
self.gate_proj = nn.Linear(dim, intermediate_size, bias=False)
self.up_proj = nn.Linear(dim, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, dim, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, use_rope):
super().__init__()
self.self_attn = Attention(args, use_rope)
self.feed_forward = MLP(
args.hidden_size,
args.intermediate_size_mlp,
)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
h = x + r
r = self.feed_forward(self.post_attention_layernorm(h))
return h + r
class LanguageModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.vocab_size = args.vocab_size
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
TransformerBlock(args=args, use_rope=args.no_rope_layers[i])
for i in range(args.num_hidden_layers)
]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
h = self.embed_tokens(inputs)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
return self.norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = LanguageModel(args)
self.tie_word_embeddings = args.tie_word_embeddings
if not self.tie_word_embeddings:
self.output = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
h = self.model(inputs, cache)
if self.tie_word_embeddings:
return h @ self.model.embed_tokens.weight.T
else:
return self.output(h)
@property
def layers(self):
return self.model.layers
+493
View File
@@ -0,0 +1,493 @@
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import CacheList, KVCache
from .mla import MultiLinear
from .rope_utils import initialize_rope
from .switch_layers import SwitchGLU
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
attention_method: str
zero_expert_type: str
hidden_size: int
ffn_hidden_size: int
moe_topk: int
expert_ffn_hidden_size: int
n_routed_experts: int
zero_expert_num: int
num_layers: int
vocab_size: int
max_position_embeddings: int
num_attention_heads: int
kv_lora_rank: int
q_lora_rank: int
qk_rope_head_dim: int
qk_nope_head_dim: int
v_head_dim: int
routed_scaling_factor: float
rms_norm_eps: float
rope_theta: float
mla_scale_q_lora: bool
mla_scale_kv_lora: bool
attention_bias: bool
norm_topk_prob: bool = False
router_bias: bool = False
rope_scaling: Optional[Dict] = None
class LongcatFlashMLA(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.qk_rope_head_dim = args.qk_rope_head_dim
self.qk_nope_head_dim = args.qk_nope_head_dim
self.kv_lora_rank = args.kv_lora_rank
self.q_lora_rank = args.q_lora_rank
self.v_head_dim = args.v_head_dim
self.qk_head_dim = args.qk_nope_head_dim + args.qk_rope_head_dim
self.scale = self.qk_head_dim**-0.5
if self.q_lora_rank is None:
self.q_proj = nn.Linear(
args.hidden_size,
self.num_attention_heads * self.qk_head_dim,
bias=False,
)
else:
self.q_a_proj = nn.Linear(
args.hidden_size, self.q_lora_rank, bias=args.attention_bias
)
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank)
self.q_b_proj = nn.Linear(
self.q_lora_rank,
self.num_attention_heads * self.qk_head_dim,
bias=False,
)
self.kv_a_proj_with_mqa = nn.Linear(
args.hidden_size,
self.kv_lora_rank + self.qk_rope_head_dim,
bias=args.attention_bias,
)
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank)
self.embed_q = MultiLinear(
self.qk_nope_head_dim, self.kv_lora_rank, self.num_attention_heads
)
self.unembed_out = MultiLinear(
self.kv_lora_rank, self.v_head_dim, self.num_attention_heads
)
self.o_proj = nn.Linear(
self.num_attention_heads * args.v_head_dim,
args.hidden_size,
bias=args.attention_bias,
)
if args.mla_scale_q_lora:
self.mla_scale_q_lora = (args.hidden_size / self.q_lora_rank) ** 0.5
if args.mla_scale_kv_lora:
self.mla_scale_kv_lora = (args.hidden_size / self.kv_lora_rank) ** 0.5
if args.rope_scaling is not None:
mscale_all_dim = args.rope_scaling.get("mscale_all_dim", 0)
if mscale_all_dim:
scaling_factor = args.rope_scaling["factor"]
if scaling_factor > 1:
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
self.scale = self.scale * s * s
self.rope = initialize_rope(
dims=self.qk_rope_head_dim,
base=args.rope_theta,
traditional=True,
scaling_config=args.rope_scaling,
max_position_embeddings=args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, _ = x.shape
if self.q_lora_rank is None:
q = 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_attention_heads, self.qk_head_dim).transpose(
0, 2, 1, 3
)
if self.mla_scale_q_lora is not None:
q = q * self.mla_scale_q_lora
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
compressed_kv = self.kv_a_proj_with_mqa(x)
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
kv_latent = self.kv_a_layernorm(compressed_kv)
if self.mla_scale_kv_lora is not None:
kv_latent = kv_latent * self.mla_scale_kv_lora
offset = cache.offset if cache is not None else 0
q_pe = self.rope(q_pe, offset)
k_pe = self.rope(k_pe, offset)
kv_latent = mx.expand_dims(kv_latent, axis=1)
if cache is not None:
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
if mask is not None:
pe_scores = mx.where(
mask,
pe_scores,
mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype),
)
if L == 1:
q_nope = self.embed_q(q_nope)
k = v = kv_latent
else:
k = self.embed_q(kv_latent, transpose=False)
v = self.unembed_out(kv_latent)
output = scaled_dot_product_attention(
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
)
if L == 1:
output = self.unembed_out(output)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class LongcatFlashMLP(nn.Module):
def __init__(self, args: ModelArgs, is_expert: bool = False):
super().__init__()
hidden_size = args.expert_ffn_hidden_size if is_expert else args.ffn_hidden_size
self.gate_proj = nn.Linear(args.hidden_size, hidden_size, bias=False)
self.up_proj = nn.Linear(args.hidden_size, hidden_size, bias=False)
self.down_proj = nn.Linear(hidden_size, args.hidden_size, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class LongcatFlashTopkRouter(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.config = args
self.top_k = args.moe_topk
self.n_routed_experts = args.n_routed_experts + args.zero_expert_num
self.routed_scaling_factor = args.routed_scaling_factor
self.norm_topk_prob = args.norm_topk_prob
self.router_bias = args.router_bias
self.classifier = nn.Linear(
args.hidden_size, self.n_routed_experts, bias=self.router_bias
)
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
def __call__(self, hidden_states: mx.array) -> Tuple[mx.array, mx.array]:
dtype = hidden_states.dtype
router_logits = self.classifier(hidden_states)
scores = mx.softmax(router_logits, axis=-1)
corrected_scores = scores + self.e_score_correction_bias
topk_indices = mx.argpartition(corrected_scores, kth=-self.top_k, axis=-1)[
..., -self.top_k :
]
topk_weights = mx.take_along_axis(scores, topk_indices, axis=-1)
if self.norm_topk_prob:
denominator = mx.sum(topk_weights, axis=-1, keepdims=True) + 1e-20
topk_weights = topk_weights / denominator
topk_weights = topk_weights * self.routed_scaling_factor
return topk_indices, topk_weights.astype(dtype)
class LongcatFlashMoE(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.config = args
self.num_experts_per_tok = args.moe_topk
self.n_routed_experts = args.n_routed_experts
self.zero_expert_num = args.zero_expert_num
self.zero_expert_type = args.zero_expert_type
self.switch_mlp = SwitchGLU(
args.hidden_size,
args.expert_ffn_hidden_size,
args.n_routed_experts,
)
self.router = LongcatFlashTopkRouter(args)
self.sharding_group = None
def __call__(self, hidden_states):
if self.sharding_group is not None:
hidden_states = sum_gradients(self.sharding_group)(hidden_states)
topk_indices, topk_weights = self.router(hidden_states)
# Process all regular experts at once
mask = topk_indices >= self.n_routed_experts
topk_indices = mx.where(mask, 0, topk_indices)
regular_weights = mx.where(mask, 0.0, topk_weights)
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
weighted_outputs = regular_outputs * regular_weights[..., None]
final_output = mx.sum(weighted_outputs, axis=-2)
if self.sharding_group is not None:
final_output = mx.distributed.all_sum(
final_output, group=self.sharding_group
)
# Add identity expert contribution after all_sum to avoid summing it N times
assert self.zero_expert_type == "identity"
identity_weights_sum = mx.sum(
mx.where(mask, topk_weights, 0.0), axis=-1, keepdims=True
)
final_output = final_output + hidden_states * identity_weights_sum
return final_output
class LongcatFlashDecoderLayer(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.hidden_size = args.hidden_size
self.mlp = LongcatFlashMoE(args)
self.self_attn = [LongcatFlashMLA(args) for _ in range(2)]
self.mlps = [LongcatFlashMLP(args, False) for _ in range(2)]
self.input_layernorm = [
nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) for _ in range(2)
]
self.post_attention_layernorm = [
nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) for _ in range(2)
]
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
hidden_states = x
shortcut_mlp_output = None
if cache is None:
cache = (None, None)
for i in range(2):
residual = hidden_states
hidden_states = self.input_layernorm[i](hidden_states)
hidden_states = self.self_attn[i](hidden_states, mask=mask, cache=cache[i])
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm[i](hidden_states)
if i == 0:
shortcut_mlp_output = self.mlp(hidden_states)
hidden_states = self.mlps[i](hidden_states)
hidden_states = residual + hidden_states
if i == 1:
hidden_states = hidden_states + shortcut_mlp_output
return hidden_states
class LongcatFlashModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_layers = args.num_layers
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [LongcatFlashDecoderLayer(args) for idx in range(args.num_layers)]
self.norm = nn.RMSNorm(args.hidden_size, args.rms_norm_eps)
def __call__(
self,
x: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
h = self.embed_tokens(x)
if cache is None:
cache = [(None, None)] * self.num_layers
mask = create_attention_mask(h, cache[0][0], return_array=True)
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
return self.norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = LongcatFlashModel(args)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
):
out = self.model(inputs, cache)
return self.lm_head(out)
@property
def layers(self):
return self.model.layers
@property
def quant_predicate(self):
def predicate(path, _):
if path.endswith("classifier"):
return {"group_size": 64, "bits": 8}
return True
return predicate
@property
def cast_predicate(self):
def predicate(k):
return "e_score_correction_bias" not in k
return predicate
def sanitize(self, weights):
for l in range(self.args.num_layers):
prefix = f"model.layers.{l}"
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
for k in ["weight", "scales", "biases"]:
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
to_join = [
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
for e in range(self.args.n_routed_experts)
]
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
for l in range(self.args.num_layers):
for i in range(2):
prefix = f"model.layers.{l}.self_attn.{i}"
kv_b_key = f"{prefix}.kv_b_proj.weight"
if kv_b_key in weights:
num_heads = self.args.num_attention_heads
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
quantized = f"{prefix}.kv_b_proj.scales" in weights
v = weights.pop(kv_b_key)
if quantized:
dims = self.args.kv_lora_rank
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
bits = (v.shape[-1] * 32) // dims
group_size = dims // scales.shape[-1]
v = mx.dequantize(
v, scales, biases, bits=bits, group_size=group_size
)
v = v.reshape(num_heads, head_dim, -1)
wk = mx.contiguous(
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
)
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
if quantized:
wk, wk_s, wk_b = mx.quantize(
wk, bits=bits, group_size=group_size
)
wv, wv_s, wv_b = mx.quantize(
wv, bits=bits, group_size=group_size
)
weights[f"{prefix}.embed_q.scales"] = wk_s
weights[f"{prefix}.embed_q.biases"] = wk_b
weights[f"{prefix}.unembed_out.scales"] = wv_s
weights[f"{prefix}.unembed_out.biases"] = wv_b
weights[f"{prefix}.embed_q.weight"] = wk
weights[f"{prefix}.unembed_out.weight"] = wv
new_weights = {}
for k, v in weights.items():
if k.startswith("model.mtp"):
continue
new_weights[k] = v
return new_weights
def make_cache(self):
return [CacheList(KVCache(), KVCache()) for _ in self.model.layers]
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
rank = group.rank()
for layer in self.model.layers:
for attn in layer.self_attn:
if attn.q_lora_rank is None:
attn.q_proj = shard_linear(
attn.q_proj, "all-to-sharded", group=group
)
else:
attn.q_b_proj = shard_linear(
attn.q_b_proj, "all-to-sharded", group=group
)
attn.o_proj = shard_linear(attn.o_proj, "sharded-to-all", group=group)
attn.num_attention_heads //= N
num_heads = attn.num_attention_heads
sh = rank * num_heads
eh = sh + num_heads
def shard_heads(w):
return w[sh:eh]
attn.embed_q.apply(shard_heads)
attn.unembed_out.apply(shard_heads)
for mlp in layer.mlps:
mlp.gate_proj = shard_linear(
mlp.gate_proj, "all-to-sharded", group=group
)
mlp.up_proj = shard_linear(mlp.up_proj, "all-to-sharded", group=group)
mlp.down_proj = shard_linear(
mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.sharding_group = group
shard_inplace(layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group)
shard_inplace(layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group)
shard_inplace(layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group)
+214
View File
@@ -0,0 +1,214 @@
# Copyright © 2026 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
from .base import BaseModelArgs, create_attention_mask
from .cache import ArraysCache, CacheList, KVCache
from .longcat_flash import LongcatFlashDecoderLayer
from .longcat_flash import Model as LongcatFlashLM
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
hidden_size: int
ffn_hidden_size: int
moe_topk: int
expert_ffn_hidden_size: int
n_routed_experts: int
zero_expert_num: int
num_layers: int
vocab_size: int
max_position_embeddings: int
num_attention_heads: int
kv_lora_rank: int
q_lora_rank: int
qk_rope_head_dim: int
qk_nope_head_dim: int
v_head_dim: int
routed_scaling_factor: float
rms_norm_eps: float
rope_theta: float
mla_scale_q_lora: bool
mla_scale_kv_lora: bool
attention_bias: bool = False
zero_expert_type: str = "identity"
ngram_vocab_size_ratio: int = 78
emb_neighbor_num: int = 4
emb_split_num: int = 4
norm_topk_prob: bool = False
router_bias: bool = False
rope_scaling: Optional[Dict] = None
class NgramEmbedding(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.vocab_size = args.vocab_size
self.hidden_size = args.hidden_size
self.m = args.ngram_vocab_size_ratio * args.vocab_size
self.k = args.emb_split_num
self.n = args.emb_neighbor_num
self.word_embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
num_embedders = self.k * (self.n - 1)
emb_dim = args.hidden_size // num_embedders
self.embedders = []
self.post_projs = []
for i in range(num_embedders):
emb_vocab_size = int(self.m + i * 2 + 1)
self.embedders.append(nn.Embedding(emb_vocab_size, emb_dim))
self.post_projs.append(nn.Linear(emb_dim, args.hidden_size, bias=False))
self._compute_vocab_mods()
def _compute_vocab_mods(self):
vocab_mods = {}
for i in range(2, self.n + 1):
for j in range(self.k):
index = (i - 2) * self.k + j
emb_vocab_dim = int(self.m + index * 2 + 1)
mods = []
power_mod = 1
for _ in range(i - 1):
power_mod = (power_mod * self.vocab_size) % emb_vocab_dim
mods.append(power_mod)
vocab_mods[(i, j)] = mods
self._vocab_mods = vocab_mods
def _shift_right(self, x: mx.array, n: int) -> mx.array:
if n <= 0:
return x
batch_size, seq_len = x.shape
if seq_len <= n:
return mx.zeros_like(x)
return mx.concatenate(
[mx.zeros((batch_size, n), dtype=x.dtype), x[..., :-n]], axis=-1
)
def _get_ngram_ids(
self,
input_ids: mx.array,
shifted_ids: Dict[int, mx.array],
vocab_mods: List[int],
ngram: int,
) -> mx.array:
ngram_ids = input_ids
for k in range(2, ngram + 1):
ngram_ids = ngram_ids + shifted_ids[k] * vocab_mods[k - 2]
return ngram_ids
def __call__(
self,
input_ids: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
seq_len = input_ids.shape[-1]
input_ids = input_ids.astype(mx.int64)
if cache is not None:
context = cache[0]
if context is None:
context = input_ids
else:
context = mx.concatenate([context, input_ids], axis=-1)
cache[0] = context[..., max(0, context.shape[-1] - self.n + 1) :]
else:
context = input_ids
x = self.word_embeddings(input_ids)
vocab_mods = self._vocab_mods
shifted_ids = {}
for i in range(2, self.n + 1):
shifted_ids[i] = self._shift_right(context, i - 1)
for i in range(2, self.n + 1):
for j in range(self.k):
index = (i - 2) * self.k + j
emb_vocab_dim = int(self.m + index * 2 + 1)
ngram_ids = self._get_ngram_ids(
context, shifted_ids, vocab_mods[(i, j)], ngram=i
)
new_ids = (ngram_ids % emb_vocab_dim)[..., -seq_len:]
x_ngram = self.embedders[index](new_ids)
x_proj = self.post_projs[index](x_ngram)
x = x + x_proj
return x / (1 + self.k * (self.n - 1))
class LongcatFlashNgramModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_layers = args.num_layers
self.ngram_embeddings = NgramEmbedding(args)
self.layers = [LongcatFlashDecoderLayer(args) for _ in range(args.num_layers)]
self.norm = nn.RMSNorm(args.hidden_size, args.rms_norm_eps)
def __call__(
self,
input_ids: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
if cache is None:
cache = [None] + [(None, None)] * self.num_layers
h = self.ngram_embeddings(input_ids, cache=cache[0])
mask = create_attention_mask(h, cache[1][0], return_array=True)
for layer, c in zip(self.layers, cache[1:]):
h = layer(h, mask, cache=c)
return self.norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = LongcatFlashNgramModel(args)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
):
out = self.model(inputs, cache)
return self.lm_head(out)
@property
def layers(self):
return self.model.layers
@property
def quant_predicate(self):
return LongcatFlashLM.quant_predicate.fget(self)
@property
def cast_predicate(self):
return LongcatFlashLM.cast_predicate.fget(self)
def sanitize(self, weights):
weights = LongcatFlashLM.sanitize(self, weights)
if "model.embed_tokens.weight" in weights:
weights["model.ngram_embeddings.word_embeddings.weight"] = weights.pop(
"model.embed_tokens.weight"
)
return weights
def make_cache(self):
return [ArraysCache(size=1)] + [
CacheList(KVCache(), KVCache()) for _ in self.model.layers
]
def shard(self, group: Optional[mx.distributed.Group] = None):
LongcatFlashLM.shard(self, group)
+23 -44
View File
@@ -6,8 +6,9 @@ from dataclasses import dataclass
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs
from .cache import MambaCache
from .cache import ArraysCache
@dataclass
@@ -50,32 +51,6 @@ class ModelArgs(BaseModelArgs):
self.use_bcdt_rms = True
class DepthWiseConv1d(nn.Module):
def __init__(self, channels, kernel_size, bias=True, padding=0):
super().__init__()
self.channels = channels
self.kernel_size = kernel_size
self.padding = padding
self.weight = mx.random.normal((self.channels, kernel_size, 1))
self.bias = mx.zeros((channels,)) if bias else None
def __call__(self, x, cache=None):
B, L, C = x.shape
groups, K, _ = self.weight.shape
if cache is not None:
x = mx.concatenate([cache, x], axis=1)
else:
x = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
y = mx.conv_general(x, self.weight, groups=groups)
if self.bias is not None:
y = y + self.bias
return y, x[:, -K + 1 :, :]
class MambaBlock(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
@@ -97,11 +72,13 @@ class MambaBlock(nn.Module):
self.hidden_size, self.intermediate_size * 2, bias=args.use_bias
)
self.conv1d = DepthWiseConv1d(
channels=self.intermediate_size,
self.conv1d = nn.Conv1d(
in_channels=self.intermediate_size,
out_channels=self.intermediate_size,
kernel_size=self.conv_kernel_size,
groups=self.intermediate_size,
bias=self.use_conv_bias,
padding=self.conv_kernel_size - 1,
padding=0,
)
self.x_proj = nn.Linear(
@@ -148,20 +125,22 @@ class MambaBlock(nn.Module):
B, T, D = x.shape
xz = self.in_proj(x)
x, z = xz.split(indices_or_sections=2, axis=-1)
conv_out, new_conv_cache = self.conv1d(x, conv_cache)
K = self.conv_kernel_size
if conv_cache is not None:
x_full = mx.concatenate([conv_cache, x], axis=1)
else:
x_full = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
conv_out = self.conv1d(x_full)
new_conv_cache = x_full[:, -(K - 1) :, :]
x = nn.silu(conv_out)
A = -mx.exp(self.A_log)
outputs = []
current_state = state_cache
y = []
for t in range(T):
y_t, current_state = self.ssm_step(x[:, t], A, current_state)
y.append(y_t)
y = mx.stack(y, axis=1)
z = self.out_proj(nn.silu(z) * y)
z = self.out_proj(swiglu(z, y))
return z, (new_conv_cache, current_state)
def __call__(self, x, cache):
@@ -174,7 +153,7 @@ class MambaBlock(nn.Module):
x, conv_cache, state_cache
)
if isinstance(cache, MambaCache):
if isinstance(cache, ArraysCache):
cache[0] = new_conv_cache
cache[1] = new_state_cache
@@ -228,15 +207,15 @@ class Model(nn.Module):
return logits
def make_cache(self):
return [ArraysCache(size=2) for _ in range(len(self.layers))]
@property
def layers(self):
return self.backbone.layers
def sanitize(self, weights):
for k, v in weights.items():
if "conv1d.weight" in k and v.shape[-1] != 1:
weights[k] = v.moveaxis(2, 1)
return weights
def make_cache(self):
return [MambaCache() for _ in range(len(self.layers))]
@property
def layers(self):
return self.backbone.layers

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