Compare commits

..

1 Commits

Author SHA1 Message Date
Awni Hannun 1e0c5727b6 add responses API with examples 2025-09-20 12:52:34 -07:00
177 changed files with 3102 additions and 22090 deletions
+100
View File
@@ -0,0 +1,100 @@
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
@@ -1,16 +0,0 @@
name: 'Setup macOS Environment'
description: 'Install dependencies for macOS'
inputs:
python-version:
description: 'Python version to use'
required: false
default: '3.10'
runs:
using: "composite"
steps:
- uses: conda-incubator/setup-miniconda@v3
with:
miniconda-version: "latest"
python-version: ${{ inputs.python-version }}
-44
View File
@@ -1,44 +0,0 @@
name: Build and Test
on:
push:
branches: ["main"]
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/head/main' }}
jobs:
check_lint:
if: github.repository == 'ml-explore/mlx-lm'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.10"
- uses: pre-commit/action@v3.0.1
mac_build_and_test:
if: github.repository == 'ml-explore/mlx-lm'
runs-on: [self-hosted, macos]
needs: check_lint
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-macos
- name: Install test dependencies
shell: bash -l {0}
run: |
pip install unittest-xml-reporting
pip install -e ".[test]"
- name: Run tests
shell: bash -l {0}
run: |
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
@@ -1,41 +0,0 @@
name: PyPI Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: read
jobs:
build_release:
if: github.repository == 'ml-explore/mlx-lm'
runs-on: ubuntu-22.04
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/p/mlx-lm
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Build package
shell: sh
run: |
pip install build
python -m build
- name: Upload artifacts
uses: actions/upload-artifact@v5
with:
overwrite: true
name: mlx-lm
path: dist/*
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://upload.pypi.org/legacy/
+13 -22
View File
@@ -8,26 +8,17 @@ with a short description of your contribution(s) below. For example:
MLX LM was developed with contributions from the following individuals:
- Shunta Saito: Added support for PLaMo models.
- Gökdeniz Gülmez: Added support for the following architectures:
OpenBMB's `MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1` and
`Mamba v2`, Z.ai & THUKEG's `GLM`, `GLM4`, `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)`.
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's
`MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1`, `Mamba v2`, Z.ai &
THUKEG's `GLM`, `GLM4`, Rednote `dots.llm1`, Baisu's `Ernie4.5 MoE`, inclusionAI's
`Bailing MoE e.g. Ling-family`, Klear team - Kuaishou Technology's `Klear`,
IBM's `Granite MoE`, Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's
`Apertus`, Nikity's `Lille130m`, Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE`;
Helped add support for the following model architectures: Alibaba Qwen's `Qwen3 & Qwen3MoE)`;
Added support for the following training algorithms: `Full Weight Fine-Tuning`, and the `Muon`
optimizer; Added support for the following other features: `Multiple Optimizers
to choose for training`, and `reporting training metrics to WandB (Weights & Biases)`.
- 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)`
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`.
+41 -15
View File
@@ -71,7 +71,7 @@ prompt = "Write a story about Einstein"
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=True,
messages, add_generation_prompt=True
)
text = generate(model, tokenizer, prompt=prompt, verbose=True)
@@ -130,7 +130,7 @@ prompt = "Write a story about Einstein"
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=True,
messages, add_generation_prompt=True
)
for response in stream_generate(model, tokenizer, prompt, max_tokens=512):
@@ -170,7 +170,7 @@ mlx_lm.generate --help
To quantize a model from the command line run:
```
mlx_lm.convert --model mistralai/Mistral-7B-Instruct-v0.3 -q
mlx_lm.convert --hf-path mistralai/Mistral-7B-Instruct-v0.3 -q
```
For more options run:
@@ -185,7 +185,7 @@ You can upload new models to Hugging Face by specifying `--upload-repo` to
```
mlx_lm.convert \
--model mistralai/Mistral-7B-Instruct-v0.3 \
--hf-path mistralai/Mistral-7B-Instruct-v0.3 \
-q \
--upload-repo mlx-community/my-4bit-mistral
```
@@ -236,19 +236,45 @@ for more usage details.
### Supported Models
`mlx-lm` supports thousands of LLMs available on the Hugging Face Hub. If the
model you want to run is not supported, file an
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet, submit
a pull request. Many supported models are available in various quantization
formats in the [MLX Community](https://huggingface.co/mlx-community) Hugging
Face organization.
`mlx-lm` supports thousands of Hugging Face format LLMs. If the model you want to
run is not supported, file an
[issue](https://github.com/ml-explore/mlx-lm/issues/new) or better yet,
submit a pull request.
For some models the tokenizer may require you to enable the `trust_remote_code`
option. You can do this by passing `--trust-remote-code` in the command line.
If you don't specify the flag explicitly, you will be prompted to trust remote
code in the terminal when running the model.
Here are a few examples of Hugging Face models that work with this example:
Tokenizer options can also be set in the Python API. For example:
- [mistralai/Mistral-7B-v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1)
- [meta-llama/Llama-2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf)
- [deepseek-ai/deepseek-coder-6.7b-instruct](https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-instruct)
- [01-ai/Yi-6B-Chat](https://huggingface.co/01-ai/Yi-6B-Chat)
- [microsoft/phi-2](https://huggingface.co/microsoft/phi-2)
- [mistralai/Mixtral-8x7B-Instruct-v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1)
- [Qwen/Qwen-7B](https://huggingface.co/Qwen/Qwen-7B)
- [pfnet/plamo-13b](https://huggingface.co/pfnet/plamo-13b)
- [pfnet/plamo-13b-instruct](https://huggingface.co/pfnet/plamo-13b-instruct)
- [stabilityai/stablelm-2-zephyr-1_6b](https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b)
- [internlm/internlm2-7b](https://huggingface.co/internlm/internlm2-7b)
- [tiiuae/falcon-mamba-7b-instruct](https://huggingface.co/tiiuae/falcon-mamba-7b-instruct)
Most
[Mistral](https://huggingface.co/models?library=transformers,safetensors&other=mistral&sort=trending),
[Llama](https://huggingface.co/models?library=transformers,safetensors&other=llama&sort=trending),
[Phi-2](https://huggingface.co/models?library=transformers,safetensors&other=phi&sort=trending),
and
[Mixtral](https://huggingface.co/models?library=transformers,safetensors&other=mixtral&sort=trending)
style models should work out of the box.
For some models (such as `Qwen` and `plamo`) the tokenizer requires you to
enable the `trust_remote_code` option. You can do this by passing
`--trust-remote-code` in the command line. If you don't specify the flag
explicitly, you will be prompted to trust remote code in the terminal when
running the model.
For `Qwen` models you must also specify the `eos_token`. You can do this by
passing `--eos-token "<|endoftext|>"` in the command
line.
These options can also be set in the Python API. For example:
```python
model, tokenizer = load(
-348
View File
@@ -1,348 +0,0 @@
"""
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
@@ -1,63 +0,0 @@
# Benchmarks
## Commands
The command for evaluating on MMLU Pro:
```
mlx_lm.evaluate --model model/repo --task mmlu_pro
```
The command for efficiency benchmarks:
```
mlx_lm.benchmark --model model/repo -p 2048 -g 128
```
To get the package versions run:
```
python -m mlx --version && python -m mlx_lm --version
```
## Models
<details>
<summary> Qwen/Qwen3-4B-Instruct-2507 </summary>
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
--------- | -------- | ------------------- | ------------------------ | --------- | ----
bf16 | 64.05 | 1780.63 | 52.47 | 9.02 | Qwen/Qwen3-4B-Instruct-2507
q8 | 63.85 | 1606.573| 86.907 | 5.254 | mlx-community/Qwen3-4B-Instruct-2507-8bit
q6 | 63.53 | 1576.73 | 104.68 | 4.25 | mlx-community/Qwen3-4B-Instruct-2507-6bit
q5 g32 | 63.16 | 1570.80 | 110.29 | 4.00 | mlx-community/Qwen3-4B-Instruct-2507-5bit-g32
q5 | 62.38 | 1584.33 | 116.39 | 3.86 | mlx-community/Qwen3-4B-Instruct-2507-5bit
q4 g32 | 61.46 | 1610.03 | 126.00 | 3.603 | mlx-community/Qwen3-4B-Instruct-2507-4bit-g32
q4 | 60.72 | 1622.27 | 134.52 | 3.35 | mlx-community/Qwen3-4B-Instruct-2507-4bit
- Performance benchmark on 64GB M4 Max
- mlx 0.29.2.dev20251008+85a8824a8
- mlx-lm 0.28.2
- macOS 26.1
</details>
<details>
<summary> Qwen/Qwen3-30B-A3B-Instruct-2507 </summary>
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
--------- | -------- | ------------------- | ------------------------ | --------- | ----
bf16 | 72.62 | :skull: | :skull: | :skull: | Qwen/Qwen3-30B-A3B-Instruct-2507
q8 | 72.46 | 1719.47 | 83.16 | 33.46 | mlx-community/Qwen3-30B-A3B-Instruct-2507-8bit
q6 | 72.41 | 1667.45 | 94.14 | 25.82 | mlx-community/Qwen3-30B-A3B-Instruct-2507-6bit
q5 | 71.97 | 1664.24 | 101.00 |22.01 | mlx-community/Qwen3-30B-A3B-Instruct-2507-5bit
q4 | 70.71 | 1753.90 | 113.33 |18.20 | mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit
- Performance benchmarks on 64GB M4 Max
- mlx 0.29.2.dev20251008+85a8824a8
- mlx-lm 0.28.2
- macOS 26.1
</details>
+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.gptq --model Qwen/Qwen3-0.6B
mlx_lm.awq --model Qwen/Qwen3-0.6B
```
The script can take anywhere from a few minutes to several hours depending on
+7 -12
View File
@@ -66,10 +66,9 @@ 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` 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).
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).
For example, to fine-tune a Mistral 7B you can use `--model
mistralai/Mistral-7B-v0.1`.
@@ -185,10 +184,9 @@ Face.
### Local Datasets
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.
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.
Currently, `*.jsonl` files support `chat`, `tools`, `completions`, and `text`
data formats. Here are examples of these formats:
@@ -373,10 +371,7 @@ of memory. Here are some tips to reduce memory use should you need to do so:
2. Try using a smaller batch size with `--batch-size`. The default is `4` so
setting this to `2` or `1` will reduce memory consumption. This may slow
things down a little, but will also reduce the memory use. You can increase
the effective batch size without increasing the memory use by accumulating
gradients using `--grad-accumulation-steps <N>` which will accumulate the
gradient of `<N>` batches before updating the parameters.
things down a little, but will also reduce the memory use.
3. Reduce the number of layers to fine-tune with `--num-layers`. The default
is `16`, so you can try `8` or `4`. This reduces the amount of memory
+21 -15
View File
@@ -30,6 +30,8 @@ To see a full list of options run:
mlx_lm.server --help
```
## Chat completions API
You can make a request to the model by running:
```shell
@@ -72,24 +74,12 @@ 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 multiplicative penalty to repeated
tokens. Defaults to `0.0` (disabled).
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
Defaults to `1.0`.
- `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`.
@@ -140,7 +130,23 @@ curl localhost:8080/v1/chat/completions \
- `completion_tokens`: The number of tokens generated.
- `total_tokens`: The total number of tokens, i.e. the sum of the above two fields.
### List Models
## Responses API
The responses API follows the [OpenAI responses API
spec](https://platform.openai.com/docs/quickstart?api-mode=responses)
To make a request, use the `/reponses` endpoint. For exapmle:
```shell
curl localhost:8080/responses \
-H "Content-Type: application/json" \
-d '{
"input": [{"role": "user", "content": "Say this is a test!"}],
"temperature": 0.7
}'
```
## Models API
Use the `v1/models` endpoint to list available models:
-9
View File
@@ -9,12 +9,3 @@ os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
from .convert import convert
from .generate import batch_generate, generate, stream_generate
from .utils import load
__all__ = [
"__version__",
"convert",
"batch_generate",
"generate",
"stream_generate",
"load",
]
+28 -3
View File
@@ -1,6 +1,31 @@
# Copyright © 2025 Apple Inc.
if __name__ == "__main__":
from . import cli
import importlib
import sys
cli.main()
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()
+1 -1
View File
@@ -1,3 +1,3 @@
# Copyright © 2023-2025 Apple Inc.
__version__ = "0.31.3"
__version__ = "0.28.0"
+28 -74
View File
@@ -1,13 +1,16 @@
# Copyright © 2025 Apple Inc.
import argparse
import time
import mlx.core as mx
from mlx_lm import batch_generate, load, stream_generate
from mlx_lm import batch_generate, stream_generate
from mlx_lm.generate import DEFAULT_MODEL
from mlx_lm.utils import pipeline_load, sharded_load
from mlx_lm.tokenizer_utils import load_tokenizer
from mlx_lm.utils import (
fetch_from_hub,
get_model_path,
)
def setup_arg_parser():
@@ -22,6 +25,11 @@ def setup_arg_parser():
),
default=None,
)
parser.add_argument(
"--trust-remote-code",
action="store_true",
help="Enable trusting remote code for tokenizer",
)
parser.add_argument(
"--prompt-tokens",
"-p",
@@ -50,29 +58,6 @@ 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
@@ -81,81 +66,50 @@ 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
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 = {}
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},
)
prompt_tokens = args.prompt_tokens
generation_tokens = args.generation_tokens
batch_size = args.batch_size
vocab_size = config.get("vocab_size") or config["text_config"]["vocab_size"]
prompts = mx.random.randint(0, vocab_size, (batch_size, prompt_tokens)).tolist()
prompts = mx.random.randint(
0, config["vocab_size"], (batch_size, prompt_tokens)
).tolist()
prompt = prompts[0]
def single_bench():
for response in stream_generate(
model,
tokenizer,
prompt,
max_tokens=generation_tokens,
prefill_step_size=args.prefill_step_size,
model, tokenizer, prompt, max_tokens=generation_tokens
):
pass
return response
def batch_bench():
return batch_generate(
model,
tokenizer,
prompts,
max_tokens=generation_tokens,
prefill_step_size=args.prefill_step_size,
model, tokenizer, prompts, max_tokens=generation_tokens
).stats
if batch_size == 1:
_bench = single_bench
else:
_bench = batch_bench
_bench = batch_bench
rprint("Running warmup..")
print("Running warmup..")
_bench()
report_keys = ["prompt_tps", "generation_tps", "peak_memory"]
rprint(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
print(f"Timing with {prompt_tokens=}, {generation_tokens=}, {batch_size=}.")
responses = []
for i in range(args.num_trials):
if args.delay > 0:
time.sleep(args.delay)
tic = time.perf_counter()
response = _bench()
toc = time.perf_counter()
responses.append(response)
results = [(k, getattr(response, k)) for k in report_keys]
results = [f"{k}={v:.3f}" for k, v in results]
results.append(f"total_time={toc - tic:.3f}")
rprint(f"Trial {i+1}: " + ", ".join(results))
print(f"Trial {i+1}: " + ", ".join(results))
def avg(k):
vals = (getattr(response, k) for response in responses)
@@ -163,7 +117,7 @@ def main():
results = [(k, avg(k)) for k in report_keys]
results = [f"{k}={v:.3f}" for k, v in results]
rprint(f"Averages: " + ", ".join(results))
print(f"Averages: " + ", ".join(results))
if __name__ == "__main__":
+17 -4
View File
@@ -41,6 +41,16 @@ 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,
@@ -97,12 +107,14 @@ def main():
args.prompt = sys.stdin.read() if args.prompt == "-" else args.prompt
if tokenizer.has_chat_template:
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:
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:
@@ -141,6 +153,7 @@ 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)
+20 -41
View File
@@ -7,13 +7,13 @@ import mlx.core as mx
from .generate import stream_generate
from .models.cache import make_prompt_cache
from .sample_utils import make_sampler
from .utils import load, sharded_load
from .utils import load
DEFAULT_TEMP = 0.0
DEFAULT_TOP_P = 1.0
DEFAULT_XTC_PROBABILITY = 0.0
DEFAULT_XTC_THRESHOLD = 0.0
DEFAULT_SEED = 0
DEFAULT_SEED = None
DEFAULT_MAX_TOKENS = 256
DEFAULT_MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
@@ -79,11 +79,6 @@ 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
@@ -91,41 +86,28 @@ def main():
parser = setup_arg_parser()
args = parser.parse_args()
group = mx.distributed.init()
rank = group.rank()
pipeline_group = group if args.pipeline else None
tensor_group = group if not args.pipeline else None
if args.seed is not None:
mx.random.seed(args.seed)
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
},
)
model, tokenizer = load(
args.model,
adapter_path=args.adapter_path,
tokenizer_config={
"trust_remote_code": True if args.trust_remote_code else None
},
)
def print_help():
rprint("The command list:")
rprint("- 'q' to exit")
rprint("- 'r' to reset the chat")
rprint("- 'h' to display these commands")
print("The command list:")
print("- 'q' to exit")
print("- 'r' to reset the chat")
print("- 'h' to display these commands")
rprint(f"[INFO] Starting chat session with {args.model}.")
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(">> " if rank == 0 else "")
query = input(">> ")
if query == "q":
break
if query == "r":
@@ -138,10 +120,7 @@ def main():
if args.system_prompt is not None:
messages.append({"role": "system", "content": args.system_prompt})
messages.append({"role": "user", "content": query})
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
)
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
for response in stream_generate(
model,
tokenizer,
@@ -158,8 +137,8 @@ def main():
),
prompt_cache=prompt_cache,
):
rprint(response.text, flush=True, end="")
rprint()
print(response.text, flush=True, end="")
print()
if __name__ == "__main__":
View File
-345
View File
@@ -1,345 +0,0 @@
# 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
@@ -1,53 +0,0 @@
# 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}")
+19 -40
View File
@@ -10,7 +10,8 @@ from mlx.utils import tree_map_with_path
from .utils import (
dequantize_model,
load,
fetch_from_hub,
get_model_path,
quantize_model,
save,
upload_to_hub,
@@ -20,7 +21,6 @@ from .utils import (
def mixed_quant_predicate_builder(
recipe: str, model: nn.Module, group_size: int = 64
) -> Callable[[str, nn.Module, dict], Union[bool, dict]]:
mode = "affine"
high_bits = 6
if recipe == "mixed_2_6":
@@ -63,16 +63,14 @@ def mixed_quant_predicate_builder(
or index >= 7 * num_layers // 8
or (index - num_layers // 8) % 3 == 2
)
if (
"v_proj" in path or "v_a_proj" in path or "v_b_proj" in path
) and use_more_bits:
return {"group_size": group_size, "bits": high_bits, "mode": mode}
if "v_proj" in path and use_more_bits:
return {"group_size": group_size, "bits": high_bits}
if "down_proj" in path and use_more_bits:
return {"group_size": group_size, "bits": high_bits, "mode": mode}
return {"group_size": group_size, "bits": high_bits}
if "lm_head" in path:
return {"group_size": group_size, "bits": high_bits, "mode": mode}
return {"group_size": group_size, "bits": high_bits}
return {"group_size": group_size, "bits": low_bits, "mode": mode}
return {"group_size": group_size, "bits": low_bits}
return mixed_quant_predicate
@@ -86,8 +84,8 @@ def convert(
hf_path: str,
mlx_path: str = "mlx_model",
quantize: bool = False,
q_group_size: Optional[int] = None,
q_bits: Optional[int] = None,
q_group_size: int = 64,
q_bits: int = 4,
q_mode: str = "affine",
dtype: Optional[str] = None,
upload_repo: str = None,
@@ -109,27 +107,18 @@ def convert(
)
print("[INFO] Loading")
model, tokenizer, config = load(
hf_path,
revision=revision,
return_config=True,
tokenizer_config={"trust_remote_code": trust_remote_code},
lazy=True,
model_path, 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
)
if isinstance(quant_predicate, str):
if q_mode != "affine":
raise ValueError(f"Quant predicates only support 'affine' quantization.")
quant_predicate = mixed_quant_predicate_builder(
quant_predicate,
model,
q_group_size,
quant_predicate, model, q_group_size
)
if dtype is None:
dtype = config.get("torch_dtype", None)
if dtype is None and (text_config := config.get("text_config", None)):
dtype = text_config.get("dtype", None)
if dtype in MODEL_CONVERSION_DTYPES:
print("[INFO] Using dtype:", dtype)
dtype = getattr(mx, dtype)
@@ -165,10 +154,11 @@ def convert(
save(
mlx_path,
hf_path,
model_path,
model,
tokenizer,
config,
hf_repo=hf_path,
)
if upload_repo is not None:
@@ -186,12 +176,7 @@ def configure_parser() -> argparse.ArgumentParser:
description="Convert Hugging Face model to MLX format"
)
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("--hf-path", type=str, help="Path to the Hugging Face model.")
parser.add_argument(
"--mlx-path", type=str, default="mlx_model", help="Path to save the MLX model."
)
@@ -199,23 +184,17 @@ 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=None,
"--q-group-size", help="Group size for quantization.", type=int, default=64
)
parser.add_argument(
"--q-bits",
help="Bits per weight for quantization.",
type=int,
default=None,
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
)
parser.add_argument(
"--q-mode",
help="The quantization mode.",
type=str,
default="affine",
choices=["affine", "mxfp4", "nvfp4", "mxfp8"],
choices=["affine", "mxfp4"],
)
parser.add_argument(
"--quant-predicate",
+15 -83
View File
@@ -12,7 +12,7 @@ import logging
import os
from importlib.metadata import version
from pathlib import Path
from typing import Any, Callable, Optional
from typing import Any, Optional
import lm_eval
import mlx.core as mx
@@ -20,14 +20,12 @@ import mlx.nn as nn
import numpy as np
from lm_eval.api.model import LM
from lm_eval.api.registry import register_model
from lm_eval.models import huggingface
from tqdm import tqdm
from .generate import batch_generate
from .models.cache import make_prompt_cache
from .sample_utils import make_sampler
from .utils import load
DEFAULT_MAX_TOKENS = 8192
from .utils import common_prefix_len, load
def _rstrip_until(s, untils):
@@ -38,13 +36,6 @@ def _rstrip_until(s, untils):
return s[: min(f)]
def _lstrip(s, pattern):
"""Truncate the prefix of the string after the first occurrence of pattern."""
if (idx := s.find(pattern)) != -1:
return s[idx + len(pattern) :]
return s
def _pad_inputs(inputs):
lengths = np.array([len(x) for x in inputs])
maxlen = lengths.max()
@@ -71,16 +62,15 @@ 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,
max_tokens: int,
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}
@@ -88,11 +78,10 @@ class MLXLM(LM):
path_or_hf_repo, tokenizer_config=tokenizer_config
)
self._max_tokens = max_tokens
self._batch_size = batch_size
self._batch_size = 8
self.use_chat_template = use_chat_template
if use_chat_template is None:
self.use_chat_template = self.tokenizer.chat_template is not None
self._sampler = sampler
def _process_prompt(self, prompt, step_size: int = 2048):
prompt = mx.array(prompt)[None]
@@ -145,10 +134,6 @@ 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
@@ -197,8 +182,7 @@ class MLXLM(LM):
max_completed_l = max(len(s) for s in full_sequences)
# compute truncation length
max_tokens = self._max_tokens or DEFAULT_MAX_TOKENS
truncation = max(0, max_completed_l - max_tokens - 1)
truncation = max(0, max_completed_l - self._max_tokens - 1)
orig_prefix_l = len(prefix)
prefix_l = max(len(prefix) - truncation, 0)
prefix = prefix[len(prefix) - prefix_l :]
@@ -320,16 +304,11 @@ class MLXLM(LM):
continuation: str
The generated continuation.
"""
group = mx.distributed.init()
# split data accross ranks
total_requests = len(requests)
requests = requests[group.rank() :: group.size()]
logging.info("Generating continuation for %d sequences." % len(requests))
contexts, options = zip(*[req.args for req in requests])
# The second element of the tuple contains:
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
completions = []
# Tokenize all contexts
contexts = [
@@ -340,10 +319,7 @@ class MLXLM(LM):
]
# 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
]
max_tokens = [opt.get("max_gen_toks", self._max_tokens) for opt in options]
completions = batch_generate(
model=self._model,
@@ -351,46 +327,12 @@ class MLXLM(LM):
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)
]
until = opt["until"]
if any(u in text for u in until):
completions[e] = _rstrip_until(text, until)
return completions
@@ -408,9 +350,8 @@ def main():
parser.add_argument(
"--max-tokens",
type=int,
help="Maximum number of tokens to generate. When set, this value takes"
" precedence over task specific defaults.",
default=None,
help="Maximum number of tokens to generate.",
default=8912,
)
parser.add_argument(
"--limit",
@@ -452,9 +393,7 @@ 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)
@@ -471,18 +410,11 @@ 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)
+3 -22
View File
@@ -26,26 +26,7 @@ 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])
result = batch_generate(model, tokenizer, prompts, verbose=False, max_tokens=128)
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])
# The returned result contains texts completions in the same order as prompts
print(result.texts[0])
+2 -8
View File
@@ -15,10 +15,7 @@ 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(
@@ -32,10 +29,7 @@ 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(
+1 -2
View File
@@ -14,8 +14,7 @@ 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
+2 -4
View File
@@ -1,5 +1,5 @@
# The path to the local model directory or Hugging Face repo.
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
model: "mlx-community/Llama-3.2-1B-Instruct"
# Whether or not to train (boolean)
train: true
@@ -47,9 +47,6 @@ steps_per_report: 10
# Number of training steps between validations.
steps_per_eval: 200
# Number of micro-steps to accumulate before each optimizer update.
grad_accumulation_steps: 1
# Load path to resume training with the given adapter weights.
resume_adapter_file: null
@@ -93,3 +90,4 @@ lora_parameters:
# valid_split: "train[-100:]"
# prompt_feature: "text"
# completion_feature: "summary"
@@ -1,40 +0,0 @@
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()
+59
View File
@@ -0,0 +1,59 @@
# Copyright © 2025 Apple Inc.
"""
Examples using the OpenAI responses endpoint with mlx_lm.server.
To run, first start the server:
>>> mlx_lm.server
Then run this script.
More documentation on the API spec here:
https://platform.openai.com/docs/quickstart?api-mode=responses
"""
from openai import OpenAI
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
### Basic response example
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.responses.create(
model=model, input="Write a one-sentence bedtime story about a unicorn."
)
print(response.output_text)
### Input with roles
response = client.responses.create(
model=model,
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Write a one-sentence bedtime story about a unicorn.",
},
],
}
],
)
print(response.output_text)
### Streaming
stream = client.responses.create(
model=model,
input=[
{
"role": "user",
"content": "Say 'double bubble bath' ten times fast.",
},
],
stream=True,
)
for event in stream:
print(event)
+1 -3
View File
@@ -8,13 +8,11 @@ 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-Instruct-2507-4bit"
model = "mlx-community/qwen3-4b-4bit-DWQ"
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
tools = [
+135
View File
@@ -0,0 +1,135 @@
# 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
@@ -1,86 +0,0 @@
# 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")
+8 -9
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/Qwen3-4B-Instruct-2507-4bit"
checkpoint = "mlx-community/Qwen2.5-32B-Instruct-4bit"
# Load the corresponding model and tokenizer
model, tokenizer = load(path_or_hf_repo=checkpoint)
@@ -31,9 +31,7 @@ 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)
@@ -49,11 +47,12 @@ response = generate(
)
# Parse the tool call:
# - The tool call format is model specific.
# - The tokenizer's tool parser expects tool call text to be already extracted.
start_tool = response.find(tokenizer.tool_call_start) + len(tokenizer.tool_call_start)
end_tool = response.find(tokenizer.tool_call_end)
tool_call = tokenizer.tool_parser(response[start_tool:end_tool].strip())
# (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())
tool_result = tools[tool_call["name"]](**tool_call["arguments"])
# Put the tool result in the prompt
+21 -14
View File
@@ -4,9 +4,10 @@ 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 (
dequantize_model,
load,
fetch_from_hub,
get_model_path,
save,
upload_to_hub,
)
@@ -39,8 +40,8 @@ def parse_arguments() -> argparse.Namespace:
default=None,
)
parser.add_argument(
"--dequantize",
help="Generate a dequantized model.",
"--de-quantize",
help="Generate a de-quantized model.",
action="store_true",
)
parser.add_argument(
@@ -61,12 +62,14 @@ def main() -> None:
print("Loading pretrained model")
args = parse_arguments()
model, tokenizer, config = load(
args.model, adapter_path=args.adapter_path, return_config=True
)
model_path, hf_path = get_model_path(args.model)
model, config, tokenizer = fetch_from_hub(model_path)
model.freeze()
model = load_adapters(model, args.adapter_path)
fused_linears = [
(n, m.fuse(dequantize=args.dequantize))
(n, m.fuse(de_quantize=args.de_quantize))
for n, m in model.named_modules()
if hasattr(m, "fuse")
]
@@ -74,19 +77,19 @@ def main() -> None:
if fused_linears:
model.update_modules(tree_unflatten(fused_linears))
if args.dequantize:
print("Dequantizing model")
model = dequantize_model(model)
if args.de_quantize:
print("De-quantizing model")
model = dequantize(model)
config.pop("quantization", None)
config.pop("quantization_config", None)
save_path = Path(args.save_path)
save(
save_path,
args.model,
model_path,
model,
tokenizer,
config,
hf_repo=hf_path,
donate_model=False,
)
@@ -97,9 +100,13 @@ def main() -> None:
f"Model type {model_type} not supported for GGUF conversion."
)
weights = dict(tree_flatten(model.parameters()))
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
if args.upload_repo is not None:
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)
+270 -1120
View File
File diff suppressed because it is too large Load Diff
+20 -16
View File
@@ -21,7 +21,7 @@ from .tuner.utils import (
load_adapters,
print_trainable_parameters,
)
from .utils import _parse_size, load, save_config
from .utils import load, save_config
yaml_loader = yaml.SafeLoader
yaml_loader.add_implicit_resolver(
@@ -40,7 +40,7 @@ yaml_loader.add_implicit_resolver(
)
CONFIG_DEFAULTS = {
"model": "Qwen/Qwen3-0.6b",
"model": "mlx_model",
"train": False,
"fine_tune_type": "lora",
"optimizer": "adam",
@@ -51,7 +51,7 @@ CONFIG_DEFAULTS = {
"sgd": {},
"adafactor": {},
},
"data": "mlx-community/WikiSQL",
"data": "data/",
"seed": 0,
"num_layers": 16,
"batch_size": 4,
@@ -68,11 +68,10 @@ 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,
}
@@ -143,11 +142,6 @@ def build_parser():
type=int,
help="Number of training steps between validations.",
)
parser.add_argument(
"--grad-accumulation-steps",
type=int,
help="Number of steps to accumulate before each optimizer update.",
)
parser.add_argument(
"--resume-adapter-file",
type=str,
@@ -191,11 +185,14 @@ def build_parser():
help="Use gradient checkpointing to reduce memory use.",
default=None,
)
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( # 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(
"--report-to",
@@ -268,7 +265,6 @@ 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
@@ -318,6 +314,14 @@ def evaluate_model(args, model: nn.Module, test_set):
def run(args, training_callback: TrainingCallback = None):
np.random.seed(args.seed)
if args.wandb is not None:
warnings.warn(
"The 'wandb' argument is deprecated and will be removed in a future release. "
"Use 'report_to: wandb' and 'project_name' in the configuration instead.",
DeprecationWarning,
)
args.report_to = "wandb"
args.project_name = args.wandb
training_callback = get_reporting_callbacks(
args.report_to,
project_name=args.project_name,
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -115,7 +114,7 @@ class KlearMLP(nn.Module):
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class KlearSparseMoeBlock(nn.Module):
-43
View File
@@ -1,43 +0,0 @@
# 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)
+8 -4
View File
@@ -9,7 +9,6 @@ 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
@@ -51,7 +50,7 @@ class FusedLoRALinear(nn.Module):
]
self.lora_b = [mx.zeros((r, od)) for od in output_dims]
def fuse(self, dequantize: bool = False):
def fuse(self, de_quantize: bool = False):
linear = self.linear
weight = linear.weight
is_quantized = isinstance(linear, FusedQuantizedLinear)
@@ -80,7 +79,7 @@ class FusedLoRALinear(nn.Module):
delta = mx.concatenate(deltas, axis=0)
fused_linear.weight = weight + delta
if is_quantized and not dequantize:
if is_quantized and not de_quantize:
fused_linear = fused_linear.to_quantized(linear.group_size, linear.bits)
return fused_linear
@@ -263,6 +262,11 @@ 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__()
@@ -277,7 +281,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):
-405
View File
@@ -1,405 +0,0 @@
# Copyright © 2024 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .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
+34 -10
View File
@@ -7,7 +7,6 @@ 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
@@ -33,6 +32,38 @@ class ModelArgs(BaseModelArgs):
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
@partial(mx.compile, shapeless=True)
def xielu(x, alpha_p, alpha_n, beta, eps):
alpha_p = nn.softplus(alpha_p)
alpha_n = beta + nn.softplus(alpha_n)
return mx.where(
x > 0,
alpha_p * mx.square(x) + beta * x,
(mx.expm1(mx.minimum(x, eps)) - x) * alpha_n + beta * x,
)
class XieLU(nn.Module):
def __init__(
self,
alpha_p_init=0.8,
alpha_n_init=0.8,
beta=0.5,
eps=-1e-6,
):
super().__init__()
alpha_p_tensor = mx.array(alpha_p_init)
alpha_n_tensor = mx.array(alpha_n_init - beta)
self.alpha_p = mx.log(mx.exp(alpha_p_tensor) - 1)
self.alpha_n = mx.log(mx.exp(alpha_n_tensor) - 1)
self.beta = mx.array(beta)
self.eps = mx.array(eps)
def __call__(self, x: mx.array) -> mx.array:
return xielu(x, self.alpha_p, self.alpha_n, self.beta, self.eps)
class ApertusMLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
@@ -167,8 +198,7 @@ class Model(nn.Module):
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)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
@@ -176,18 +206,12 @@ class Model(nn.Module):
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
return self.lm_head(out)
def sanitize(self, weights):
for k, v in weights.items():
if k.endswith("alpha_p") or k.endswith("alpha_n"):
weights[k] = v.squeeze()
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
return weights
@property
+3 -4
View File
@@ -6,9 +6,8 @@ from typing import Any, List, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import ArraysCache, CacheList, KVCache, RotatingKVCache
from .cache import CacheList, KVCache, MambaCache, RotatingKVCache
@dataclass
@@ -141,7 +140,7 @@ class MLP(nn.Module):
)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class DecoderLayer(nn.Module):
@@ -223,7 +222,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 = ArraysCache(size=2)
conv_cache = MambaCache()
if is_swa:
kv_cache = RotatingKVCache(max_size=self.config.sliding_window)
else:
+6 -19
View File
@@ -1,13 +1,11 @@
# 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
@@ -39,7 +37,6 @@ class ModelArgs(BaseModelArgs):
use_qk_norm: bool = False
tie_word_embeddings: bool = False
partial_rotary_factor: float = 1.0
rotary_dim: Optional[int] = None
moe_router_enable_expert_bias: bool = False
moe_router_enable_routed_scaling: bool = True
routed_scaling_factor: float = 1.0
@@ -50,13 +47,6 @@ class ModelArgs(BaseModelArgs):
moe_router_enable_shared_expert: bool = True
@partial(mx.compile, shapeless=True)
def aggregate_expert_outputs(expert_outputs, scores):
return (
(expert_outputs * scores[..., None]).sum(axis=-2).astype(expert_outputs.dtype)
)
class BailingMoeMLP(nn.Module):
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
super().__init__()
@@ -77,7 +67,7 @@ class BailingMoeMLP(nn.Module):
)
def __call__(self, x) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class BailingMoeAttention(nn.Module):
@@ -104,10 +94,8 @@ class BailingMoeAttention(nn.Module):
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
if (rope_dim := args.rotary_dim) is None:
rope_dim = int(self.head_dim * args.partial_rotary_factor)
self.rope = initialize_rope(
rope_dim,
int(self.head_dim * args.partial_rotary_factor),
args.rope_theta,
traditional=False,
scaling_config=args.rope_scaling,
@@ -158,7 +146,6 @@ class BailingMoeAttention(nn.Module):
return self.dense(output)
@mx.compile
def group_expert_select(
gates,
e_score_correction_bias,
@@ -184,15 +171,15 @@ def group_expert_select(
k = n_group - topk_group
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
scores = mx.put_along_axis(
scores, mx.stop_gradient(group_idx), mx.array(0.0, scores.dtype), axis=-2
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, axis=-1)[..., -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) + 1e-20
denominator = scores.sum(axis=-1, keepdims=True)
scores = scores / denominator
scores = scores * routed_scaling_factor
@@ -258,7 +245,7 @@ class BailingMoeSparseMoeBlock(nn.Module):
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)
out = (out * topk_weight[..., None]).sum(axis=-2)
if self.shared_experts is not None:
out = out + self.shared_experts(x)
return out
-595
View File
@@ -1,595 +0,0 @@
# Copyright © 2025 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
from .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
+49 -1054
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -110,7 +109,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
def __call__(self, x):
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -107,7 +106,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
def __call__(self, x):
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
+1 -2
View File
@@ -7,7 +7,6 @@ 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
@@ -108,7 +107,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 = swiglu(self.w1(x), self.v1(x))
current_hidden_states = nn.silu(self.w1(x)) * self.v1(x)
current_hidden_states = self.w2(current_hidden_states)
return current_hidden_states
+1 -2
View File
@@ -4,7 +4,6 @@ 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
@@ -121,7 +120,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class MoEGate(nn.Module):
+31 -77
View File
@@ -6,11 +6,8 @@ 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
@@ -261,7 +258,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(swiglu(self.gate_proj(x), self.up_proj(x)))
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return down_proj
@@ -317,21 +314,13 @@ 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
@@ -366,7 +355,7 @@ class DeepseekV2DecoderLayer(nn.Module):
return out
class DeepseekV2Model(PipelineMixin, nn.Module):
class DeepseekV2Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -375,8 +364,32 @@ class DeepseekV2Model(PipelineMixin, nn.Module):
DeepseekV2DecoderLayer(config, idx)
for idx in range(config.num_hidden_layers)
]
self.start_idx = 0
self.end_idx = len(self.layers)
self.num_layers = self.end_idx
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.pipeline_rank = 0
self.pipeline_size = 1
def pipeline(self, group):
# Split layers in reverse so rank=0 gets the last layers and
# rank=pipeline_size-1 gets the first
self.pipeline_rank = group.rank()
self.pipeline_size = group.size()
layers_per_rank = len(self.layers) // self.pipeline_size
extra = len(self.layers) - layers_per_rank * self.pipeline_size
if self.pipeline_rank < extra:
layers_per_rank += 1
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
self.end_idx = self.start_idx + layers_per_rank
self.num_layers = layers_per_rank
self.layers = self.layers[: self.end_idx]
self.layers[: self.start_idx] = [None] * self.start_idx
self.num_layers = len(self.layers) - self.start_idx
def __call__(
self,
x: mx.array,
@@ -388,25 +401,22 @@ class DeepseekV2Model(PipelineMixin, nn.Module):
pipeline_size = self.pipeline_size
if cache is None:
cache = [None] * len(self.pipeline_layers)
cache = [None] * self.num_layers
mask = create_attention_mask(h, cache[0])
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
@@ -440,62 +450,6 @@ 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.pipeline_layers
return self.model.layers[self.model.start_idx : self.model.end_idx]
+158 -182
View File
@@ -7,13 +7,8 @@ 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
@@ -50,6 +45,85 @@ 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__()
@@ -86,11 +160,11 @@ class DeepseekV3Attention(nn.Module):
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.kv_b_proj = nn.Linear(
self.kv_lora_rank,
self.num_heads
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
bias=False,
)
self.o_proj = nn.Linear(
@@ -101,19 +175,35 @@ class DeepseekV3Attention(nn.Module):
if self.config.rope_scaling is not None:
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
scaling_factor = self.config.rope_scaling["factor"]
if mscale_all_dim:
scaling_factor = self.config.rope_scaling["factor"]
if scaling_factor > 1:
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
self.scale = self.scale * s * s
mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
self.scale = self.scale * mscale * mscale
self.rope = initialize_rope(
dims=self.qk_rope_head_dim,
base=self.rope_theta,
traditional=True,
max_position_embeddings=self.max_position_embeddings,
scaling_config=self.config.rope_scaling,
)
rope_kwargs = {
key: self.config.rope_scaling[key]
for key in [
"original_max_position_embeddings",
"beta_fast",
"beta_slow",
"mscale",
"mscale_all_dim",
]
if key in self.config.rope_scaling
}
self.rope = DeepseekV3YarnRotaryEmbedding(
dim=self.qk_rope_head_dim,
max_position_embeddings=self.max_position_embeddings,
scaling_factor=scaling_factor,
base=self.rope_theta,
**rope_kwargs,
)
else:
self.rope = nn.RoPE(
dims=self.qk_rope_head_dim,
base=self.rope_theta,
traditional=True,
)
def __call__(
self,
@@ -133,38 +223,29 @@ 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_latent = self.kv_a_layernorm(compressed_kv)
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)
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)
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], 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),
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
)
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)
q_pe = self.rope(q_pe)
k_pe = self.rope(k_pe)
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
keys = mx.concatenate([k_nope, k_pe], axis=-1)
queries = mx.concatenate([q_nope, q_pe], axis=-1)
output = scaled_dot_product_attention(
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
if L == 1:
output = self.unembed_out(output)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
@@ -185,7 +266,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(swiglu(self.gate_proj(x), self.up_proj(x)))
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return down_proj
@@ -268,21 +349,13 @@ 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
@@ -316,7 +389,7 @@ class DeepseekV3DecoderLayer(nn.Module):
return h + r
class DeepseekV3Model(PipelineMixin, nn.Module):
class DeepseekV3Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -325,7 +398,28 @@ class DeepseekV3Model(PipelineMixin, nn.Module):
DeepseekV3DecoderLayer(config, idx)
for idx in range(config.num_hidden_layers)
]
self.start_idx = 0
self.end_idx = len(self.layers)
self.num_layers = self.end_idx
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.pipeline_rank = 0
self.pipeline_size = 1
def pipeline(self, group):
# Split layers in reverse so rank=0 gets the last layers and
# rank=pipeline_size-1 gets the first
self.pipeline_rank = group.rank()
self.pipeline_size = group.size()
layers_per_rank = len(self.layers) // self.pipeline_size
extra = len(self.layers) - layers_per_rank * self.pipeline_size
if self.pipeline_rank < extra:
layers_per_rank += 1
self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank
self.end_idx = self.start_idx + layers_per_rank
self.layers = self.layers[: self.end_idx]
self.layers[: self.start_idx] = [None] * self.start_idx
self.num_layers = len(self.layers) - self.start_idx
def __call__(
self,
@@ -338,25 +432,23 @@ class DeepseekV3Model(PipelineMixin, nn.Module):
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)
cache = [None] * self.num_layers
mask = create_attention_mask(h, cache[0])
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
@@ -379,8 +471,7 @@ class Model(nn.Module):
def sanitize(self, weights):
def dequant(weight, scale_inv):
dtype = mx.bfloat16
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
dtype = weight.dtype
bs = 128 # block size
m, n = weight.shape
pad_bottom = (-m) % bs
@@ -394,22 +485,7 @@ class Model(nn.Module):
)
return weight[:m, :n].astype(dtype)
# Remap for int4
new_weights = {}
for k, v in weights.items():
if k.endswith("weight_shape"):
base = k.replace("weight_shape", "")
new_weights[base + "weight"] = weights[base + "weight_packed"].view(
mx.uint32
)
s = weights[base + "weight_scale"]
new_weights[base + "scales"] = s
new_weights[base + "biases"] = -8 * s
elif not (k.endswith("weight_scale") or k.endswith("weight_packed")):
new_weights[k] = v
weights = new_weights
# Dequantize fp8
# Dequantize
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
@@ -433,42 +509,6 @@ 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 {
@@ -477,73 +517,9 @@ 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.pipeline_layers
return self.model.layers[self.model.start_idx : self.model.end_idx]
@property
def cast_predicate(self):
-654
View File
@@ -1,654 +0,0 @@
# Copyright © 2025 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional
import mlx.core as mx
import mlx.nn as nn
from 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]
+1 -2
View File
@@ -7,7 +7,6 @@ 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
@@ -181,7 +180,7 @@ class Dots1MLP(nn.Module):
)
def __call__(self, x) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Dots1MoE(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -88,7 +87,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class DecoderLayer(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -99,7 +98,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Ernie4_5_MoeMLP(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -92,7 +91,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(swiglu(self.c_fc_0(x), self.c_fc_1(x)))
return self.c_proj(nn.silu(self.c_fc_0(x)) * self.c_fc_1(x))
class TransformerBlock(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -103,7 +102,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
-439
View File
@@ -1,439 +0,0 @@
# 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
@@ -1,504 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass, field
from typing import List, Optional
import mlx.core as mx
import mlx.nn as nn
from .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
+55 -119
View File
@@ -7,27 +7,15 @@ 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))
return mx.exp(
-mx.exp(A_log.astype(mx.float32)) * nn.softplus(a + dt_bias).astype(A_log.dtype)
)
def _make_gated_delta_kernel(has_mask=False, vectorized=False):
def _make_gated_delta_kernel():
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"""
source = """
auto n = thread_position_in_grid.z;
auto b_idx = n / Hv;
auto hv_idx = n % Hv;
@@ -50,80 +38,60 @@ def _make_gated_delta_kernel(has_mask=False, vectorized=False):
auto o_state = state_out + (n * Dv + dv_idx) * Dk;
float state[n_per_t];
for (int i = 0; i < n_per_t; ++i) {{
auto s_idx = n_per_t * dk_idx + i;
state[i] = static_cast<float>(i_state[s_idx]);
}}
for (int i = 0; i < n_per_t; ++i) {
auto s_idx = n_per_t * dk_idx + i;
state[i] = static_cast<float>(i_state[s_idx]);
}
{g_comment}
{g_setup}
// beta, g: [B, T, Hv]
auto g_ = g + b_idx * T * Hv;
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);
for (int t = 0; t < T; ++t) {
float kv_mem = 0.0f;
for (int i = 0; i < n_per_t; ++i) {
auto s_idx = n_per_t * dk_idx + i;
state[i] = state[i] * g_[hv_idx];
kv_mem += state[i] * k_[s_idx];
}
kv_mem = simd_sum(kv_mem);
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
float out = 0.0f;
for (int i = 0; i < n_per_t; ++i) {{
auto s_idx = n_per_t * dk_idx + i;
state[i] = state[i] + k_[s_idx] * delta;
out += state[i] * q_[s_idx];
}}
out = simd_sum(out);
if (thread_index_in_simdgroup == 0) {{
y[dv_idx] = static_cast<InT>(out);
}}
}} else {{
y[dv_idx] = static_cast<InT>(0);
}}
float out = 0.0f;
for (int i = 0; i < n_per_t; ++i) {
auto s_idx = n_per_t * dk_idx + i;
state[i] = state[i] + k_[s_idx] * delta;
out += state[i] * q_[s_idx];
}
out = simd_sum(out);
if (thread_index_in_simdgroup == 0) {
y[dv_idx] = static_cast<InT>(out);
}
// Increment data pointers to next time step
q_ += Hk * Dk;
k_ += Hk * Dk;
v_ += Hv * Dv;
y += Hv * Dv;
{g_advance}
g_ += Hv;
beta_ += Hv;
}}
for (int i = 0; i < n_per_t; ++i) {{
}
for (int i = 0; i < n_per_t; ++i) {
auto s_idx = n_per_t * dk_idx + i;
o_state[s_idx] = static_cast<StT>(state[i]);
}}
o_state[s_idx] = static_cast<InT>(state[i]);
}
"""
inputs = ["q", "k", "v", "g", "beta", "state_in", "T"]
if has_mask:
inputs.append("mask")
suffix = ""
if vectorized:
suffix += "_vec"
if has_mask:
suffix += "_mask"
return mx.fast.metal_kernel(
name=f"gated_delta_step{suffix}",
input_names=inputs,
name="gated_delta_step",
input_names=["q", "k", "v", "g", "beta", "state_in", "T"],
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
)
_gated_delta_kernel = _make_gated_delta_kernel()
@mx.compile
def _gated_delta_step_ops(
q: mx.array,
k: mx.array,
@@ -131,7 +99,6 @@ def _gated_delta_step_ops(
g: mx.array,
beta: mx.array,
state: mx.array,
mask: Optional[mx.array] = None,
) -> Tuple[mx.array, mx.array]:
"""
Ops-based reference implementation for a single recurrent step.
@@ -139,8 +106,7 @@ def _gated_delta_step_ops(
Shapes:
- q, k: [B, H, Dk]
- v: [B, H, Dv]
- g: [B, H] or [B, H, Dk]
- beta: [B, H]
- g, beta: [B, H]
- state: [B, H, Dv, Dk]
Returns:
- y: [B, H, Dv]
@@ -148,24 +114,13 @@ def _gated_delta_step_ops(
"""
# 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
state = state * g[..., None, None]
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
return y, state
def gated_delta_kernel(
@@ -175,30 +130,14 @@ def gated_delta_kernel(
g: mx.array,
beta: mx.array,
state: mx.array,
mask: Optional[mx.array] = None,
) -> Tuple[mx.array, mx.array]:
B, T, Hk, Dk = k.shape
Hv, Dv = v.shape[2:]
input_type = q.dtype
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,
return _gated_delta_kernel(
inputs=[q, k, v, g, beta, state, T],
template=[
("InT", input_type),
("StT", state_type),
("Dk", Dk),
("Dv", Dv),
("Hk", Hk),
@@ -207,7 +146,7 @@ def gated_delta_kernel(
grid=(32, Dv, B * Hv),
threadgroup=(32, 4, 1),
output_shapes=[(B, T, Hv, Dv), state.shape],
output_dtypes=[input_type, state_type],
output_dtypes=[input_type, input_type],
)
@@ -218,26 +157,23 @@ def gated_delta_ops(
g: mx.array,
beta: mx.array,
state: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
) -> Tuple[mx.array, mx.array]:
"""
Ops-based reference implementation for prompt prefill (sequential loop).
Supports both scalar and vectorized gating.
Shapes:
- q, k: [B, T, Hk, Dk]
- v: [B, T, Hv, Dv]
- g: [B, T, Hv] (scalar) or [B, T, Hv, Dk] (vectorized)
- beta: [B, T, Hv]
- state: [B, Hv, Dv, Dk]
- g, beta: [B, T, Hv]
- state: [B, Hv, Dk, Dv]
Returns:
- y: [B, T, Hv, Dv]
- state: [B, Hv, Dv, Dk]
- state: [B, Hv, Dk, Dv]
"""
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)
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
if (repeat_factor := Hv // Hk) > 1:
q = mx.repeat(q, repeat_factor, -2)
@@ -252,7 +188,6 @@ def gated_delta_ops(
g[:, t],
beta[:, t],
state,
None if mask is None else mask[:, t],
)
ys.append(y)
y = mx.stack(ys, axis=1)
@@ -268,16 +203,17 @@ def gated_delta_update(
A_log: mx.array,
dt_bias: mx.array,
state: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
use_kernel: bool = True,
) -> Tuple[mx.array, mx.array]:
beta = mx.sigmoid(b)
g = compute_g(A_log, a, dt_bias)
if state is None:
B, _, Hk, Dk = q.shape
Hv, Dv = v.shape[-2:]
state = mx.zeros((B, Hv, Dv, Dk), dtype=mx.float32)
if state is None:
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
if not use_kernel or mx.default_device() != mx.gpu or not mx.metal.is_available():
return gated_delta_ops(q, k, v, g, beta, state, mask)
return gated_delta_kernel(q, k, v, g, beta, state, mask)
if mx.default_device() != mx.gpu or not mx.metal.is_available():
return gated_delta_ops(q, k, v, g, beta, state)
else:
return gated_delta_kernel(q, k, v, g, beta, state)
+23 -28
View File
@@ -2,14 +2,13 @@
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, Optional
from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .rope_utils import initialize_rope
@dataclass
@@ -23,13 +22,12 @@ class ModelArgs(BaseModelArgs):
rms_norm_eps: float = 1.0e-6
vocab_size: int = 262144
num_key_value_heads: int = 1
rope_theta: float = 1_000_000.0
rope_global_base_freq: float = 1_000_000.0
rope_local_base_freq: float = 10_000.0
rope_traditional: bool = False
query_pre_attn_scalar: float = 256
sliding_window: int = 512
sliding_window_pattern: int = 6
max_position_embeddings: int = 32768
rope_scaling: Dict = None
class Attention(nn.Module):
@@ -54,20 +52,15 @@ 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
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,
)
self.rope = nn.RoPE(
head_dim,
traditional=args.rope_traditional,
base=(
args.rope_local_base_freq
if self.is_sliding
else args.rope_global_base_freq
),
)
def __call__(
self,
@@ -94,6 +87,8 @@ 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
)
@@ -194,14 +189,12 @@ class Gemma3Model(nn.Module):
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
sliding_window_mask = create_attention_mask(
h,
cache[0],
window_size=self.window_size,
)
for i, (layer, c) in enumerate(zip(self.layers, cache)):
is_global = (
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
@@ -253,5 +246,7 @@ class Model(nn.Module):
):
caches.append(KVCache())
else:
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
caches.append(
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
)
return caches
-92
View File
@@ -1,92 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from typing import Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten, tree_unflatten
from . import 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
@@ -1,688 +0,0 @@
# 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
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -103,7 +102,7 @@ class GLMMLP(nn.Module):
def __call__(self, x) -> mx.array:
x = self.gate_up_proj(x)
gate, x = mx.split(x, 2, axis=-1)
return self.down_proj(swiglu(gate, x))
return self.down_proj(nn.silu(gate) * x)
class GLMBlock(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -39,7 +38,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(swiglu(gate, up_states))
return self.down_proj(nn.silu(gate) * up_states)
class Glm4Attention(nn.Module):
+11 -89
View File
@@ -7,11 +7,8 @@ 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
@@ -124,7 +121,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(swiglu(self.gate_proj(x), self.up_proj(x)))
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
return down_proj
@@ -207,21 +204,13 @@ 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
@@ -254,7 +243,7 @@ class DecoderLayer(nn.Module):
return h + r
class LanguageModel(PipelineMixin, nn.Module):
class LanguageModel(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
@@ -262,6 +251,10 @@ class LanguageModel(PipelineMixin, 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__(
@@ -271,29 +264,13 @@ class LanguageModel(PipelineMixin, nn.Module):
) -> 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)
cache = [None] * self.num_layers
mask = create_attention_mask(h, cache[0])
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for l, c in zip(self.pipeline_layers, cache):
h = l(h, mask, cache=c)
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
for i in range(self.num_layers):
h = self.layers[self.start_idx + i](h, mask, cache[i])
return self.norm(h)
@@ -336,64 +313,9 @@ 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.pipeline_layers
return self.model.layers
@property
def cast_predicate(self):
-531
View File
@@ -1,531 +0,0 @@
# 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
@@ -1,53 +0,0 @@
# 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)
+2 -4
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
@@ -138,9 +138,7 @@ class GPT2Model(nn.Module):
if cache[0] is not None:
offset = cache[0].offset
offset = mx.array(offset)
position_ids = mx.arange(L) + offset[..., None]
position_ids = mx.arange(offset, offset + L)
hidden_states += self.wpe(position_ids)
mask = create_attention_mask(hidden_states, cache[0])
+6 -16
View File
@@ -23,7 +23,6 @@ class ModelArgs(BaseModelArgs):
vocab_size: int
rotary_emb_base: int
rotary_pct: float
use_parallel_residual: bool = True
num_key_value_heads: int = None
def __post_init__(self):
@@ -108,7 +107,6 @@ class TransformerBlock(nn.Module):
self.layer_norm_eps = args.layer_norm_eps
self.attention = Attention(args)
self.mlp = MLP(args)
self.use_parallel_residual = args.use_parallel_residual
self.input_layernorm = nn.LayerNorm(
self.hidden_size,
eps=self.layer_norm_eps,
@@ -123,20 +121,12 @@ class TransformerBlock(nn.Module):
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
if self.use_parallel_residual:
residual = x
# Run attention and feedforward network in parallel.
attn = self.attention(self.input_layernorm(x), mask, cache)
ffn = self.mlp(self.post_attention_layernorm(x))
out = attn + ffn + residual
return out
else:
# Run attention and feedforward network sequentially.
attn_output = self.attention(self.input_layernorm(x), mask, cache)
x = x + attn_output
ffn_output = self.mlp(self.post_attention_layernorm(x))
x = x + ffn_output
return x
residual = x
# NeoX runs attention and feedforward network in parallel.
attn = self.attention(self.input_layernorm(x), mask, cache)
ffn = self.mlp(self.post_attention_layernorm(x))
out = attn + ffn + residual
return out
class GPTNeoXModel(nn.Module):
+1 -53
View File
@@ -7,7 +7,6 @@ from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
@@ -143,12 +142,8 @@ 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)
@@ -157,13 +152,7 @@ class MLPBlock(nn.Module):
x = self.experts(x, indices)
x = x * mx.expand_dims(expert_weights, axis=-1)
y = x.sum(axis=-2)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
return x.sum(axis=-2)
class TransformerBlock(nn.Module):
@@ -279,47 +268,6 @@ 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
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -105,7 +104,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
+63 -149
View File
@@ -6,14 +6,13 @@ 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 .cache import KVCache, MambaCache
from .rope_utils import initialize_rope
from .ssm import ssm_update
from .switch_layers import SwitchGLU
@@ -21,7 +20,6 @@ from .switch_layers import SwitchGLU
@dataclass
class ModelArgs(BaseModelArgs):
# Required fields (no defaults)
model_type: str
vocab_size: int
hidden_size: int
@@ -31,42 +29,34 @@ class ModelArgs(BaseModelArgs):
num_attention_heads: int
num_key_value_heads: int
attention_bias: bool
# Scalar multipliers
embedding_multiplier: float
attention_multiplier: float
logits_scaling: float
residual_multiplier: float
# MoE parameters
num_local_experts: int
num_experts_per_tok: int
shared_intermediate_size: int
# Mamba parameters
mamba_n_heads: int
mamba_d_head: int
mamba_proj_bias: bool
mamba_d_state: int
mamba_d_conv: int
mamba_n_groups: int
mamba_conv_bias: bool
layer_types: List[str]
rms_norm_eps: float
rope_theta: float
# Optional fields (with defaults)
# MoE parameters (optional for dense mode)
num_local_experts: Optional[int] = None
num_experts_per_tok: Optional[int] = None
shared_intermediate_size: Optional[int] = None
# Mamba parameters (optional for non-hybrid mode)
mamba_n_heads: Optional[int] = None
mamba_d_head: Optional[int] = None
mamba_proj_bias: Optional[bool] = None
mamba_d_state: Optional[int] = None
mamba_d_conv: Optional[int] = None
mamba_n_groups: Optional[int] = None
mamba_conv_bias: Optional[bool] = None
# Dense MLP parameters (for non-MoE mode)
mlp_bias: bool = False
# Other optional parameters
position_embedding_type: str = "rope"
tie_word_embeddings: bool = True
time_step_limit: Tuple[float, float] = (0.001, 100.0)
# Mode flags - inferred from num_local_experts
@property
def use_moe(self) -> bool:
return bool(self.num_local_experts)
class GraniteMoeHybridRMSNormGated(nn.Module):
def __init__(self, hidden_size: int, eps: float = 1e-6):
@@ -76,7 +66,7 @@ class GraniteMoeHybridRMSNormGated(nn.Module):
def __call__(self, hidden_states: mx.array, gate: mx.array = None) -> mx.array:
if gate is not None:
hidden_states = swiglu(gate, hidden_states)
hidden_states = hidden_states * nn.silu(gate)
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
@@ -120,36 +110,21 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
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],
def _apply_conv(
self, conv_input: mx.array, cache: Optional[MambaCache] = None
) -> mx.array:
if mask is not None:
conv_input = mx.where(mask[..., None], conv_input, 0)
if cache is None or cache[0] is None:
conv_state = mx.zeros(
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
dtype=conv_input.dtype,
)
else:
conv_state = cache[0]
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
if cache is not None:
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)]
)
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
conv_output = self.conv1d(padded_input)
return nn.silu(conv_output)
@@ -160,8 +135,8 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
B: mx.array,
C: mx.array,
dt: mx.array,
cache: Optional[ArraysCache],
mask: Optional[mx.array],
state: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
) -> mx.array:
batch_size, seq_len, _ = hidden_states.shape
@@ -170,34 +145,27 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
)
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
if cache:
state = cache[1]
lengths = cache.lengths
else:
state, lengths = None, None
y, state = ssm_update(
hidden_states,
self.A_log,
B,
C,
self.D.astype(hidden_states.dtype),
self.D,
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)
return y.reshape(batch_size, seq_len, self.intermediate_size), state
def __call__(
self,
hidden_states: mx.array,
mask: Optional[mx.array],
cache: Optional[ArraysCache] = None,
mask: Optional[mx.array] = None,
cache: Optional[MambaCache] = None,
) -> mx.array:
projected = self.in_proj(hidden_states)
@@ -207,7 +175,11 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
[self.intermediate_size, self.intermediate_size + self.conv_dim],
axis=-1,
)
conv_output = self._conv(conv_input, cache, mask)
if mask is not None:
conv_input = mx.where(mask[..., None], conv_input, 0)
conv_output = self._apply_conv(conv_input, cache)
hidden_states_ssm, B, C = mx.split(
conv_output,
[
@@ -216,9 +188,10 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
],
axis=-1,
)
y = self._ssm(hidden_states_ssm, B, C, dt, cache, mask)
state = cache[1] if cache else None
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
if cache:
cache.advance(y.shape[1])
cache[1] = state
y = self.norm(y, gate)
return self.out_proj(y)
@@ -338,22 +311,7 @@ class GraniteMoeHybridSharedMLP(nn.Module):
def __call__(self, x: mx.array) -> mx.array:
gate, up = mx.split(self.input_linear(x), 2, axis=-1)
return self.output_linear(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)))
return self.output_linear(nn.silu(gate) * up)
class GraniteMoeHybridLayer(nn.Module):
@@ -361,7 +319,6 @@ class GraniteMoeHybridLayer(nn.Module):
super().__init__()
self.layer_type = layer_type
self.residual_multiplier = args.residual_multiplier
self.use_moe = args.use_moe
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
@@ -372,14 +329,8 @@ class GraniteMoeHybridLayer(nn.Module):
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.shared_mlp = GraniteMoeHybridSharedMLP(args)
self.block_sparse_moe = GraniteMoeHybridMoE(args)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
@@ -401,16 +352,13 @@ class GraniteMoeHybridLayer(nn.Module):
hidden_states = residual + hidden_states * self.residual_multiplier
# Second block: MoE + shared_mlp OR dense MLP
# Second block: MoE + shared_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)
moe_out = self.block_sparse_moe(normed)
shared_out = self.shared_mlp(normed)
mlp_out = moe_out + shared_out
hidden_states = residual + mlp_out * self.residual_multiplier
@@ -427,16 +375,9 @@ class GraniteMoeHybridModel(nn.Module):
]
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
)
self.fa_idx = args.layer_types.index("attention")
self.ssm_idx = args.layer_types.index("mamba")
self.layer_types = args.layer_types
def __call__(
self,
@@ -448,16 +389,11 @@ class GraniteMoeHybridModel(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
# Create masks based on what layer types exist
attn_mask = None
mamba_mask = None
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
mamba_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
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):
cache_counter = 0
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
mask = attn_mask if layer.layer_type == "attention" else mamba_mask
hidden_states = layer(hidden_states, mask=mask, cache=c)
@@ -496,7 +432,7 @@ class Model(nn.Module):
caches = []
for layer in self.layers:
if layer.layer_type == "mamba":
caches.append(ArraysCache(size=2))
caches.append(MambaCache())
elif layer.layer_type == "attention":
caches.append(KVCache())
return caches
@@ -507,11 +443,8 @@ class Model(nn.Module):
if "conv1d.weight" in k and v.shape[-1] != 1:
weights[k] = v.moveaxis(2, 1)
# Handle MoE weight transformation to SwitchGLU format (only for MoE models)
if (
self.args.use_moe
and "model.layers.0.block_sparse_moe.input_linear.weight" in weights
):
# Handle MoE weight transformation to SwitchGLU format
if "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"
@@ -528,31 +461,12 @@ class Model(nn.Module):
f"{prefix}.output_linear.weight"
)
# Handle dense MLP weight transformation (for dense models)
elif (
not self.args.use_moe
and "model.layers.0.shared_mlp.input_linear.weight" in weights
):
for l in range(self.args.num_hidden_layers):
prefix = f"model.layers.{l}.shared_mlp"
# Transform shared_mlp weights to standard mlp weights
input_weight = weights.pop(f"{prefix}.input_linear.weight")
# Split into gate and up projections (each half)
gate_proj, up_proj = mx.split(input_weight, 2, axis=0)
weights[f"model.layers.{l}.mlp.gate_proj.weight"] = gate_proj
weights[f"model.layers.{l}.mlp.up_proj.weight"] = up_proj
weights[f"model.layers.{l}.mlp.down_proj.weight"] = weights.pop(
f"{prefix}.output_linear.weight"
)
return weights
@property
def quant_predicate(self):
def predicate(path, _):
if self.args.use_moe and path.endswith("router.layer"):
if path.endswith("router.layer"):
return {"group_size": 64, "bits": 8}
return True
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -93,7 +92,7 @@ class HeliumMLP(nn.Module):
)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class HeliumDecoderLayer(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -149,7 +148,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Gate(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -145,7 +144,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -157,7 +156,7 @@ class MLP(nn.Module):
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.w2(swiglu(self.w1(x), self.w3(x)))
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
class TransformerBlock(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -155,7 +154,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
-286
View File
@@ -1,286 +0,0 @@
# 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
@@ -1,385 +0,0 @@
# Copyright © 2025 Apple Inc.
import math
from dataclasses import dataclass
from typing import Any, List, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .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
@@ -1,83 +0,0 @@
# 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
@@ -1,611 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
import mlx.core as mx
import mlx.nn as nn
from .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
View File
@@ -18,6 +18,11 @@ 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):
+13 -38
View File
@@ -5,7 +5,6 @@ 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,
@@ -32,22 +31,8 @@ class ModelArgs(BaseModelArgs):
block_multiple_of: int
block_ffn_dim_multiplier: float
block_auto_adjust_ff_dim: bool
rope_theta: float = 1000000.0
rope_parameters: Optional[dict] = None
full_attn_idxs: Optional[List[int]] = None
layer_types: Optional[List[str]] = None
def __post_init__(self):
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
self.rope_theta = self.rope_parameters["rope_theta"]
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.full_attn_idxs is None:
self.full_attn_idxs = [
i
for i, layer_type in enumerate(self.layer_types)
if layer_type == "full_attention"
]
full_attn_idxs: List[int]
rope_theta: float
class Attention(nn.Module):
@@ -137,33 +122,23 @@ class ShortConv(nn.Module):
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
seqlen = x.shape[1]
BCx = self.in_proj(x)
B, C, x = mx.split(BCx, 3, axis=-1)
Bx = B * x
if mask is not None:
Bx = mx.where(mask[..., None], Bx, 0)
state = None
if cache is not None:
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)])
state = cache[0]
if state is None:
state = mx.zeros(
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
)
Bx = mx.concatenate([state, Bx], axis=-2)
if cache is not None:
cache[0] = Bx[:, -(self.L_cache - 1) :]
conv_out = self.conv(Bx)
y = C * conv_out
@@ -191,7 +166,7 @@ class MLP(nn.Module):
self.w2 = nn.Linear(ff_dim, dim, bias=False)
def __call__(self, x) -> mx.array:
return self.w2(swiglu(self.w1(x), self.w3(x)))
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
class Lfm2DecoderLayer(nn.Module):
-387
View File
@@ -1,387 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from typing import Any, List, Optional
import mlx.core as mx
import mlx.nn as nn
from .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
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -88,7 +87,7 @@ class Lille130mMLP(nn.Module):
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)))
return self.down_proj(nn.silu(self.gate_proj(h)) * self.up_proj(h))
class Lille130Block(nn.Module):
+7 -71
View File
@@ -1,15 +1,12 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from 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
@@ -31,16 +28,11 @@ class ModelArgs(BaseModelArgs):
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = True
layer_types: Optional[List[str]] = None
sliding_window: Optional[int] = None
def __post_init__(self):
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.layer_types is None:
self.layer_types = ["full_attention"] * self.num_hidden_layers
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
@@ -118,15 +110,14 @@ 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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, use_sliding: bool = False):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.hidden_size = args.hidden_size
self.use_sliding = use_sliding
self.self_attn = Attention(args)
self.mlp = MLP(args)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
@@ -154,21 +145,12 @@ class LlamaModel(nn.Module):
self.args = args
self.vocab_size = args.vocab_size
self.num_hidden_layers = args.num_hidden_layers
self.layer_types = args.layer_types
self.sliding_window = args.sliding_window
assert self.vocab_size > 0
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
for layer_type in self.layer_types
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.fa_idx = self.layer_types.index("full_attention")
self.swa_idx = None
for e, l in enumerate(self.layers):
if l.use_sliding:
self.swa_idx = e
break
def __call__(
self,
@@ -184,15 +166,10 @@ class LlamaModel(nn.Module):
if cache is None:
cache = [None] * len(self.layers)
fa_mask = create_attention_mask(h, cache[self.fa_idx])
if self.swa_idx is not None:
swa_mask = create_attention_mask(
h, cache[self.swa_idx], window_size=self.sliding_window
)
mask = create_attention_mask(h, cache[0])
for layer, cache in zip(self.layers, cache):
mask = swa_mask if layer.use_sliding else fa_mask
h = layer(h, mask, cache=cache)
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
return self.norm(h)
@@ -228,47 +205,6 @@ 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
]
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -146,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class MoE(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -96,7 +95,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(intermediate_size, dim, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
+47 -159
View File
@@ -4,13 +4,9 @@ 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
@@ -42,7 +38,6 @@ class ModelArgs(BaseModelArgs):
attention_bias: bool
norm_topk_prob: bool = False
router_bias: bool = False
rope_scaling: Optional[Dict] = None
class LongcatFlashMLA(nn.Module):
@@ -81,11 +76,10 @@ class LongcatFlashMLA(nn.Module):
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.kv_b_proj = nn.Linear(
self.kv_lora_rank,
self.num_attention_heads * (self.qk_nope_head_dim + args.v_head_dim),
bias=False,
)
self.o_proj = nn.Linear(
@@ -99,20 +93,8 @@ class LongcatFlashMLA(nn.Module):
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,
self.rope = nn.RoPE(
dims=self.qk_rope_head_dim, base=args.rope_theta, traditional=True
)
def __call__(
@@ -124,59 +106,56 @@ class LongcatFlashMLA(nn.Module):
B, L, _ = x.shape
if self.q_lora_rank is None:
q = self.q_proj(x)
q_states = self.q_proj(x)
else:
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
q_states = 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
)
q_states = q_states.reshape(B, L, -1, self.qk_head_dim).transpose(0, 2, 1, 3)
if self.mla_scale_q_lora is not None:
q = q * self.mla_scale_q_lora
q_states = q_states * self.mla_scale_q_lora
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
q_pass, q_rot = mx.split(q_states, [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)
k_pass, k_rot = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
k_pass = self.kv_a_layernorm(k_pass)
if self.mla_scale_kv_lora is not None:
kv_latent = kv_latent * self.mla_scale_kv_lora
k_pass = k_pass * 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)
key_shape = (B, L, -1, self.qk_nope_head_dim + self.v_head_dim)
k_pass = self.kv_b_proj(k_pass).reshape(*key_shape).transpose(0, 2, 1, 3)
k_pass, value_states = mx.split(k_pass, [self.qk_nope_head_dim], axis=-1)
kv_latent = mx.expand_dims(kv_latent, axis=1)
k_rot = k_rot.reshape(B, 1, L, self.qk_rope_head_dim)
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
q_rot = self.rope(q_rot, cache.offset)
k_rot = self.rope(k_rot, cache.offset)
else:
k = self.embed_q(kv_latent, transpose=False)
v = self.unembed_out(kv_latent)
q_rot = self.rope(q_rot)
k_rot = self.rope(k_rot)
output = scaled_dot_product_attention(
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
k_rot = mx.broadcast_to(k_rot, (*k_pass.shape[:-1], k_rot.shape[-1]))
query_states = mx.concatenate([q_pass, q_rot], axis=-1)
key_states = mx.concatenate([k_pass, k_rot], axis=-1)
if cache is not None:
key_states, value_states = cache.update_and_fetch(key_states, value_states)
attn_output = scaled_dot_product_attention(
query_states,
key_states,
value_states,
cache=cache,
scale=self.scale,
mask=mask,
)
if L == 1:
output = self.unembed_out(output)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(attn_output)
class LongcatFlashMLP(nn.Module):
@@ -189,7 +168,7 @@ class LongcatFlashMLP(nn.Module):
self.down_proj = nn.Linear(hidden_size, args.hidden_size, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class LongcatFlashTopkRouter(nn.Module):
@@ -244,11 +223,8 @@ class LongcatFlashMoE(nn.Module):
)
self.router = LongcatFlashTopkRouter(args)
self.sharding_group = None
def __call__(self, hidden_states):
if self.sharding_group is not None:
hidden_states = sum_gradients(self.sharding_group)(hidden_states)
topk_indices, topk_weights = self.router(hidden_states)
@@ -259,21 +235,15 @@ class LongcatFlashMoE(nn.Module):
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
weighted_outputs = regular_outputs * regular_weights[..., None]
final_output = mx.sum(weighted_outputs, axis=-2)
weighted_outputs = regular_outputs * topk_weights[..., None]
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
# Add identity expert contribution if needed
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
identity_weights = mx.where(mask, topk_weights, 0.0)
identity_outputs = hidden_states[..., None, :] * identity_weights[..., None]
weighted_outputs = weighted_outputs + identity_outputs
final_output = mx.sum(weighted_outputs, axis=-2)
return final_output
@@ -344,7 +314,7 @@ class LongcatFlashModel(nn.Module):
if cache is None:
cache = [(None, None)] * self.num_layers
mask = create_attention_mask(h, cache[0][0], return_array=True)
mask = create_attention_mask(h, cache[0][0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, cache=c)
@@ -400,47 +370,6 @@ class Model(nn.Module):
]
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
for l in range(self.args.num_layers):
for i in range(2):
prefix = f"model.layers.{l}.self_attn.{i}"
kv_b_key = f"{prefix}.kv_b_proj.weight"
if kv_b_key in weights:
num_heads = self.args.num_attention_heads
head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim
quantized = f"{prefix}.kv_b_proj.scales" in weights
v = weights.pop(kv_b_key)
if quantized:
dims = self.args.kv_lora_rank
scales = weights.pop(f"{prefix}.kv_b_proj.scales")
biases = weights.pop(f"{prefix}.kv_b_proj.biases")
bits = (v.shape[-1] * 32) // dims
group_size = dims // scales.shape[-1]
v = mx.dequantize(
v, scales, biases, bits=bits, group_size=group_size
)
v = v.reshape(num_heads, head_dim, -1)
wk = mx.contiguous(
v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2)
)
wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :])
if quantized:
wk, wk_s, wk_b = mx.quantize(
wk, bits=bits, group_size=group_size
)
wv, wv_s, wv_b = mx.quantize(
wv, bits=bits, group_size=group_size
)
weights[f"{prefix}.embed_q.scales"] = wk_s
weights[f"{prefix}.embed_q.biases"] = wk_b
weights[f"{prefix}.unembed_out.scales"] = wv_s
weights[f"{prefix}.unembed_out.biases"] = wv_b
weights[f"{prefix}.embed_q.weight"] = wk
weights[f"{prefix}.unembed_out.weight"] = wv
new_weights = {}
for k, v in weights.items():
if k.startswith("model.mtp"):
@@ -450,44 +379,3 @@ class Model(nn.Module):
def make_cache(self):
return [CacheList(KVCache(), KVCache()) for _ in self.model.layers]
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
rank = group.rank()
for layer in self.model.layers:
for attn in layer.self_attn:
if attn.q_lora_rank is None:
attn.q_proj = shard_linear(
attn.q_proj, "all-to-sharded", group=group
)
else:
attn.q_b_proj = shard_linear(
attn.q_b_proj, "all-to-sharded", group=group
)
attn.o_proj = shard_linear(attn.o_proj, "sharded-to-all", group=group)
attn.num_attention_heads //= N
num_heads = attn.num_attention_heads
sh = rank * num_heads
eh = sh + num_heads
def shard_heads(w):
return w[sh:eh]
attn.embed_q.apply(shard_heads)
attn.unembed_out.apply(shard_heads)
for mlp in layer.mlps:
mlp.gate_proj = shard_linear(
mlp.gate_proj, "all-to-sharded", group=group
)
mlp.up_proj = shard_linear(mlp.up_proj, "all-to-sharded", group=group)
mlp.down_proj = shard_linear(
mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.sharding_group = group
shard_inplace(layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group)
shard_inplace(layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group)
shard_inplace(layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group)
-214
View File
@@ -1,214 +0,0 @@
# 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)
+4 -5
View File
@@ -6,9 +6,8 @@ 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 ArraysCache
from .cache import MambaCache
@dataclass
@@ -140,7 +139,7 @@ class MambaBlock(nn.Module):
y_t, current_state = self.ssm_step(x[:, t], A, current_state)
y.append(y_t)
y = mx.stack(y, axis=1)
z = self.out_proj(swiglu(z, y))
z = self.out_proj(nn.silu(z) * y)
return z, (new_conv_cache, current_state)
def __call__(self, x, cache):
@@ -153,7 +152,7 @@ class MambaBlock(nn.Module):
x, conv_cache, state_cache
)
if isinstance(cache, ArraysCache):
if isinstance(cache, MambaCache):
cache[0] = new_conv_cache
cache[1] = new_state_cache
@@ -208,7 +207,7 @@ class Model(nn.Module):
return logits
def make_cache(self):
return [ArraysCache(size=2) for _ in range(len(self.layers))]
return [MambaCache() for _ in range(len(self.layers))]
@property
def layers(self):
+20 -39
View File
@@ -7,9 +7,8 @@ from typing import Optional, Tuple, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_ssm_mask
from .cache import ArraysCache
from .cache import MambaCache
from .ssm import ssm_update
@@ -49,7 +48,7 @@ class MambaRMSNormGated(nn.Module):
def __call__(self, hidden_states: mx.array, gate: mx.array = None) -> mx.array:
if gate is not None:
hidden_states = swiglu(gate, hidden_states)
hidden_states = hidden_states * nn.silu(gate)
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
@@ -94,15 +93,9 @@ class Mamba2Block(nn.Module):
self.intermediate_size, self.hidden_size, bias=args.use_bias
)
def _conv(
self,
conv_input: mx.array,
cache: Optional[ArraysCache],
mask: Optional[mx.array],
def _apply_conv(
self, conv_input: mx.array, cache: Optional[MambaCache] = None
) -> 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(
@@ -112,14 +105,7 @@ class Mamba2Block(nn.Module):
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:, :]
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :, :]
else:
padded_input = mx.pad(
conv_input, [(0, 0), (self.conv_kernel_size - 1, 0), (0, 0)]
@@ -134,8 +120,8 @@ class Mamba2Block(nn.Module):
B: mx.array,
C: mx.array,
dt: mx.array,
cache: Optional[ArraysCache],
mask: Optional[mx.array],
state: Optional[mx.array] = None,
mask: Optional[mx.array] = None,
) -> mx.array:
batch_size, seq_len, _ = hidden_states.shape
hidden_states = hidden_states.reshape(
@@ -143,11 +129,6 @@ class Mamba2Block(nn.Module):
)
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
if cache:
state = cache[1]
lengths = cache.lengths
else:
state, lengths = None, None
y, state = ssm_update(
hidden_states,
self.A_log,
@@ -159,17 +140,14 @@ class Mamba2Block(nn.Module):
state,
self.time_step_limit,
mask,
lengths,
)
if cache:
cache[1] = state
return y.reshape(batch_size, seq_len, self.intermediate_size)
return y.reshape(batch_size, seq_len, self.intermediate_size), state
def __call__(
self,
hidden_states: mx.array,
mask: Optional[mx.array],
cache: Optional[ArraysCache] = None,
cache: Optional[MambaCache] = None,
) -> mx.array:
projected = self.in_proj(hidden_states)
gate, conv_input, dt = mx.split(
@@ -177,7 +155,9 @@ class Mamba2Block(nn.Module):
[self.intermediate_size, self.intermediate_size + self.conv_dim],
axis=-1,
)
conv_output = self._conv(conv_input, cache, mask)
if mask is not None:
conv_input = mx.where(mask[..., None], conv_input, 0)
conv_output = self._apply_conv(conv_input, cache)
hidden_states, B, C = mx.split(
conv_output,
[
@@ -186,9 +166,10 @@ class Mamba2Block(nn.Module):
],
axis=-1,
)
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
state = cache[1] if cache else None
y, state = self._ssm(hidden_states, B, C, dt, state, mask=mask)
if cache:
cache.advance(y.shape[1])
cache[1] = state
y = self.norm(y, gate)
return self.out_proj(y)
@@ -200,7 +181,7 @@ class ResidualBlock(nn.Module):
self.norm = nn.RMSNorm(args.hidden_size)
def __call__(
self, x: mx.array, mask: Optional[mx.array], cache: Optional[ArraysCache] = None
self, x: mx.array, mask: Optional[mx.array], cache: Optional[MambaCache] = None
) -> mx.array:
output = self.mixer(self.norm(x), mask, cache)
return output + x
@@ -215,7 +196,7 @@ class Mamba2(nn.Module):
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.layer_norm_epsilon)
def __call__(
self, x: mx.array, cache: Optional[list[ArraysCache]] = None
self, x: mx.array, cache: Optional[list[MambaCache]] = None
) -> mx.array:
hidden = self.embeddings(x)
@@ -240,7 +221,7 @@ class Model(nn.Module):
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self, inputs: mx.array, cache: Optional[list[ArraysCache]] = None
self, inputs: mx.array, cache: Optional[list[MambaCache]] = None
) -> mx.array:
hidden = self.backbone(inputs, cache)
@@ -250,8 +231,8 @@ class Model(nn.Module):
logits = self.lm_head(hidden)
return logits
def make_cache(self, batch_size: int = 1) -> list[ArraysCache]:
return [ArraysCache(size=2) for _ in range(self.args.num_hidden_layers)]
def make_cache(self, batch_size: int = 1) -> list[MambaCache]:
return [MambaCache() for _ in range(self.args.num_hidden_layers)]
@property
def layers(self):
+1 -2
View File
@@ -6,7 +6,6 @@ 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 +90,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(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class TransformerBlock(nn.Module):
-384
View File
@@ -1,384 +0,0 @@
# Copyright © 2024 Apple Inc.
import math
from dataclasses import dataclass
from functools import partial
from typing import Any, Dict, List, Optional
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .switch_layers import SwitchGLU
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
num_experts_per_tok: int
hybrid_layer_pattern: List[int]
moe_layer_freq: List[int]
add_swa_attention_sink_bias: bool
add_full_attention_sink_bias: bool
sliding_window_size: int
vocab_size: int
hidden_size: int
intermediate_size: int
moe_intermediate_size: int
num_hidden_layers: int
num_attention_heads: int
num_key_value_heads: int
n_shared_experts: Optional[int]
n_routed_experts: Optional[int]
routed_scaling_factor: Optional[float]
topk_method: str
scoring_func: str
norm_topk_prob: bool
n_group: int
topk_group: int
max_position_embeddings: int
layernorm_epsilon: float
rope_theta: float
swa_rope_theta: float
swa_num_attention_heads: int
swa_num_key_value_heads: int
head_dim: int
v_head_dim: int
swa_head_dim: int
swa_v_head_dim: int
partial_rotary_factor: int
class Attention(nn.Module):
def __init__(self, args: ModelArgs, is_sliding_window: bool):
super().__init__()
dim = args.hidden_size
self.is_sliding_window = is_sliding_window
if self.is_sliding_window:
self.n_heads = n_heads = args.swa_num_attention_heads
self.n_kv_heads = n_kv_heads = args.swa_num_key_value_heads
self.has_sinks = args.add_swa_attention_sink_bias
head_dim = args.swa_head_dim
v_head_dim = args.swa_v_head_dim
rope_theta = args.swa_rope_theta
else:
self.n_heads = n_heads = args.num_attention_heads
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
self.has_sinks = args.add_full_attention_sink_bias
head_dim = args.head_dim
v_head_dim = args.v_head_dim
rope_theta = args.rope_theta
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
self.v_proj = nn.Linear(dim, n_kv_heads * v_head_dim, bias=False)
self.o_proj = nn.Linear(n_heads * v_head_dim, dim, bias=False)
if self.has_sinks:
self.attention_sink_bias = mx.ones((self.n_heads,))
else:
self.attention_sink_bias = None
self.rope = nn.RoPE(
int(args.partial_rotary_factor * head_dim),
traditional=False,
base=rope_theta,
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
if cache is not None:
queries = self.rope(queries, offset=cache.offset)
keys = self.rope(keys, offset=cache.offset)
keys, values = cache.update_and_fetch(keys, values)
else:
queries = self.rope(queries)
keys = self.rope(keys)
output = scaled_dot_product_attention(
queries,
keys,
values,
cache=cache,
scale=self.scale,
mask=mask,
sinks=self.attention_sink_bias,
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class MLP(nn.Module):
def __init__(
self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None
):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
self.intermediate_size = (
config.intermediate_size if intermediate_size is None else intermediate_size
)
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
def __call__(self, x):
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return down_proj
@mx.compile
def group_expert_select(
gates,
e_score_correction_bias,
top_k,
n_group,
topk_group,
routed_scaling_factor,
norm_topk_prob,
):
scores = mx.sigmoid(gates.astype(mx.float32))
orig_scores = scores
scores = scores + e_score_correction_bias
if n_group > 1:
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
k = n_group - topk_group
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
scores = mx.put_along_axis(
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
)
scores = mx.flatten(scores, -2, -1)
k = top_k
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
if top_k > 1 and norm_topk_prob:
denominator = scores.sum(axis=-1, keepdims=True)
scores = scores / (denominator + 1e-20)
scores = scores * routed_scaling_factor
return inds, scores
class MoEGate(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.config = config
self.top_k = config.num_experts_per_tok
self.norm_topk_prob = config.norm_topk_prob
self.n_routed_experts = config.n_routed_experts
self.routed_scaling_factor = (
config.routed_scaling_factor
if config.routed_scaling_factor is not None
else 1.0
)
self.n_group = config.n_group
self.topk_group = config.topk_group
self.weight = mx.zeros((self.n_routed_experts, config.hidden_size))
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
assert config.topk_method == "noaux_tc", "Unsupported topk method."
def __call__(self, x):
return group_expert_select(
x @ self.weight.T,
self.e_score_correction_bias,
self.top_k,
self.n_group,
self.topk_group,
self.routed_scaling_factor,
self.norm_topk_prob,
)
class MoE(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.config = config
self.num_experts_per_tok = config.num_experts_per_tok
self.switch_mlp = SwitchGLU(
config.hidden_size,
config.moe_intermediate_size,
config.n_routed_experts,
)
self.gate = MoEGate(config)
if config.n_shared_experts is not None:
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
self.shared_experts = MLP(
config=config, intermediate_size=intermediate_size
)
def __call__(self, x):
inds, scores = self.gate(x)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
if self.config.n_shared_experts is not None:
y = y + self.shared_experts(x)
return y
class DecoderLayer(nn.Module):
def __init__(self, config: ModelArgs, is_moe, is_sliding_window):
super().__init__()
self.self_attn = Attention(config, is_sliding_window)
self.mlp = MoE(config) if is_moe else MLP(config)
self.is_sliding_window = is_sliding_window
self.input_layernorm = nn.RMSNorm(
config.hidden_size, eps=config.layernorm_epsilon
)
self.post_attention_layernorm = nn.RMSNorm(
config.hidden_size, eps=config.layernorm_epsilon
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), mask, cache)
h = x + r
r = self.mlp(self.post_attention_layernorm(h))
return h + r
class LanguageModel(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
self.layers = [
DecoderLayer(
config,
is_moe=config.moe_layer_freq[idx] == 1,
is_sliding_window=config.hybrid_layer_pattern[idx] == 1,
)
for idx in range(config.num_hidden_layers)
]
self.norm = nn.RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
self.swa_idx = config.hybrid_layer_pattern.index(1)
self.ga_idx = config.hybrid_layer_pattern.index(0)
self.sliding_window_size = config.sliding_window_size
def __call__(
self,
x: mx.array,
cache: Optional[Any] = None,
) -> mx.array:
h = self.embed_tokens(x)
if cache is None:
cache = [None] * len(self.layers)
full_mask = create_attention_mask(x, cache[self.ga_idx])
swa_mask = create_attention_mask(
x, cache[self.swa_idx], window_size=self.sliding_window_size
)
for l, c in zip(self.layers, cache):
mask = swa_mask if l.is_sliding_window else full_mask
h = l(h, mask, cache=c)
return self.norm(h)
class Model(nn.Module):
def __init__(self, config: ModelArgs):
super().__init__()
self.args = config
self.model_type = config.model_type
self.model = LanguageModel(config)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache: Optional[Any] = None,
):
out = self.model(inputs, cache)
return self.lm_head(out)
def sanitize(self, weights):
def dequant(weight, scale_inv):
dtype = mx.bfloat16
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
bs = 128 # block size
m, n = weight.shape
pad_bottom = bs * scale_inv.shape[0] - m
pad_side = bs * scale_inv.shape[1] - n
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
weight = weight.reshape(
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
)
weight = (weight * scale_inv[:, None, :, None]).reshape(
m + pad_bottom, n + pad_side
)
return weight[:m, :n].astype(dtype)
# Dequantize fp8
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
scale_inv = v
wk = k.replace("_scale_inv", "")
weight = weights[wk]
weight = dequant(weight, scale_inv)
new_weights[wk] = weight
elif k not in new_weights:
new_weights[k] = v
weights = new_weights
# Stack experts
for l in range(self.args.num_hidden_layers):
prefix = f"model.layers.{l}"
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
for k in ["weight", "scales", "biases"]:
if f"{prefix}.mlp.experts.0.{m}.{k}" in weights:
to_join = [
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
for e in range(self.args.n_routed_experts)
]
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
# Remove multi-token prediction layer
return {k: v for k, v in weights.items() if not k.startswith("model.mtp")}
@property
def layers(self):
return self.model.layers
@property
def cast_predicate(self):
def predicate(k):
return "e_score_correction_bias" not in k
return predicate
def make_cache(self):
caches = []
for l in self.layers:
if l.is_sliding_window:
caches.append(RotatingKVCache(max_size=self.args.sliding_window_size))
else:
caches.append(KVCache())
return caches
+1 -2
View File
@@ -6,7 +6,6 @@ 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
@@ -39,7 +38,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
def __call__(self, x):
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Attention(nn.Module):
+1 -2
View File
@@ -6,7 +6,6 @@ from typing import Any, Dict, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .rope_utils import SuScaledRoPE
@@ -157,7 +156,7 @@ class MLP(nn.Module):
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
def __call__(self, x):
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class DecoderLayer(nn.Module):
-394
View File
@@ -1,394 +0,0 @@
# Copyright © 2025 Apple Inc.
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, List, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .switch_layers import SwitchGLU
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
hidden_size: int
intermediate_size: int
num_attention_heads: int
num_key_value_heads: int
max_position_embeddings: int
num_experts_per_tok: int
num_local_experts: int
shared_intermediate_size: int
num_hidden_layers: int
rms_norm_eps: float
rope_theta: float
rotary_dim: int
vocab_size: int
tie_word_embeddings: bool = False
scoring_func: str = "sigmoid"
head_dim: Optional[int] = None
use_qk_norm: bool = True
@lru_cache
def sharded_rms_norm(group):
@mx.compile
def _cast_square_sum(x):
return x.astype(mx.float32).square().sum(-1, keepdims=True)
@mx.compile
def _normalize(x, norm2, w, eps):
norm2 = mx.distributed.all_sum(norm2, group=group)
norm = mx.rsqrt(norm2 / (x.shape[-1] * group.size()) + eps)
return (x.astype(mx.float32) * norm * w).astype(x.dtype)
# Split the compile so that x upcasting doesn't break the compile and we
# have 2 kernels generated 1 for f(x) = square(upcast(x)) and another
# g(x) = downcast(upcast(x) * norm * w)
def _inner_sharded_rms_norm(x, w, eps):
return _normalize(x, _cast_square_sum(x), w, eps)
return _inner_sharded_rms_norm
class ShardedRMSNorm(nn.Module):
def __init__(
self, dims: int, eps: float = 1e-5, group: Optional[mx.distributed.Group] = None
):
super().__init__()
group = group or mx.distributed.init()
self.weight = mx.ones((dims // group.size(),))
self.group = group
self.eps = eps
def _extra_repr(self):
return f"{self.weight.shape[0] * self.group.size()}, eps={self.eps}"
def __call__(self, x):
return sharded_rms_norm(self.group)(x, self["weight"], self.eps)
@classmethod
def from_rms_norm(
cls, norm_module, *, group: Optional[mx.distributed.Group] = None
):
sn = cls(norm_module.weight.shape[0], norm_module.eps, group=group)
sn.weight = mx.contiguous(
mx.split(norm_module.weight, group.size(), axis=-1)[group.rank()]
)
return sn
class MiniMaxAttention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.hidden_dim = hidden_size = args.hidden_size
self.num_attention_heads = args.num_attention_heads
self.num_key_value_heads = args.num_key_value_heads
self.head_dim = head_dim = (
args.head_dim or hidden_size // args.num_attention_heads
)
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(
args.hidden_size, self.num_attention_heads * head_dim, bias=False
)
self.k_proj = nn.Linear(
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
)
self.v_proj = nn.Linear(
args.hidden_size, self.num_key_value_heads * head_dim, bias=False
)
self.o_proj = nn.Linear(
self.num_attention_heads * head_dim, args.hidden_size, bias=False
)
self.use_qk_norm = args.use_qk_norm if hasattr(args, "use_qk_norm") else False
if self.use_qk_norm:
self.q_norm = nn.RMSNorm(
head_dim * self.num_attention_heads, eps=args.rms_norm_eps
)
self.k_norm = nn.RMSNorm(
head_dim * self.num_key_value_heads, eps=args.rms_norm_eps
)
self.rope = nn.RoPE(args.rotary_dim, traditional=False, base=args.rope_theta)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
if self.use_qk_norm:
queries = self.q_norm(queries)
keys = self.k_norm(keys)
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
0, 2, 1, 3
)
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
0, 2, 1, 3
)
if cache is not None:
queries = self.rope(queries, offset=cache.offset)
keys = self.rope(keys, offset=cache.offset)
keys, values = cache.update_and_fetch(keys, values)
else:
queries = self.rope(queries)
keys = self.rope(keys)
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class MiniMaxSparseMoeBlock(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.num_experts_per_tok = args.num_experts_per_tok
self.gate = nn.Linear(args.hidden_size, args.num_local_experts, bias=False)
self.switch_mlp = SwitchGLU(
args.hidden_size, args.intermediate_size, args.num_local_experts
)
self.e_score_correction_bias = mx.zeros((args.num_local_experts,))
self.sharding_group = None
def __call__(self, x: mx.array) -> mx.array:
if self.sharding_group is not None:
x = sum_gradients(self.sharding_group)(x)
gates = self.gate(x.astype(mx.float32))
scores = mx.sigmoid(gates)
orig_scores = scores
scores = scores + self.e_score_correction_bias
k = self.num_experts_per_tok
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
scores = scores / (mx.sum(scores, axis=-1, keepdims=True) + 1e-20)
scores = scores.astype(x.dtype)
y = self.switch_mlp(x, inds)
y = (y * scores[..., None]).sum(axis=-2)
if self.sharding_group is not None:
y = mx.distributed.all_sum(y, group=self.sharding_group)
return y
class MiniMaxDecoderLayer(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.self_attn = MiniMaxAttention(args)
self.block_sparse_moe = MiniMaxSparseMoeBlock(args)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
def __call__(
self,
x: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = x + self.self_attn(self.input_layernorm(x), mask, cache)
r = r + self.block_sparse_moe(self.post_attention_layernorm(r))
return r
class MiniMaxModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
MiniMaxDecoderLayer(args=args) for _ in range(args.num_hidden_layers)
]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
def __call__(
self,
inputs: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
h = self.embed_tokens(inputs)
if cache is None:
cache = [None] * len(self.layers)
mask = create_attention_mask(h, cache[0])
for layer, c in zip(self.layers, cache):
h = layer(h, mask, c)
return self.norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = MiniMaxModel(args)
if not args.tie_word_embeddings:
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
):
out = self.model(inputs=inputs, mask=mask, cache=cache)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
out = self.lm_head(out)
return out
def sanitize(self, weights):
"""Dequantize FP8 weights and restructure MoE experts."""
def dequant(weight, scale_inv):
dtype = mx.bfloat16
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
bs = 128 # block size
m, n = weight.shape
pad_bottom = (-m) % bs
pad_side = (-n) % bs
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
weight = weight.reshape(
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
)
weight = (weight * scale_inv[:, None, :, None]).reshape(
m + pad_bottom, n + pad_side
)
return weight[:m, :n].astype(dtype)
# Dequantize
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
scale_inv = v
wk = k.replace("_scale_inv", "")
weight = weights[wk]
weight = dequant(weight, scale_inv)
new_weights[wk] = weight
elif k not in new_weights:
new_weights[k] = v
weights = new_weights
# Step 2: Handle MoE expert weights restructuring
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
return weights
for l in range(self.args.num_hidden_layers):
prefix = f"model.layers.{l}"
mapping = {"w1": "gate_proj", "w2": "down_proj", "w3": "up_proj"}
for orig_name, new_name in mapping.items():
if f"{prefix}.block_sparse_moe.experts.0.{orig_name}.weight" in weights:
to_join = [
weights.pop(
f"{prefix}.block_sparse_moe.experts.{e}.{orig_name}.weight"
)
for e in range(self.args.num_local_experts)
]
weights[
f"{prefix}.block_sparse_moe.switch_mlp.{new_name}.weight"
] = mx.stack(to_join)
return weights
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
rank = group.rank()
for layer in self.model.layers:
# Shard the self attention
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
layer.self_attn.k_proj = shard_linear(
layer.self_attn.k_proj, "all-to-sharded", group=group
)
layer.self_attn.v_proj = shard_linear(
layer.self_attn.v_proj, "all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
if layer.self_attn.use_qk_norm:
layer.self_attn.q_norm = ShardedRMSNorm.from_rms_norm(
layer.self_attn.q_norm, group=group
)
layer.self_attn.k_norm = ShardedRMSNorm.from_rms_norm(
layer.self_attn.k_norm, group=group
)
layer.self_attn.num_attention_heads //= N
layer.self_attn.num_key_value_heads //= N
# Shard the MLP
shard_inplace(
layer.block_sparse_moe.switch_mlp.gate_proj,
"all-to-sharded",
group=group,
)
shard_inplace(
layer.block_sparse_moe.switch_mlp.down_proj,
"sharded-to-all",
group=group,
)
shard_inplace(
layer.block_sparse_moe.switch_mlp.up_proj,
"all-to-sharded",
group=group,
)
layer.block_sparse_moe.sharding_group = group
@property
def layers(self):
return self.model.layers
@property
def cast_predicate(self):
def predicate(k):
return "e_score_correction_bias" not in k
return predicate
@property
def quant_predicate(self):
def predicate(path, _):
if path.endswith("block_sparse_moe.gate"):
return {"group_size": 64, "bits": 8}
return True
return predicate
-333
View File
@@ -1,333 +0,0 @@
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Union
import mlx.core as mx
import mlx.nn as nn
from mlx.nn.layers.distributed import shard_linear
from .activations import swiglu
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
from .cache import KVCache, RotatingKVCache
from .pipeline import PipelineMixin
from .rope_utils import initialize_rope
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
hidden_size: int
num_hidden_layers: int
intermediate_size: int
num_attention_heads: int
rms_norm_eps: float
vocab_size: int
head_dim: Optional[int] = None
max_position_embeddings: Optional[int] = None
num_key_value_heads: Optional[int] = None
rope_parameters: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = True
layer_types: Optional[List[str]] = None
sliding_window: Optional[int] = None
def __post_init__(self):
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.layer_types is None:
self.layer_types = ["full_attention"] * self.num_hidden_layers
def _get_llama_4_attn_scale(size, offset, beta: float, max_position_embeddings: int):
if isinstance(offset, mx.array) and offset.ndim > 0:
offset = offset[:, None]
scaling = 1 + beta * mx.log(
1 + mx.floor((mx.arange(size) + offset) / max_position_embeddings)
)
if scaling.ndim == 2:
return scaling[:, None, :, None]
else:
return scaling[:, None]
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_size
self.n_heads = n_heads = args.num_attention_heads
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
self.head_dim = head_dim = args.head_dim or args.hidden_size // n_heads
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
self.rope = initialize_rope(
self.head_dim,
args.rope_parameters["rope_theta"],
False,
args.rope_parameters,
args.max_position_embeddings,
)
def __call__(
self,
x: mx.array,
attn_scale: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
B, L, D = x.shape
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
# Prepare the queries, keys and values for the attention computation
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
offset = 0
if cache is not None:
offset = cache.offset
queries = self.rope(queries, offset=offset)
keys = self.rope(keys, offset=offset)
keys, values = cache.update_and_fetch(keys, values)
else:
queries = self.rope(queries)
keys = self.rope(keys)
queries = queries * attn_scale
output = scaled_dot_product_attention(
queries, keys, values, cache=cache, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
return self.o_proj(output)
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_size
hidden_dim = args.intermediate_size
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
def __call__(self, x) -> mx.array:
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
class TransformerBlock(nn.Module):
def __init__(self, args: ModelArgs, use_sliding: bool = False):
super().__init__()
self.num_attention_heads = args.num_attention_heads
self.hidden_size = args.hidden_size
self.use_sliding = use_sliding
self.self_attn = Attention(args)
self.mlp = MLP(args)
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.post_attention_layernorm = nn.RMSNorm(
args.hidden_size, eps=args.rms_norm_eps
)
self.args = args
def __call__(
self,
x: mx.array,
attn_scale: mx.array,
mask: Optional[mx.array] = None,
cache: Optional[Any] = None,
) -> mx.array:
r = self.self_attn(self.input_layernorm(x), attn_scale, mask, cache)
h = x + r
r = self.mlp(self.post_attention_layernorm(h))
out = h + r
return out
class LanguageModel(PipelineMixin, nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.vocab_size = args.vocab_size
self.num_hidden_layers = args.num_hidden_layers
self.layer_types = args.layer_types
self.sliding_window = args.sliding_window
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
for layer_type in self.layer_types
]
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
self.fa_idx = self.layer_types.index("full_attention")
self.swa_idx = None
for e, l in enumerate(self.layers):
if l.use_sliding:
self.swa_idx = e
break
def pipeline(self, group):
super().pipeline(group)
self.fa_idx = None
self.swa_idx = None
for e, l in enumerate(self.pipeline_layers):
if self.swa_idx is None and l.use_sliding:
self.swa_idx = e
elif self.fa_idx is None and not l.use_sliding:
self.fa_idx = e
if self.fa_idx is not None and self.swa_idx is not None:
break
def __call__(
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
if input_embeddings is not None:
h = input_embeddings
else:
h = self.embed_tokens(inputs)
pipeline_rank = self.pipeline_rank
pipeline_size = self.pipeline_size
if cache is None:
cache = [None] * len(self.pipeline_layers)
offset = 0
else:
offset = cache[0].offset
swa_mask = fa_mask = None
if self.fa_idx is not None:
fa_mask = create_attention_mask(h, cache[self.fa_idx])
if self.swa_idx is not None:
swa_mask = create_attention_mask(
h, cache[self.swa_idx], window_size=self.sliding_window
)
attn_scale = _get_llama_4_attn_scale(
inputs.shape[1],
offset,
self.args.rope_parameters["llama_4_scaling_beta"],
self.args.rope_parameters["original_max_position_embeddings"],
).astype(h.dtype)
# Receive from the previous process in the pipeline
if pipeline_rank < pipeline_size - 1:
h = mx.distributed.recv_like(h, (pipeline_rank + 1))
for l, c in zip(self.pipeline_layers, cache):
mask = swa_mask if l.use_sliding else fa_mask
h = l(h, attn_scale, mask, cache=c)
# Send to the next process in the pipeline
if pipeline_rank != 0:
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
if cache[-1] is not None:
cache[-1].keys = mx.depends(cache[-1].keys, h)
# Broadcast h while keeping it in the graph
if pipeline_size > 1:
h = mx.distributed.all_gather(h)[: h.shape[0]]
return self.norm(h)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = LanguageModel(args)
if not args.tie_word_embeddings:
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
def __call__(
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
):
out = self.model(inputs, cache, input_embeddings)
if self.args.tie_word_embeddings:
out = self.model.embed_tokens.as_linear(out)
else:
out = self.lm_head(out)
return out
def sanitize(self, weights):
# Remove unused precomputed rotary freqs
weights = {
k: v for k, v in weights.items() if "self_attn.rotary_emb.inv_freq" not in k
}
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
new_weights = {}
for k, v in weights.items():
if "weight_scale_inv" in k:
scale_inv = v
wk = k.replace("_scale_inv", "")
weight = weights[wk]
new_weights[wk] = weight * scale_inv
elif "activation_scale" in k:
continue
elif k not in new_weights:
new_weights[k] = v
weights = new_weights
return weights
def shard(self, group: Optional[mx.distributed.Group] = None):
group = group or mx.distributed.init()
N = group.size()
for layer in self.model.layers:
# Shard the self attention
layer.self_attn.q_proj = shard_linear(
layer.self_attn.q_proj, "all-to-sharded", group=group
)
layer.self_attn.k_proj = shard_linear(
layer.self_attn.k_proj, "all-to-sharded", group=group
)
layer.self_attn.v_proj = shard_linear(
layer.self_attn.v_proj, "all-to-sharded", group=group
)
layer.self_attn.o_proj = shard_linear(
layer.self_attn.o_proj, "sharded-to-all", group=group
)
layer.self_attn.n_heads //= N
layer.self_attn.n_kv_heads //= N
# Shard the MLP
layer.mlp.gate_proj = shard_linear(
layer.mlp.gate_proj, "all-to-sharded", group=group
)
layer.mlp.down_proj = shard_linear(
layer.mlp.down_proj, "sharded-to-all", group=group
)
layer.mlp.up_proj = shard_linear(
layer.mlp.up_proj, "all-to-sharded", group=group
)
@property
def layers(self):
return self.model.pipeline_layers
def make_cache(self):
return [
(
RotatingKVCache(max_size=self.model.sliding_window)
if layer.use_sliding
else KVCache()
)
for layer in self.layers
]
+3 -13
View File
@@ -7,7 +7,7 @@ import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten, tree_unflatten
from . import llama, ministral3
from . import llama
from .base import BaseModelArgs
@@ -17,8 +17,7 @@ class ModelArgs(BaseModelArgs):
text_config: dict
def __post_init__(self):
if "tie_word_embeddings" not in self.text_config:
self.text_config["tie_word_embeddings"] = False
self.text_config["tie_word_embeddings"] = False
class Model(nn.Module):
@@ -26,14 +25,7 @@ class Model(nn.Module):
super().__init__()
self.args = args
self.model_type = args.model_type
if args.text_config.get("model_type") == "ministral3":
self.language_model = ministral3.Model(
ministral3.ModelArgs.from_dict(args.text_config)
)
else:
self.language_model = llama.Model(
llama.ModelArgs.from_dict(args.text_config)
)
self.language_model = llama.Model(llama.ModelArgs.from_dict(args.text_config))
def __call__(
self,
@@ -49,8 +41,6 @@ class Model(nn.Module):
weights = tree_unflatten(list(weights.items()))
weights.pop("vision_tower", None)
weights.pop("multi_modal_projector", None)
lm_weights = dict(tree_flatten(weights["language_model"]))
weights["language_model"] = self.language_model.sanitize(lm_weights)
return dict(tree_flatten(weights))
@property
+8 -20
View File
@@ -1,4 +1,4 @@
# Copyright © 2026 Apple Inc.
# Copyright © 2023-2024 Apple Inc.
from dataclasses import dataclass
from typing import Any, Dict, Optional, Union
@@ -25,7 +25,6 @@ class ModelArgs(BaseModelArgs):
rope_theta: float = 1e6
rope_traditional: bool = False
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
tie_word_embeddings: bool = False
def __post_init__(self):
if self.num_key_value_heads is None:
@@ -163,12 +162,8 @@ class MixtralModel(nn.Module):
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
) -> mx.array:
if input_embeddings is not None:
h = input_embeddings
else:
h = self.embed_tokens(inputs)
):
h = self.embed_tokens(inputs)
if cache is None:
cache = [None] * len(self.layers)
@@ -184,27 +179,20 @@ class MixtralModel(nn.Module):
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.model = MixtralModel(args)
if not args.tie_word_embeddings:
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
self.args = args
def __call__(
self,
inputs: mx.array,
cache=None,
input_embeddings: Optional[mx.array] = None,
) -> mx.array:
out = self.model(inputs, cache, input_embeddings)
if self.args.tie_word_embeddings:
return self.model.embed_tokens.as_linear(out)
else:
return self.lm_head(out)
):
out = self.model(inputs, cache)
return self.lm_head(out)
def sanitize(self, weights):
if self.args.tie_word_embeddings:
weights.pop("lm_head.weight", None)
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
return weights
for l in range(self.args.num_hidden_layers):

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