Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02228601cd | |||
| 8daabcc7c1 | |||
| cd7d9a536e | |||
| 25246632cf | |||
| 6651d2e0bf | |||
| 43dcf2f0c0 | |||
| 769069d66b | |||
| 5261ab85ee | |||
| c27c94a0ff | |||
| fd80ac89fb | |||
| edbf61dd8b | |||
| c2a716c871 | |||
| 63c9873617 | |||
| 7585c142a6 | |||
| 44d12e5d6f | |||
| 7a86c1289e | |||
| a20eefd7c2 | |||
| 3eb6ecf2b6 | |||
| 39a96ab18b | |||
| 43082feafa | |||
| 5cce1495e0 | |||
| 509f5aef89 | |||
| 0f76343ea4 | |||
| 298b67c755 | |||
| 94497d5255 | |||
| 4c80c68ea6 | |||
| ac8ae2c05a | |||
| 7a4d137df6 | |||
| 90db1e6266 | |||
| d3dc2e3f33 | |||
| 7423bf6752 | |||
| 5dec49a12f | |||
| 3727e01cd7 | |||
| 09579644ac | |||
| 0081085a91 | |||
| fed582eede | |||
| 7973b8cfe8 | |||
| 7096618d50 | |||
| 1e0c0f3985 | |||
| 68f18bae14 | |||
| f5ae09a807 | |||
| 08c8c0a5ea | |||
| a9311cca23 | |||
| 9fe5f43abf | |||
| 1b2d11b5c7 | |||
| 657a66c5c4 | |||
| 595fb4bdbf | |||
| 79a0721c9a | |||
| cc3264c22e | |||
| a227a9e9f3 | |||
| cd9ca9f068 | |||
| 7744d0f40b | |||
| f3ed856610 | |||
| ede65a1484 | |||
| 3d3e0751a3 | |||
| 085e36e6ab | |||
| eea2e5f5de | |||
| cb763947ee | |||
| b343a0556f | |||
| 82dfd39ef2 | |||
| 84996808a2 | |||
| 99f8fd6cc8 |
@@ -38,4 +38,6 @@ jobs:
|
||||
- name: Run tests
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
python -m xmlrunner discover -v tests -o test-results/
|
||||
curl -o test_data.zip -L https://github.com/ml-explore/mlx-lm/releases/download/test_data/test_data.zip
|
||||
unzip test_data.zip
|
||||
HF_HOME="." python -m xmlrunner discover -v tests -o test-results/
|
||||
|
||||
@@ -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 --hf-path mistralai/Mistral-7B-Instruct-v0.3 -q
|
||||
mlx_lm.convert --model 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 \
|
||||
--hf-path mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
--model mistralai/Mistral-7B-Instruct-v0.3 \
|
||||
-q \
|
||||
--upload-repo mlx-community/my-4bit-mistral
|
||||
```
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
Spin up the local server:
|
||||
|
||||
mlx_lm.server
|
||||
|
||||
Then run the benchmark:
|
||||
|
||||
python server_benchmark.py --concurrency 4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from itertools import cycle
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
# Default prompts if no file is provided
|
||||
DEFAULT_PROMPTS = [
|
||||
"Explain quantum computing in simple terms.",
|
||||
"What are the main differences between Python and JavaScript?",
|
||||
"Describe the process of photosynthesis in plants.",
|
||||
"How does a neural network learn from data?",
|
||||
"What is the significance of the Turing test in AI?",
|
||||
"Explain the concept of blockchain technology.",
|
||||
"What causes seasons on Earth?",
|
||||
"How do vaccines work in the human body?",
|
||||
"Describe the water cycle and its importance.",
|
||||
"What is the theory of relativity proposed by Einstein?",
|
||||
"How do electric cars help reduce carbon emissions?",
|
||||
"What are the key features of a market economy?",
|
||||
"Explain how DNA replication works in cells.",
|
||||
"What is machine learning and its real-world applications?",
|
||||
"Describe the structure and function of the human heart.",
|
||||
]
|
||||
|
||||
|
||||
def tokens_per_second(tokens):
|
||||
start = math.floor(tokens[0])
|
||||
stop = math.ceil(tokens[-1])
|
||||
n_bins = int(stop - start) * 10
|
||||
bins = [0] * n_bins
|
||||
for t in tokens:
|
||||
bins[int(n_bins * (t - start) / (stop - start))] += 1
|
||||
|
||||
result = []
|
||||
|
||||
ms = 0
|
||||
cnt = 0
|
||||
for i, b in enumerate(bins):
|
||||
ms += b
|
||||
if cnt == 10:
|
||||
ms -= bins[i - 10]
|
||||
else:
|
||||
cnt += 1
|
||||
|
||||
result.append(10 * ms / cnt)
|
||||
|
||||
times = [start]
|
||||
while times[-1] < stop:
|
||||
times.append(times[-1] + 0.1)
|
||||
|
||||
return times, result
|
||||
|
||||
|
||||
def plot_generation(times, tokens_per_sec, start=None, interval=1.0, width=50):
|
||||
c = "█"
|
||||
start = start or times[0]
|
||||
stop = times[-1]
|
||||
|
||||
bar_times = [start]
|
||||
while bar_times[-1] < stop:
|
||||
bar_times.append(bar_times[-1] + interval)
|
||||
|
||||
bar_values = [[] for _ in bar_times]
|
||||
bar_idx = 0
|
||||
|
||||
for t, v in zip(times, tokens_per_sec):
|
||||
while t > bar_times[bar_idx] + interval:
|
||||
bar_idx += 1
|
||||
bar_values[bar_idx].append(v)
|
||||
|
||||
bar_values = [sum(v) / len(v) if v else 0 for v in bar_values]
|
||||
m = max(bar_values)
|
||||
|
||||
for t, v in zip(bar_times, bar_values):
|
||||
t = t - start
|
||||
b = c * int(v * width / m)
|
||||
print(f"{t:3.2f} {b} ({v})")
|
||||
|
||||
|
||||
def percentile(data, percent):
|
||||
if not data:
|
||||
return 0
|
||||
data = sorted(data)
|
||||
k = (len(data) - 1) * percent / 100
|
||||
f = math.floor(k)
|
||||
c = math.ceil(k)
|
||||
return (
|
||||
data[int(f)]
|
||||
if f == c
|
||||
else data[int(f)] + (data[int(c)] - data[int(f)]) * (k - f)
|
||||
)
|
||||
|
||||
|
||||
def median(data):
|
||||
return percentile(data, 50)
|
||||
|
||||
|
||||
async def make_request(
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
max_tokens: int,
|
||||
) -> Tuple[bool, float, list]:
|
||||
"""
|
||||
Make a single streaming API request and return
|
||||
|
||||
- whether the request succeeded
|
||||
- the request start time
|
||||
- the time of every generated token
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"stream": True,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
start_time = time.perf_counter()
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_body = await response.text()
|
||||
print(f"Error {response.status}: {error_body}")
|
||||
return (False, 0, [])
|
||||
|
||||
# Process streaming response
|
||||
async for chunk in response.content:
|
||||
if chunk:
|
||||
chunk_str = chunk.decode("utf-8").strip()
|
||||
if chunk_str.startswith("data:"):
|
||||
data_str = chunk_str[5:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
if choices := data.get("choices", False):
|
||||
if choices[0].get("finish_reason") != "length":
|
||||
tokens.append(time.perf_counter())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return (bool(tokens), start_time, tokens)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {str(e)}")
|
||||
return (False, 0, [])
|
||||
|
||||
|
||||
async def run_benchmark(
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
concurrency: int,
|
||||
total_requests: int,
|
||||
prompts: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
prompt_cycle = cycle(prompts)
|
||||
semaphore = asyncio.Semaphore(concurrency)
|
||||
results = []
|
||||
request_times = []
|
||||
bar = tqdm(total=total_requests)
|
||||
|
||||
async def worker():
|
||||
async with semaphore:
|
||||
prompt = next(prompt_cycle)
|
||||
result = await make_request(
|
||||
session, url, api_key, model, prompt, max_tokens
|
||||
)
|
||||
bar.update(1)
|
||||
return result
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for _ in range(total_requests):
|
||||
task = asyncio.create_task(worker())
|
||||
tasks.append(task)
|
||||
await asyncio.sleep(0.01) # Stagger requests slightly
|
||||
|
||||
for task in tasks:
|
||||
result = await task
|
||||
results.append(result)
|
||||
bar.close()
|
||||
|
||||
successful_requests = [r for r in results if r[0]]
|
||||
total_tokens = sum(len(r[2]) for r in successful_requests)
|
||||
|
||||
# Gather all the tokens generated with their corresponding timestamps
|
||||
all_tokens = []
|
||||
for r in successful_requests:
|
||||
all_tokens.extend(r[2])
|
||||
all_tokens.sort()
|
||||
full_generation = tokens_per_second(all_tokens)
|
||||
start = min(r[1] for r in successful_requests)
|
||||
|
||||
# Aggregate metrics
|
||||
metrics = {
|
||||
"total_requests": total_requests,
|
||||
"successful_requests": len(successful_requests),
|
||||
"failed_requests": total_requests - len(successful_requests),
|
||||
"total_tokens": total_tokens,
|
||||
"total_time": all_tokens[-1] - start,
|
||||
"aggregate_tokens_per_sec": median(full_generation[1]),
|
||||
"per_request": [],
|
||||
"start": start,
|
||||
"full_generation": full_generation,
|
||||
}
|
||||
|
||||
# Per-request metrics
|
||||
for i, (_, start, tokens) in enumerate(successful_requests):
|
||||
metrics["per_request"].append(
|
||||
{
|
||||
"request_id": i + 1,
|
||||
"time_to_first_token": tokens[0] - start,
|
||||
"total_time": tokens[-1] - start,
|
||||
"tokens_received": len(tokens),
|
||||
"tokens_per_sec": median(tokens_per_second(tokens)[1]),
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate percentiles
|
||||
ttft_values = [m["time_to_first_token"] for m in metrics["per_request"]]
|
||||
tps_values = [m["tokens_per_sec"] for m in metrics["per_request"]]
|
||||
|
||||
metrics["aggregate_metrics"] = {
|
||||
"time_to_first_token": {
|
||||
"min": min(ttft_values) if ttft_values else 0,
|
||||
"max": max(ttft_values) if ttft_values else 0,
|
||||
"avg": sum(ttft_values) / len(ttft_values) if ttft_values else 0,
|
||||
"p95": percentile(ttft_values, 95) if ttft_values else 0,
|
||||
},
|
||||
"tokens_per_sec": {
|
||||
"min": min(tps_values) if tps_values else 0,
|
||||
"max": max(tps_values) if tps_values else 0,
|
||||
"avg": sum(tps_values) / len(tps_values) if tps_values else 0,
|
||||
"p95": percentile(tps_values, 95) if tps_values else 0,
|
||||
},
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LLM API Benchmark Tool")
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
default="http://localhost:8080/v1/chat/completions",
|
||||
help="Chat completions API endpoint URL",
|
||||
)
|
||||
parser.add_argument("--api-key", default="none", help="API key")
|
||||
parser.add_argument("--model", default="default_model", help="Model name")
|
||||
parser.add_argument(
|
||||
"--max-tokens", type=int, default=100, help="Max tokens to generate"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--concurrency", type=int, default=1, help="Number of concurrent requests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-requests", type=int, default=10, help="Total requests to make"
|
||||
)
|
||||
parser.add_argument("--prompt-file", help="File containing prompts (one per line)")
|
||||
parser.add_argument("--output", help="Output file for results (JSON format)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load prompts
|
||||
if args.prompt_file:
|
||||
with open(args.prompt_file, "r") as f:
|
||||
prompts = [line.strip() for line in f if line.strip()]
|
||||
else:
|
||||
prompts = DEFAULT_PROMPTS
|
||||
|
||||
print(
|
||||
f"Starting benchmark with {args.concurrency} concurrency and {args.total_requests} total requests..."
|
||||
)
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Run benchmark
|
||||
results = asyncio.run(
|
||||
run_benchmark(
|
||||
url=args.url,
|
||||
api_key=args.api_key,
|
||||
model=args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
concurrency=args.concurrency,
|
||||
total_requests=args.total_requests,
|
||||
prompts=prompts,
|
||||
)
|
||||
)
|
||||
|
||||
duration = time.perf_counter() - start_time
|
||||
print(f"\nBenchmark completed in {duration:.2f} seconds")
|
||||
print(
|
||||
f"Successful requests: {results['successful_requests']}/{args.total_requests}"
|
||||
)
|
||||
print(f"Total tokens generated: {results['total_tokens']}")
|
||||
print(f"Aggregate tokens/sec: {results['aggregate_tokens_per_sec']:.2f}")
|
||||
|
||||
# Print summary
|
||||
if results["successful_requests"] > 0:
|
||||
ttft = results["aggregate_metrics"]["time_to_first_token"]
|
||||
tps = results["aggregate_metrics"]["tokens_per_sec"]
|
||||
|
||||
print("\nTime to First Token (seconds):")
|
||||
print(
|
||||
f" Min: {ttft['min']:.4f} | Max: {ttft['max']:.4f} | Avg: {ttft['avg']:.4f} | P95: {ttft['p95']:.4f}"
|
||||
)
|
||||
|
||||
print("\nTokens per Second (per request):")
|
||||
print(
|
||||
f" Min: {tps['min']:.2f} | Max: {tps['max']:.2f} | Avg: {tps['avg']:.2f} | P95: {tps['p95']:.2f}"
|
||||
)
|
||||
|
||||
print()
|
||||
plot_generation(*results["full_generation"], results["start"])
|
||||
|
||||
# Save results
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"\nResults saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.29.0"
|
||||
__version__ = "0.30.4"
|
||||
|
||||
+12
-3
@@ -6,7 +6,7 @@ import mlx.core as mx
|
||||
|
||||
from mlx_lm import batch_generate, load, stream_generate
|
||||
from mlx_lm.generate import DEFAULT_MODEL
|
||||
from mlx_lm.utils import pipeline_load
|
||||
from mlx_lm.utils import pipeline_load, sharded_load
|
||||
|
||||
|
||||
def setup_arg_parser():
|
||||
@@ -49,6 +49,11 @@ 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",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -59,6 +64,8 @@ def main():
|
||||
|
||||
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:
|
||||
@@ -67,10 +74,12 @@ def main():
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
if group.size() > 1:
|
||||
model, tokenizer, config = pipeline_load(args.model, return_config=True)
|
||||
model, tokenizer, config = sharded_load(
|
||||
model_path, pipeline_group, tensor_group, return_config=True
|
||||
)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
model_path, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
|
||||
+4
-17
@@ -41,16 +41,6 @@ def setup_arg_parser():
|
||||
default=None,
|
||||
help="End of sequence token for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-chat-template",
|
||||
action="store_true",
|
||||
help="Use the raw prompt without the tokenizer's chat template.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-default-chat-template",
|
||||
action="store_true",
|
||||
help="Use the default chat template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-kv-size",
|
||||
type=int,
|
||||
@@ -107,14 +97,12 @@ def main():
|
||||
|
||||
args.prompt = sys.stdin.read() if args.prompt == "-" else args.prompt
|
||||
|
||||
if args.use_default_chat_template:
|
||||
if tokenizer.chat_template is None:
|
||||
tokenizer.chat_template = tokenizer.default_chat_template
|
||||
|
||||
if not args.ignore_chat_template and tokenizer.chat_template is not None:
|
||||
if tokenizer.has_chat_template:
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=False, continue_final_message=True
|
||||
messages,
|
||||
add_generation_prompt=False,
|
||||
continue_final_message=True,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -153,7 +141,6 @@ def main():
|
||||
print("Saving...")
|
||||
metadata = {}
|
||||
metadata["model"] = args.model
|
||||
metadata["chat_template"] = json.dumps(tokenizer.chat_template)
|
||||
metadata["tokenizer_config"] = json.dumps(tokenizer_config)
|
||||
save_prompt_cache(args.prompt_cache_file, cache, metadata)
|
||||
|
||||
|
||||
+39
-17
@@ -7,7 +7,7 @@ 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
|
||||
from .utils import load, sharded_load
|
||||
|
||||
DEFAULT_TEMP = 0.0
|
||||
DEFAULT_TOP_P = 1.0
|
||||
@@ -79,6 +79,11 @@ def setup_arg_parser():
|
||||
default=None,
|
||||
help="System prompt to be used for the chat template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
action="store_true",
|
||||
help="Use pipelining instead of tensor parallelism",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -86,28 +91,42 @@ def main():
|
||||
parser = setup_arg_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
pipeline_group = group if args.pipeline else None
|
||||
tensor_group = group if not args.pipeline else None
|
||||
|
||||
def rprint(*args, **kwargs):
|
||||
if rank == 0:
|
||||
print(*args, **kwargs)
|
||||
|
||||
if args.seed is not None:
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model, tokenizer = load(
|
||||
args.model,
|
||||
adapter_path=args.adapter_path,
|
||||
tokenizer_config={
|
||||
"trust_remote_code": True if args.trust_remote_code else None
|
||||
},
|
||||
)
|
||||
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
|
||||
},
|
||||
)
|
||||
|
||||
def print_help():
|
||||
print("The command list:")
|
||||
print("- 'q' to exit")
|
||||
print("- 'r' to reset the chat")
|
||||
print("- 'h' to display these commands")
|
||||
rprint("The command list:")
|
||||
rprint("- 'q' to exit")
|
||||
rprint("- 'r' to reset the chat")
|
||||
rprint("- 'h' to display these commands")
|
||||
|
||||
print(f"[INFO] Starting chat session with {args.model}.")
|
||||
rprint(f"[INFO] Starting chat session with {args.model}.")
|
||||
print_help()
|
||||
prompt_cache = make_prompt_cache(model, args.max_kv_size)
|
||||
while True:
|
||||
query = input(">> ")
|
||||
query = input(">> " if rank == 0 else "")
|
||||
if query == "q":
|
||||
break
|
||||
if query == "r":
|
||||
@@ -120,7 +139,10 @@ 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,
|
||||
@@ -137,8 +159,8 @@ def main():
|
||||
),
|
||||
prompt_cache=prompt_cache,
|
||||
):
|
||||
print(response.text, flush=True, end="")
|
||||
print()
|
||||
rprint(response.text, flush=True, end="")
|
||||
rprint()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
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):
|
||||
return [tool["function"] 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
|
||||
) -> 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 = msg.get("tools")
|
||||
response_format = msg.get("response_format")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_content = msg.get("reasoning_content")
|
||||
|
||||
if tools:
|
||||
tools = tools_from_openai_format(tools)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
) -> 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
|
||||
)
|
||||
|
||||
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
|
||||
+15
-4
@@ -179,7 +179,12 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
description="Convert Hugging Face model to MLX format"
|
||||
)
|
||||
|
||||
parser.add_argument("--hf-path", type=str, help="Path to the Hugging Face model.")
|
||||
parser.add_argument(
|
||||
"--hf-path",
|
||||
"--model",
|
||||
type=str,
|
||||
help="Path to the model. This can be a local path or a Hugging Face Hub model identifier.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mlx-path", type=str, default="mlx_model", help="Path to save the MLX model."
|
||||
)
|
||||
@@ -187,17 +192,23 @@ def configure_parser() -> argparse.ArgumentParser:
|
||||
"-q", "--quantize", help="Generate a quantized model.", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-group-size", help="Group size for quantization.", type=int, default=64
|
||||
"--q-group-size",
|
||||
help="Group size for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-bits", help="Bits per weight for quantization.", type=int, default=4
|
||||
"--q-bits",
|
||||
help="Bits per weight for quantization.",
|
||||
type=int,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--q-mode",
|
||||
help="The quantization mode.",
|
||||
type=str,
|
||||
default="affine",
|
||||
choices=["affine", "mxfp4"],
|
||||
choices=["affine", "mxfp4", "nvfp4", "mxfp8"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quant-predicate",
|
||||
|
||||
+3
-1
@@ -79,6 +79,7 @@ class MLXLM(LM):
|
||||
self,
|
||||
path_or_hf_repo: str,
|
||||
max_tokens: Optional[int] = None,
|
||||
batch_size: int = 8,
|
||||
use_chat_template: Optional[bool] = None,
|
||||
trust_remote_code: bool = False,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
@@ -89,7 +90,7 @@ class MLXLM(LM):
|
||||
path_or_hf_repo, tokenizer_config=tokenizer_config
|
||||
)
|
||||
self._max_tokens = max_tokens
|
||||
self._batch_size = 8
|
||||
self._batch_size = batch_size
|
||||
self.use_chat_template = use_chat_template
|
||||
if use_chat_template is None:
|
||||
self.use_chat_template = self.tokenizer.chat_template is not None
|
||||
@@ -476,6 +477,7 @@ def main():
|
||||
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,
|
||||
|
||||
@@ -15,7 +15,10 @@ prompt_cache = make_prompt_cache(model)
|
||||
# User turn
|
||||
prompt = "Hi my name is <Name>."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
@@ -29,7 +32,10 @@ response = generate(
|
||||
# User turn
|
||||
prompt = "What's my name?"
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Assistant response
|
||||
response = generate(
|
||||
|
||||
@@ -14,7 +14,8 @@ conversation = [{"role": "user", "content": prompt}]
|
||||
|
||||
# Transform the prompt into the chat template
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
conversation=conversation, add_generation_prompt=True
|
||||
conversation=conversation,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
# Specify the maximum number of tokens
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="not-needed",
|
||||
base_url="http://localhost:8080/v1",
|
||||
)
|
||||
|
||||
model = "mlx-community/Qwen3-4B-Thinking-2507-4bit"
|
||||
|
||||
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
|
||||
|
||||
# Non-streaming example
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model, messages=messages, max_tokens=2048
|
||||
)
|
||||
|
||||
reasoning = response.choices[0].message.reasoning
|
||||
content = response.choices[0].message.content
|
||||
|
||||
print("=== reasoning ===\n")
|
||||
print(f"\033[37m{reasoning}\033[0m")
|
||||
print("=== content ===\n")
|
||||
print(content)
|
||||
|
||||
# Streaming example
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
max_tokens=2048,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
if (reasoning := chunk.choices[0].delta.reasoning) is not None:
|
||||
print(f"\033[37m{reasoning}\033[0m", end="")
|
||||
if (content := chunk.choices[0].delta.content) is not None:
|
||||
print(f"{content}", end="")
|
||||
print()
|
||||
@@ -8,11 +8,13 @@ To run, first start the server:
|
||||
|
||||
Then run this script.
|
||||
"""
|
||||
import json
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
|
||||
|
||||
model = "mlx-community/qwen3-4b-4bit-DWQ"
|
||||
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
|
||||
|
||||
tools = [
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# Copyright © 2024 Apple Inc.
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
"""
|
||||
Run with:
|
||||
|
||||
```
|
||||
mlx.launch \
|
||||
--hostfile /path/to/hosts.json \
|
||||
/path/to/pipeline_generate.py \
|
||||
--prompt "hello world"
|
||||
--backend jaccl \
|
||||
--env MLX_METAL_FAST_SYNCH=1 \
|
||||
--hostfile /path/to/hosts.json \
|
||||
/path/to/sharded_generate.py \
|
||||
--prompt 'Hello world'
|
||||
```
|
||||
|
||||
Make sure you can run MLX over MPI on two hosts. For more information see the
|
||||
documentation:
|
||||
For more information on running distributed programs with MLX see the documentation:
|
||||
|
||||
https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
|
||||
https://ml-explore.github.io/mlx/build/html/usage/distributed.html .
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -21,13 +22,13 @@ import argparse
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import stream_generate
|
||||
from mlx_lm.utils import pipeline_load
|
||||
from mlx_lm.utils import sharded_load
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
|
||||
parser = argparse.ArgumentParser(description="LLM distributed inference example")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="mlx-community/DeepSeek-R1-3bit",
|
||||
default="mlx-community/Llama-3.3-70B-Instruct-4bit",
|
||||
help="HF repo or path to local model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -43,19 +44,29 @@ if __name__ == "__main__":
|
||||
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 = pipeline_load(args.model)
|
||||
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)
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for response in stream_generate(
|
||||
model, tokenizer, prompt, max_tokens=args.max_tokens
|
||||
@@ -6,7 +6,7 @@ from mlx_lm import generate, load
|
||||
from mlx_lm.models.cache import make_prompt_cache
|
||||
|
||||
# Specify the checkpoint
|
||||
checkpoint = "mlx-community/Qwen2.5-32B-Instruct-4bit"
|
||||
checkpoint = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
|
||||
# Load the corresponding model and tokenizer
|
||||
model, tokenizer = load(path_or_hf_repo=checkpoint)
|
||||
@@ -31,7 +31,9 @@ prompt = "Multiply 12234585 and 48838483920."
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True, tools=list(tools.values())
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
tools=list(tools.values()),
|
||||
)
|
||||
|
||||
prompt_cache = make_prompt_cache(model)
|
||||
@@ -47,12 +49,11 @@ response = generate(
|
||||
)
|
||||
|
||||
# Parse the tool call:
|
||||
# (Note, the tool call format is model specific)
|
||||
tool_open = "<tool_call>"
|
||||
tool_close = "</tool_call>"
|
||||
start_tool = response.find(tool_open) + len(tool_open)
|
||||
end_tool = response.find(tool_close)
|
||||
tool_call = json.loads(response[start_tool:end_tool].strip())
|
||||
# - The tool call format is model specific.
|
||||
# - The tokenizer's tool parser expects tool call text to be already extracted.
|
||||
start_tool = response.find(tokenizer.tool_call_start) + len(tokenizer.tool_call_start)
|
||||
end_tool = response.find(tokenizer.tool_call_end)
|
||||
tool_call = tokenizer.tool_parser(response[start_tool:end_tool].strip())
|
||||
tool_result = tools[tool_call["name"]](**tool_call["arguments"])
|
||||
|
||||
# Put the tool result in the prompt
|
||||
|
||||
+2
-1
@@ -76,8 +76,9 @@ def main() -> None:
|
||||
|
||||
if args.dequantize:
|
||||
print("Dequantizing model")
|
||||
model = dequantize(model)
|
||||
model = dequantize_model(model)
|
||||
config.pop("quantization", None)
|
||||
config.pop("quantization_config", None)
|
||||
|
||||
save_path = Path(args.save_path)
|
||||
save(
|
||||
|
||||
+111
-41
@@ -181,8 +181,7 @@ def setup_arg_parser():
|
||||
parser.add_argument(
|
||||
"--kv-bits",
|
||||
type=int,
|
||||
help="Number of bits for KV cache quantization. "
|
||||
"Defaults to no quantization.",
|
||||
help="Number of bits for KV cache quantization. Defaults to no quantization.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -548,7 +547,9 @@ def speculative_generate_step(
|
||||
y = y[: -(n_predict - 1)]
|
||||
for i in range(n_predict):
|
||||
prev_tokens = (
|
||||
mx.concat([prev_tokens, y]) if prev_tokens is not None else y
|
||||
mx.concatenate([prev_tokens, y])
|
||||
if prev_tokens is not None
|
||||
else y
|
||||
)
|
||||
y, logprobs = _process_and_sample(prev_tokens, logits[:, i, :])
|
||||
out_y.append(y)
|
||||
@@ -840,6 +841,9 @@ class Batch:
|
||||
max_tokens: List[int]
|
||||
num_tokens: List[int]
|
||||
cache: List[Any]
|
||||
samplers: List[Any]
|
||||
logits_processors: List[Any]
|
||||
tokens: List[mx.array]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.uids)
|
||||
@@ -849,6 +853,9 @@ class Batch:
|
||||
self.logprobs = [self.logprobs[k] for k in keep_idx]
|
||||
self.max_tokens = [self.max_tokens[k] for k in keep_idx]
|
||||
self.num_tokens = [self.num_tokens[k] for k in keep_idx]
|
||||
self.samplers = [self.samplers[k] for k in keep_idx]
|
||||
self.logits_processors = [self.logits_processors[k] for k in keep_idx]
|
||||
self.tokens = [self.tokens[k] for k in keep_idx]
|
||||
keep_idx = mx.array(keep_idx, mx.int32)
|
||||
self.y = self.y[keep_idx]
|
||||
for c in self.cache:
|
||||
@@ -860,6 +867,9 @@ class Batch:
|
||||
self.logprobs.extend(other.logprobs)
|
||||
self.num_tokens.extend(other.num_tokens)
|
||||
self.max_tokens.extend(other.max_tokens)
|
||||
self.samplers.extend(other.samplers)
|
||||
self.logits_processors.extend(other.logits_processors)
|
||||
self.tokens.extend(other.tokens)
|
||||
for c, o in zip(self.cache, other.cache):
|
||||
c.extend(o)
|
||||
|
||||
@@ -874,7 +884,7 @@ def _make_cache(model, left_padding):
|
||||
"""
|
||||
|
||||
def to_batch_cache(c):
|
||||
if isinstance(c, KVCache):
|
||||
if type(c) is KVCache:
|
||||
return BatchKVCache(left_padding)
|
||||
elif isinstance(c, ArraysCache):
|
||||
c.left_padding = mx.array(left_padding)
|
||||
@@ -898,21 +908,16 @@ def _make_cache(model, left_padding):
|
||||
def _merge_caches(caches):
|
||||
batch_cache = []
|
||||
for i in range(len(caches[0])):
|
||||
cache = None
|
||||
if isinstance(caches[0][i], KVCache):
|
||||
cache = BatchKVCache.merge([c[i] for c in caches])
|
||||
elif isinstance(caches[0][i], RotatingKVCache):
|
||||
cache = BatchRotatingKVCache.merge([c[i] for c in caches])
|
||||
if hasattr(caches[0][i], "merge"):
|
||||
batch_cache.append(caches[0][i].merge([c[i] for c in caches]))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{type(caches[0][i])} does not yet support batching with history"
|
||||
)
|
||||
batch_cache.append(cache)
|
||||
return batch_cache
|
||||
|
||||
|
||||
class BatchGenerator:
|
||||
|
||||
@dataclass
|
||||
class Response:
|
||||
uid: int
|
||||
@@ -927,6 +932,9 @@ class BatchGenerator:
|
||||
max_tokens: int = 128,
|
||||
stop_tokens: Optional[set] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
logits_processors: Optional[
|
||||
List[Callable[[mx.array, mx.array], mx.array]]
|
||||
] = None,
|
||||
completion_batch_size: int = 32,
|
||||
prefill_batch_size: int = 8,
|
||||
prefill_step_size: int = 2048,
|
||||
@@ -939,6 +947,7 @@ class BatchGenerator:
|
||||
self.max_tokens = max_tokens
|
||||
self.stop_tokens = stop_tokens or set()
|
||||
self.sampler = sampler or (lambda x: mx.argmax(x, axis=-1))
|
||||
self.logits_processors = logits_processors or []
|
||||
self.uid_count = 0
|
||||
self.prefill_step_size = prefill_step_size
|
||||
self.prefill_batch_size = prefill_batch_size
|
||||
@@ -965,7 +974,12 @@ class BatchGenerator:
|
||||
self.close()
|
||||
|
||||
def insert(
|
||||
self, prompts, max_tokens: Union[List[int], int, None] = None, caches=None
|
||||
self,
|
||||
prompts,
|
||||
max_tokens: Union[List[int], int, None] = None,
|
||||
caches=None,
|
||||
samplers: list | None = None,
|
||||
logits_processors: list | None = None,
|
||||
):
|
||||
uids = []
|
||||
|
||||
@@ -978,13 +992,19 @@ class BatchGenerator:
|
||||
if caches[i] is None:
|
||||
caches[i] = cache.make_prompt_cache(self.model)
|
||||
|
||||
for p, m, c in zip(prompts, max_tokens, caches):
|
||||
self.unprocessed_prompts.append((self.uid_count, p, m, c))
|
||||
samplers = samplers or [None] * len(prompts)
|
||||
logits_processors = logits_processors or [self.logits_processors] * len(prompts)
|
||||
|
||||
for p, m, c, s, lp in zip(
|
||||
prompts, max_tokens, caches, samplers, logits_processors
|
||||
):
|
||||
self.unprocessed_prompts.append((self.uid_count, p, m, c, s, lp))
|
||||
uids.append(self.uid_count)
|
||||
self.uid_count += 1
|
||||
# Sort in ascending order of length
|
||||
self.unprocessed_prompts = sorted(
|
||||
self.unprocessed_prompts, key=lambda x: len(x[1]) + cache.cache_length(x[3])
|
||||
self.unprocessed_prompts,
|
||||
key=lambda x: len(x[1]) + max(c.size() for c in x[3]),
|
||||
)
|
||||
return uids
|
||||
|
||||
@@ -1003,22 +1023,21 @@ class BatchGenerator:
|
||||
self.unprocessed_prompts.pop(i)
|
||||
|
||||
def _process_prompts(self, prompts):
|
||||
uids, inputs, max_tokens, caches = zip(*prompts)
|
||||
uids, inputs, max_tokens, caches, samplers, logits_processors = zip(*prompts)
|
||||
|
||||
cache_lengths = [cache.cache_length(c) for c in caches]
|
||||
max_cache_length = max(cache_lengths)
|
||||
lengths = [len(p) for p in inputs]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
|
||||
self._stats.prompt_tokens += sum(lengths)
|
||||
|
||||
tokens = [mx.array(inp) for inp in inputs]
|
||||
processed_tokens = 0
|
||||
|
||||
# New prompts so
|
||||
# 1. Left-pad the inputs
|
||||
# 2. Process
|
||||
if max_cache_length == 0:
|
||||
if all(c[0].empty() for c in caches):
|
||||
inputs = _left_pad_prompts(inputs, max_length=max_length)
|
||||
prompt_cache = _make_cache(self.model, padding)
|
||||
|
||||
@@ -1034,7 +1053,6 @@ class BatchGenerator:
|
||||
for uid, length in zip(uids, lengths)
|
||||
]
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
# Further prompt processing so we need to
|
||||
# 1. Merge the KV caches and prepare for right padded prompts
|
||||
@@ -1047,7 +1065,8 @@ class BatchGenerator:
|
||||
prompt_cache = _merge_caches(caches)
|
||||
|
||||
for c in prompt_cache:
|
||||
c.prepare(lengths=lengths, right_padding=padding)
|
||||
# subtract one from lengths since we don't process the last token during prefill
|
||||
c.prepare(lengths=[l - 1 for l in lengths], right_padding=padding)
|
||||
|
||||
while inputs.shape[1] > 1:
|
||||
n_to_process = min(self.prefill_step_size, inputs.shape[1] - 1)
|
||||
@@ -1063,23 +1082,64 @@ class BatchGenerator:
|
||||
)
|
||||
mx.clear_cache()
|
||||
|
||||
for c in prompt_cache:
|
||||
c.finalize()
|
||||
mx.eval([c.state for c in prompt_cache])
|
||||
mx.clear_cache()
|
||||
inputs = last_inputs
|
||||
|
||||
y, logprobs = self._step(inputs, prompt_cache)
|
||||
mx.async_eval(y, logprobs)
|
||||
return Batch(
|
||||
list(uids), y, logprobs, list(max_tokens), [0] * len(uids), prompt_cache
|
||||
for c in prompt_cache:
|
||||
c.finalize()
|
||||
mx.clear_cache()
|
||||
|
||||
y, logprobs = self._step(
|
||||
inputs, prompt_cache, samplers, logits_processors, tokens
|
||||
)
|
||||
|
||||
def _step(self, input_tokens: mx.array, prompt_cache: List[Any]):
|
||||
mx.async_eval(y, logprobs)
|
||||
|
||||
return Batch(
|
||||
list(uids),
|
||||
y,
|
||||
logprobs,
|
||||
list(max_tokens),
|
||||
[0] * len(uids),
|
||||
prompt_cache,
|
||||
list(samplers),
|
||||
list(logits_processors),
|
||||
tokens,
|
||||
)
|
||||
|
||||
def _step(
|
||||
self,
|
||||
input_tokens: mx.array,
|
||||
prompt_cache: List[Any],
|
||||
samplers: list | None,
|
||||
logits_processors: list | None,
|
||||
tokens: List[mx.array],
|
||||
):
|
||||
batch_size = input_tokens.shape[0]
|
||||
|
||||
logits = self.model(input_tokens, cache=prompt_cache)
|
||||
logits = logits[:, -1, :]
|
||||
|
||||
if any(logits_processors):
|
||||
processed_logits = []
|
||||
for e in range(batch_size):
|
||||
sample_logits = logits[e : e + 1]
|
||||
for processor in logits_processors[e]:
|
||||
sample_logits = processor(tokens[e], sample_logits)
|
||||
processed_logits.append(sample_logits)
|
||||
logits = mx.concatenate(processed_logits, axis=0)
|
||||
|
||||
logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
|
||||
sampled = self.sampler(logprobs)
|
||||
if any(samplers):
|
||||
all_samples = []
|
||||
for e in range(batch_size):
|
||||
sample_sampler = samplers[e] or self.sampler
|
||||
sampled = sample_sampler(logprobs[e : e + 1])
|
||||
all_samples.append(sampled)
|
||||
sampled = mx.concatenate(all_samples, axis=0)
|
||||
else:
|
||||
sampled = self.sampler(logprobs)
|
||||
|
||||
return sampled, list(logprobs)
|
||||
|
||||
def stats(self):
|
||||
@@ -1129,7 +1189,16 @@ class BatchGenerator:
|
||||
|
||||
batch = self.active_batch
|
||||
y, logprobs = batch.y, batch.logprobs
|
||||
batch.y, batch.logprobs = self._step(y[:, None], batch.cache)
|
||||
for i, toks in enumerate(batch.tokens):
|
||||
batch.tokens[i] = mx.concatenate((toks, y[i : i + 1]))
|
||||
batch.y, batch.logprobs = self._step(
|
||||
y[:, None],
|
||||
batch.cache,
|
||||
batch.samplers,
|
||||
batch.logits_processors,
|
||||
batch.tokens,
|
||||
)
|
||||
|
||||
mx.async_eval(batch.y, batch.logprobs)
|
||||
|
||||
y = y.tolist()
|
||||
@@ -1179,11 +1248,12 @@ class BatchGenerator:
|
||||
def batch_generate(
|
||||
model,
|
||||
tokenizer,
|
||||
prompts: List[int],
|
||||
prompts: List[List[int]],
|
||||
prompt_caches: Optional[List[List[Any]]] = None,
|
||||
max_tokens: Union[int, List[int]] = 128,
|
||||
verbose: bool = False,
|
||||
return_prompt_caches: bool = False,
|
||||
logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = None,
|
||||
**kwargs,
|
||||
) -> BatchResponse:
|
||||
"""
|
||||
@@ -1192,7 +1262,7 @@ def batch_generate(
|
||||
Args:
|
||||
model (nn.Module): The language model.
|
||||
tokenizer (PreTrainedTokenizer): The tokenizer.
|
||||
prompt (List[List[int]]): The input prompts.
|
||||
prompts (List[List[int]]): The input prompts.
|
||||
prompt_caches (List[List[Any]], optional): Pre-computed prompt-caches
|
||||
for each input prompt. Note, unlike ``generate_step``, the caches
|
||||
won't be updated in-place.
|
||||
@@ -1202,11 +1272,17 @@ def batch_generate(
|
||||
can be per prompt if a list is provided.
|
||||
return_prompt_caches (bool): Return the prompt caches in the batch
|
||||
responses. Default: ``False``.
|
||||
logits_processors (List[Callable[[mx.array, mx.array], mx.array]], optional):
|
||||
A list of functions that take tokens and logits and return the processed logits. Default: ``None``.
|
||||
kwargs: The remaining options get passed to :obj:`BatchGenerator`.
|
||||
See :obj:`BatchGenerator` for more details.
|
||||
"""
|
||||
|
||||
gen = BatchGenerator(model, stop_tokens=tokenizer.eos_token_ids, **kwargs)
|
||||
gen = BatchGenerator(
|
||||
model,
|
||||
stop_tokens=tokenizer.eos_token_ids,
|
||||
**kwargs,
|
||||
)
|
||||
num_samples = len(prompts)
|
||||
fin = 0
|
||||
if verbose:
|
||||
@@ -1302,15 +1378,9 @@ def main():
|
||||
if args.chat_template_config is not None:
|
||||
template_kwargs = json.loads(args.chat_template_config)
|
||||
|
||||
if args.use_default_chat_template:
|
||||
if tokenizer.chat_template is None:
|
||||
tokenizer.chat_template = tokenizer.default_chat_template
|
||||
elif using_cache:
|
||||
tokenizer.chat_template = json.loads(metadata["chat_template"])
|
||||
|
||||
prompt = args.prompt.replace("\\n", "\n").replace("\\t", "\t")
|
||||
prompt = sys.stdin.read() if prompt == "-" else prompt
|
||||
if not args.ignore_chat_template and tokenizer.chat_template is not None:
|
||||
if not args.ignore_chat_template and tokenizer.has_chat_template:
|
||||
if args.system_prompt is not None:
|
||||
messages = [{"role": "system", "content": args.system_prompt}]
|
||||
else:
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
|
||||
@@ -114,7 +115,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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class KlearSparseMoeBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
@@ -9,6 +9,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ConcatenateKVCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -262,11 +263,6 @@ class KVReuseAttention(nn.Module):
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _swiglu(g, x):
|
||||
return nn.silu(g) * x
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
@@ -281,7 +277,7 @@ class MLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
g = self.gate_proj(x)
|
||||
x = self.up_proj(x)
|
||||
return self.down_proj(_swiglu(g, x))
|
||||
return self.down_proj(swiglu(g, x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -149,7 +150,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoERouter(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, List, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache, MambaCache, RotatingKVCache
|
||||
|
||||
@@ -140,7 +141,7 @@ class MLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -49,11 +50,6 @@ class ModelArgs(BaseModelArgs):
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(gate, up):
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def aggregate_expert_outputs(expert_outputs, scores):
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Tuple, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
@@ -130,7 +131,7 @@ class MLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
|
||||
+145
-39
@@ -109,10 +109,6 @@ def trim_prompt_cache(cache: List[Any], num_tokens: int) -> List[Any]:
|
||||
return [c.trim(num_tokens) for c in cache][0]
|
||||
|
||||
|
||||
def cache_length(cache: List[Any]):
|
||||
return max(len(c) for c in cache)
|
||||
|
||||
|
||||
def create_attention_mask(
|
||||
N: int, offset: int, return_array: bool, window_size: Optional[int]
|
||||
):
|
||||
@@ -146,23 +142,20 @@ class _BaseCache:
|
||||
def is_trimmable(self):
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
"""The length of a cache is meant to represent the number of elements
|
||||
that we need to process in the attention. For instance for KVCache it
|
||||
is the size of the state, for RotatingKVCache it would be up to
|
||||
max_size etc."""
|
||||
def size(self):
|
||||
"""
|
||||
Return the size (i.e. sequence length) of the cache.
|
||||
|
||||
Not every cache is required to implement this, in which case the size
|
||||
will always be 0 (though the cache may not be empty).
|
||||
"""
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
"""When an object defines __len__ then python defines the bool operator
|
||||
as len(obj) != 0. This, for instance, doesn't allow us to write
|
||||
|
||||
cache = cache or make_cache()
|
||||
|
||||
which is why we are overriding that behaviour with a constant bool
|
||||
operator return True.
|
||||
def empty(self):
|
||||
"""
|
||||
return True
|
||||
Return if the cache is empty or not.
|
||||
"""
|
||||
raise NotImplementedError("Cache sub-class must implement this.")
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state, meta_state):
|
||||
@@ -217,6 +210,9 @@ class ConcatenateKVCache(_BaseCache):
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class QuantizedKVCache(_BaseCache):
|
||||
step = 256
|
||||
@@ -303,6 +299,9 @@ class QuantizedKVCache(_BaseCache):
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class KVCache(_BaseCache):
|
||||
step = 256
|
||||
@@ -336,7 +335,7 @@ class KVCache(_BaseCache):
|
||||
self.values[..., prev : self.offset, :] = values
|
||||
return self.keys[..., : self.offset, :], self.values[..., : self.offset, :]
|
||||
|
||||
def __len__(self):
|
||||
def size(self):
|
||||
return self.offset
|
||||
|
||||
@property
|
||||
@@ -375,6 +374,13 @@ class KVCache(_BaseCache):
|
||||
def make_mask(self, *args, **kwargs):
|
||||
return create_attention_mask(*args, offset=self.offset, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def merge(_, caches):
|
||||
return BatchKVCache.merge(caches)
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class RotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
@@ -483,7 +489,7 @@ class RotatingKVCache(_BaseCache):
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
def size(self):
|
||||
return min(self.offset, self.max_size)
|
||||
|
||||
@property
|
||||
@@ -546,11 +552,19 @@ class RotatingKVCache(_BaseCache):
|
||||
mask = mx.roll(mask, shift=idx + 1)
|
||||
return mask
|
||||
|
||||
@classmethod
|
||||
def merge(_, caches):
|
||||
return BatchRotatingKVCache.merge(caches)
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class ArraysCache(_BaseCache):
|
||||
def __init__(self, size, left_padding: Optional[List[int]] = None):
|
||||
self.cache = [None] * size
|
||||
self.left_padding = mx.array(left_padding) if left_padding else None
|
||||
self.lengths = None
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
self.cache[idx] = value
|
||||
@@ -571,30 +585,73 @@ class ArraysCache(_BaseCache):
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
self.cache = [c[batch_indices] for c in self.cache]
|
||||
self.left_padding = None
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
self.cache = [mx.concatenate([c, o]) for c, o in zip(self.cache, other.cache)]
|
||||
|
||||
def extract(self, idx):
|
||||
cache = ArraysCache(len(self.cache))
|
||||
cache.cache = [c[idx : idx + 1] for c in self.cache]
|
||||
return cache
|
||||
|
||||
def prepare(self, lengths=None, **kwargs):
|
||||
self.lengths = mx.array(lengths)
|
||||
|
||||
def finalize(self):
|
||||
self.lengths = None
|
||||
self.left_padding = None
|
||||
|
||||
def advance(self, N):
|
||||
if self.lengths is not None:
|
||||
self.lengths -= N
|
||||
if self.left_padding is not None:
|
||||
self.left_padding -= N
|
||||
|
||||
def make_mask(self, N: int):
|
||||
if self.cache[0] is None and self.left_padding is not None:
|
||||
return mx.arange(N) >= self.left_padding[:, None]
|
||||
if self.left_padding is not None:
|
||||
pos = mx.arange(N)
|
||||
return pos >= self.left_padding[:, None]
|
||||
elif self.lengths is not None:
|
||||
pos = mx.arange(N)
|
||||
return pos < self.lengths[:, None]
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
n_state = len(caches[0].cache)
|
||||
B = len(caches)
|
||||
cache = cls(n_state)
|
||||
for e in range(n_state):
|
||||
c_init = next(iter(c[e] for c in caches if c[e] is not None))
|
||||
shape = list(c_init.shape)
|
||||
shape[0] = B
|
||||
cache[e] = mx.zeros(shape, c_init.dtype)
|
||||
for i in range(B):
|
||||
if caches[i][e] is None:
|
||||
continue
|
||||
cache[e][i : i + 1] = caches[i][e]
|
||||
return cache
|
||||
|
||||
def empty(self):
|
||||
return self.cache[0] is None
|
||||
|
||||
|
||||
class MambaCache(ArraysCache):
|
||||
def __init__(self, left_padding: Optional[List[int]] = None):
|
||||
super().__init__(size=2, left_padding=left_padding)
|
||||
|
||||
|
||||
class ChunkedKVCache(KVCache):
|
||||
class ChunkedKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, chunk_size):
|
||||
super().__init__()
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.chunk_size = chunk_size
|
||||
self.start_position = 0
|
||||
|
||||
@@ -630,6 +687,24 @@ class ChunkedKVCache(KVCache):
|
||||
self.values[..., prev:end, :] = values
|
||||
return self.keys[..., :end, :], self.values[..., :end, :]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.offset == self.keys.shape[2]:
|
||||
return self.keys, self.values
|
||||
else:
|
||||
return (
|
||||
self.keys[..., : self.offset, :],
|
||||
self.values[..., : self.offset, :],
|
||||
)
|
||||
|
||||
@state.setter
|
||||
def state(self, v):
|
||||
self.keys, self.values = v
|
||||
self.offset = self.keys.shape[2]
|
||||
|
||||
def is_trimmable(self):
|
||||
return True
|
||||
|
||||
def trim(self, n):
|
||||
n = min(self.offset - self.start_position, n)
|
||||
self.offset -= n
|
||||
@@ -643,6 +718,9 @@ class ChunkedKVCache(KVCache):
|
||||
def meta_state(self, v):
|
||||
self.chunk_size, self.start_position = map(int, v)
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class CacheList(_BaseCache):
|
||||
def __init__(self, *caches):
|
||||
@@ -686,6 +764,32 @@ class CacheList(_BaseCache):
|
||||
for c, o in zip(self.caches, other.caches):
|
||||
c.extend(o)
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
cache = cls()
|
||||
cache.caches = tuple(
|
||||
caches[0].caches[i].merge([c.caches[i] for c in caches])
|
||||
for i in range(len(caches[0].caches))
|
||||
)
|
||||
return cache
|
||||
|
||||
def extract(self, idx):
|
||||
return CacheList(*(c.extract(idx) for c in self.caches))
|
||||
|
||||
def prepare(self, **kwargs):
|
||||
for c in self.caches:
|
||||
c.prepare(**kwargs)
|
||||
|
||||
def finalize(self):
|
||||
for c in self.caches:
|
||||
c.finalize()
|
||||
|
||||
def size(self):
|
||||
return max(c.size() for c in self.caches)
|
||||
|
||||
def empty(self):
|
||||
return self.caches[0].empty()
|
||||
|
||||
|
||||
def dynamic_roll(x, shifts, axis):
|
||||
n = x.shape[axis]
|
||||
@@ -751,9 +855,6 @@ class BatchKVCache(_BaseCache):
|
||||
self.values[..., prev : self._idx, :] = values
|
||||
return self.keys[..., : self._idx, :], self.values[..., : self._idx, :]
|
||||
|
||||
def __len__(self):
|
||||
return self._idx
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
@@ -859,7 +960,7 @@ class BatchKVCache(_BaseCache):
|
||||
|
||||
@classmethod
|
||||
def merge(cls, caches):
|
||||
lengths = [len(c) for c in caches]
|
||||
lengths = [c.size() for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
@@ -871,6 +972,8 @@ class BatchKVCache(_BaseCache):
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
if c.keys is None:
|
||||
continue
|
||||
keys[i : i + 1, :, p : p + c.offset] = c.keys[..., : c.offset, :]
|
||||
values[i : i + 1, :, p : p + c.offset] = c.values[..., : c.offset, :]
|
||||
|
||||
@@ -882,6 +985,9 @@ class BatchKVCache(_BaseCache):
|
||||
|
||||
return cache
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
|
||||
class BatchRotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
@@ -1015,9 +1121,6 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
def __len__(self):
|
||||
return min(self._offset, self.max_size)
|
||||
|
||||
def prepare(self, *, left_padding=None, lengths=None, right_padding=None):
|
||||
if left_padding is not None:
|
||||
if self.keys is not None:
|
||||
@@ -1157,12 +1260,10 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
cache.keys = mx.roll(cache.keys, -self._idx, axis=2)
|
||||
cache.values = mx.roll(cache.values, -self._idx, axis=2)
|
||||
cache._idx = self.max_size
|
||||
if padding > 0:
|
||||
cache.keys = mx.contiguous(cache.keys[:, :, padding : cache._idx])
|
||||
cache.values = mx.contiguous(cache.values[:, :, padding : cache._idx])
|
||||
cache.keys = mx.contiguous(cache.keys[:, :, padding : cache._idx])
|
||||
cache.values = mx.contiguous(cache.values[:, :, padding : cache._idx])
|
||||
cache.offset = offset
|
||||
cache._idx = cache.keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
@@ -1173,7 +1274,7 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
)
|
||||
|
||||
offsets = [c.offset for c in caches]
|
||||
lengths = [len(c) for c in caches]
|
||||
lengths = [c.size() for c in caches]
|
||||
max_length = max(lengths)
|
||||
padding = [max_length - l for l in lengths]
|
||||
B = len(caches)
|
||||
@@ -1185,8 +1286,10 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
keys = mx.zeros((B, H, max_length, Dk), dtype=dt)
|
||||
values = mx.zeros((B, H, max_length, Dv), dtype=dt)
|
||||
for i, (p, c) in enumerate(zip(padding, caches)):
|
||||
keys[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.keys)
|
||||
values[i : i + 1, :, p : p + c.offset] = c._temporal_order(c.values)
|
||||
if c.keys is None:
|
||||
continue
|
||||
keys[i : i + 1, :, p : p + c._idx] = c._temporal_order(c.keys)
|
||||
values[i : i + 1, :, p : p + c._idx] = c._temporal_order(c.values)
|
||||
|
||||
cache = cls(caches[0].max_size, padding)
|
||||
cache.keys = keys
|
||||
@@ -1196,3 +1299,6 @@ class BatchRotatingKVCache(_BaseCache):
|
||||
cache._offset = keys.shape[2]
|
||||
|
||||
return cache
|
||||
|
||||
def empty(self):
|
||||
return self.keys is None
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -109,7 +110,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Optional, Tuple
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
|
||||
@@ -106,7 +107,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -7,6 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import numpy as np
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -107,7 +108,7 @@ class MLP(nn.Module):
|
||||
self.w2 = nn.Linear(ffn_dim, d_model, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
current_hidden_states = nn.silu(self.w1(x)) * self.v1(x)
|
||||
current_hidden_states = swiglu(self.w1(x), self.v1(x))
|
||||
current_hidden_states = self.w2(current_hidden_states)
|
||||
return current_hidden_states
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -120,7 +121,7 @@ class DeepseekMLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
|
||||
@@ -6,7 +6,9 @@ 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
|
||||
@@ -259,7 +261,7 @@ class DeepseekV2MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -315,13 +317,21 @@ class DeepseekV2MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -395,7 +405,8 @@ class DeepseekV2Model(PipelineMixin, nn.Module):
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -429,6 +440,62 @@ class Model(nn.Module):
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
return weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
for layer in self.model.layers:
|
||||
# Shard the self attention
|
||||
if layer.self_attn.q_lora_rank is None:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.self_attn.q_b_proj = shard_linear(
|
||||
layer.self_attn.q_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.kv_b_proj = shard_linear(
|
||||
layer.self_attn.kv_b_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.num_heads //= N
|
||||
|
||||
# Shard the MLP
|
||||
if isinstance(layer.mlp, DeepseekV2MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
# Shard the MoE. Shard in place since the MoE should be responsible
|
||||
# for aggregating the results.
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.pipeline_layers
|
||||
|
||||
@@ -7,7 +7,9 @@ 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 .rope_utils import initialize_rope
|
||||
@@ -173,7 +175,7 @@ class DeepseekV3MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -256,13 +258,21 @@ class DeepseekV3MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -335,7 +345,8 @@ class DeepseekV3Model(PipelineMixin, nn.Module):
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -358,7 +369,8 @@ class Model(nn.Module):
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
@@ -419,6 +431,62 @@ 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()
|
||||
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, 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
|
||||
|
||||
@@ -6,7 +6,9 @@ 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 .rope_utils import initialize_rope
|
||||
@@ -222,6 +224,11 @@ class DeepseekV32Attention(nn.Module):
|
||||
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))
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache[0], scale=self.scale, mask=mask
|
||||
)
|
||||
@@ -245,7 +252,7 @@ class DeepseekV32MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -328,13 +335,21 @@ class DeepseekV32MoE(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
|
||||
|
||||
|
||||
@@ -428,10 +443,11 @@ class DeepseekV32Model(nn.Module):
|
||||
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)
|
||||
cache[-1][0].keys = mx.depends(cache[-1][0].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -454,7 +470,8 @@ class Model(nn.Module):
|
||||
|
||||
def sanitize(self, weights):
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
@@ -500,6 +517,56 @@ 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()
|
||||
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.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, 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]
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -180,7 +181,7 @@ class Dots1MLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Dots1MoE(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -87,7 +88,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
@@ -98,7 +99,7 @@ class Ernie4_5_MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=use_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Ernie4_5_MoeMLP(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -91,7 +92,7 @@ class MLP(nn.Module):
|
||||
self.c_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.c_proj(nn.silu(self.c_fc_0(x)) * self.c_fc_1(x))
|
||||
return self.c_proj(swiglu(self.c_fc_0(x), self.c_fc_1(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -102,7 +103,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
head_dim: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
num_shared_experts: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
sliding_window: int
|
||||
layer_types: List[str]
|
||||
is_moe_layer: List[bool]
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
routed_scaling_factor: float = 2.5
|
||||
norm_topk_prob: bool = True
|
||||
scoring_func: str = "sigmoid"
|
||||
topk_method: str = "noaux_tc"
|
||||
rope_theta: float = 1000000.0
|
||||
rope_scaling: Optional[dict] = None
|
||||
rope_parameters: Optional[dict] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rope_parameters is not None and "rope_theta" in self.rope_parameters:
|
||||
self.rope_theta = self.rope_parameters["rope_theta"]
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / (denominator + 1e-20)
|
||||
|
||||
scores = scores * routed_scaling_factor
|
||||
return inds, scores
|
||||
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.n_routed_experts = args.num_experts
|
||||
self.routed_scaling_factor = args.routed_scaling_factor
|
||||
self.n_group = args.n_group
|
||||
self.topk_group = args.topk_group
|
||||
self.weight = mx.zeros((self.n_routed_experts, args.hidden_size))
|
||||
self.e_score_correction_bias = mx.zeros((self.n_routed_experts,))
|
||||
assert args.topk_method == "noaux_tc", "Unsupported topk method."
|
||||
|
||||
def __call__(self, x):
|
||||
return group_expert_select(
|
||||
x @ self.weight.T,
|
||||
self.e_score_correction_bias,
|
||||
self.top_k,
|
||||
self.n_group,
|
||||
self.topk_group,
|
||||
self.routed_scaling_factor,
|
||||
self.norm_topk_prob,
|
||||
)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = intermediate_size or args.intermediate_size
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoE(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size,
|
||||
args.moe_intermediate_size,
|
||||
args.num_experts,
|
||||
)
|
||||
|
||||
self.gate = MoEGate(args)
|
||||
|
||||
self.shared_experts = (
|
||||
MLP(
|
||||
args,
|
||||
intermediate_size=args.moe_intermediate_size * args.num_shared_experts,
|
||||
)
|
||||
if args.num_shared_experts is not None and args.num_shared_experts > 0
|
||||
else None
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.n_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size, self.n_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.n_heads * self.head_dim, self.hidden_size, bias=False
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
self.is_sliding_window = args.layer_types[layer_idx] == "sliding_attention"
|
||||
self.apply_rope_all_layers = "sliding_attention" not in args.layer_types
|
||||
self.use_rope = self.is_sliding_window or self.apply_rope_all_layers
|
||||
|
||||
if self.use_rope:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_norm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
if self.use_rope:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
elif self.use_rope:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
|
||||
self.self_attn = Attention(args, layer_idx)
|
||||
self.mlp = MoE(args) if args.is_moe_layer[layer_idx] else MLP(args)
|
||||
self.is_sliding_window = self.self_attn.is_sliding_window
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
h = x + r
|
||||
r = self.mlp(self.post_attention_layernorm(h))
|
||||
return h + r
|
||||
|
||||
|
||||
class ExaoneMoEModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [DecoderLayer(args, idx) for idx in range(args.num_hidden_layers)]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.swa_idx = None
|
||||
self.ga_idx = None
|
||||
for i, layer in enumerate(self.layers):
|
||||
if layer.is_sliding_window and self.swa_idx is None:
|
||||
self.swa_idx = i
|
||||
if not layer.is_sliding_window and self.ga_idx is None:
|
||||
self.ga_idx = i
|
||||
|
||||
self.window_size = args.sliding_window
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
global_mask = create_attention_mask(
|
||||
h, cache[self.ga_idx] if self.ga_idx is not None else cache[0]
|
||||
)
|
||||
swa_mask = create_attention_mask(
|
||||
h,
|
||||
cache[self.swa_idx] if self.swa_idx is not None else cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.is_sliding_window else global_mask
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = ExaoneMoEModel(args)
|
||||
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
new_weights = {k: v for k, v in weights.items() if not k.startswith("mtp.")}
|
||||
weights = new_weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
if not self.args.is_moe_layer[l]:
|
||||
continue
|
||||
|
||||
prefix = f"model.layers.{l}"
|
||||
|
||||
bias_key = f"{prefix}.mlp.e_score_correction_bias"
|
||||
if bias_key in weights:
|
||||
weights[f"{prefix}.mlp.gate.e_score_correction_bias"] = weights.pop(
|
||||
bias_key
|
||||
)
|
||||
|
||||
for m in ["gate_proj", "down_proj", "up_proj"]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
first_key = f"{prefix}.mlp.experts.0.{m}.{k}"
|
||||
last_key = (
|
||||
f"{prefix}.mlp.experts.{self.args.num_experts - 1}.{m}.{k}"
|
||||
)
|
||||
if first_key in weights and last_key in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.layers:
|
||||
if layer.is_sliding_window:
|
||||
caches.append(
|
||||
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
|
||||
)
|
||||
else:
|
||||
caches.append(KVCache())
|
||||
return caches
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
if isinstance(layer.mlp, MLP):
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
else:
|
||||
layer.mlp.sharding_group = group
|
||||
if layer.mlp.shared_experts is not None:
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.gate_proj,
|
||||
"all-to-sharded",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.down_proj,
|
||||
"sharded-to-all",
|
||||
group=group,
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
shard_inplace(
|
||||
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
+56
-31
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -69,6 +70,7 @@ class ModelArgs(BaseModelArgs):
|
||||
)
|
||||
ssm_out_multiplier: float = 0.23570226039551587
|
||||
vocab_size: int = 32784
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class FalconH1RMSNormGated(nn.Module):
|
||||
@@ -81,14 +83,14 @@ class FalconH1RMSNormGated(nn.Module):
|
||||
|
||||
def __call__(self, hidden_states, gate=None):
|
||||
if not self.norm_before_gate and gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
hidden_states = 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 = hidden_states * nn.silu(gate)
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
||||
@@ -231,21 +233,36 @@ class FalconH1Mixer(nn.Module):
|
||||
self.intermediate_size, self.hidden_size, bias=args.projectors_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[MambaCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if cache is None or cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
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)
|
||||
@@ -256,17 +273,20 @@ class FalconH1Mixer(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache],
|
||||
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,
|
||||
@@ -278,9 +298,11 @@ class FalconH1Mixer(nn.Module):
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
lengths,
|
||||
)
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
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)
|
||||
@@ -291,11 +313,9 @@ class FalconH1Mixer(nn.Module):
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
@@ -303,15 +323,15 @@ class FalconH1Mixer(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
|
||||
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
|
||||
if self.mamba_rms_norm:
|
||||
y = self.norm(y, gate)
|
||||
else:
|
||||
y = y * nn.silu(gate)
|
||||
y = swiglu(gate, y)
|
||||
|
||||
return self.out_proj(y)
|
||||
|
||||
@@ -329,7 +349,7 @@ class FalconH1MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x):
|
||||
y = self.up_proj(x) * nn.silu(self.gate_proj(x))
|
||||
y = swiglu(self.gate_proj(x), self.up_proj(x))
|
||||
y = self.down_proj(y)
|
||||
return y
|
||||
|
||||
@@ -425,11 +445,16 @@ class Model(nn.Module):
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = FalconH1Model(args=args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
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)
|
||||
return self.lm_head(hidden_states)
|
||||
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
|
||||
|
||||
@@ -161,11 +161,9 @@ def _gated_delta_step_ops(
|
||||
state = state + k[..., None, :] * delta[..., None]
|
||||
# Output projection along key dim with q
|
||||
y = (state * q[..., None, :]).sum(axis=-1) # [B, H, Dv]
|
||||
|
||||
if mask is not None:
|
||||
if mask.ndim == 2:
|
||||
mask = mx.expand_dims(mask, axes=(2, 3))
|
||||
elif mask.ndim == 3:
|
||||
mask = mx.expand_dims(mask, axis=-1)
|
||||
mask = mx.expand_dims(mask, axis=(1, 2, 3))
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y, state
|
||||
|
||||
|
||||
@@ -54,13 +54,20 @@ class Attention(nn.Module):
|
||||
self.k_norm = RMSNorm(dims=head_dim, eps=args.rms_norm_eps)
|
||||
self.is_sliding = (layer_idx + 1) % args.sliding_window_pattern != 0
|
||||
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=(args.rope_local_base_freq if self.is_sliding else args.rope_theta),
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
if self.is_sliding:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_local_base_freq,
|
||||
traditional=False,
|
||||
)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
dims=head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
scaling_config=args.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -102,7 +103,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(nn.silu(gate) * x)
|
||||
return self.down_proj(swiglu(gate, x))
|
||||
|
||||
|
||||
class GLMBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class Glm4MLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
x = self.gate_up_proj(x)
|
||||
gate, up_states = mx.split(x, 2, axis=-1)
|
||||
return self.down_proj(nn.silu(gate) * up_states)
|
||||
return self.down_proj(swiglu(gate, up_states))
|
||||
|
||||
|
||||
class Glm4Attention(nn.Module):
|
||||
|
||||
@@ -7,7 +7,9 @@ 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
|
||||
@@ -122,7 +124,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x):
|
||||
down_proj = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
return down_proj
|
||||
|
||||
|
||||
@@ -205,13 +207,21 @@ class MoE(nn.Module):
|
||||
config=config, intermediate_size=intermediate_size
|
||||
)
|
||||
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x):
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
inds, scores = self.gate(x)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype)
|
||||
if self.config.n_shared_experts is not None:
|
||||
y = y + self.shared_experts(x)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
@@ -252,10 +262,6 @@ 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__(
|
||||
@@ -286,7 +292,8 @@ class LanguageModel(PipelineMixin, nn.Module):
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
if pipeline_size > 1:
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -329,6 +336,61 @@ 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
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
# 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 .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
|
||||
rope_traditional: bool = True
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
if self.config.rope_scaling is not None:
|
||||
mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = self.config.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=self.config.rope_traditional,
|
||||
max_position_embeddings=self.max_position_embeddings,
|
||||
scaling_config=self.config.rope_scaling,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
q = self.q_proj(x)
|
||||
else:
|
||||
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
||||
|
||||
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
||||
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
||||
compressed_kv = self.kv_a_proj_with_mqa(x)
|
||||
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
||||
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
||||
kv = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
|
||||
kv = kv.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if cache is not None:
|
||||
q_pe = self.rope(q_pe, cache.offset)
|
||||
k_pe = self.rope(k_pe, cache.offset)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys, values = cache.update_and_fetch(
|
||||
mx.concatenate([k_nope, k_pe], axis=-1), values
|
||||
)
|
||||
else:
|
||||
q_pe = self.rope(q_pe)
|
||||
k_pe = self.rope(k_pe)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys = mx.concatenate([k_nope, k_pe], axis=-1)
|
||||
|
||||
queries = mx.concatenate([q_nope, q_pe], axis=-1)
|
||||
|
||||
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 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
|
||||
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)
|
||||
self.mlp = (
|
||||
Glm4MoeLiteMoE(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 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])
|
||||
|
||||
# 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):
|
||||
# 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)
|
||||
|
||||
num_mpt_layers = getattr(self.args, "num_nextn_predict_layers", 0) or 0
|
||||
if num_mpt_layers:
|
||||
|
||||
def _is_mpt_layer(key: str) -> bool:
|
||||
for idx in range(num_mpt_layers):
|
||||
if key.startswith(
|
||||
f"model.layers.{self.args.num_hidden_layers + idx}"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
weights = {k: v for k, v in weights.items() if not _is_mpt_layer(k)}
|
||||
|
||||
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, 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
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -142,8 +143,12 @@ class MLPBlock(nn.Module):
|
||||
bias=True,
|
||||
)
|
||||
self.router = nn.Linear(config.hidden_size, config.num_local_experts, bias=True)
|
||||
self.sharding_group = None
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
|
||||
g = self.router(x)
|
||||
experts, indices = mlx_topk(g, k=self.num_experts_per_tok, axis=-1)
|
||||
expert_weights = mx.softmax(experts, axis=-1, precise=True)
|
||||
@@ -152,7 +157,13 @@ class MLPBlock(nn.Module):
|
||||
x = self.experts(x, indices)
|
||||
|
||||
x = x * mx.expand_dims(expert_weights, axis=-1)
|
||||
return x.sum(axis=-2)
|
||||
|
||||
y = x.sum(axis=-2)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -268,6 +279,47 @@ class Model(nn.Module):
|
||||
|
||||
return new_weights
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
R = group.rank()
|
||||
|
||||
for layer in self.model.layers:
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, sharding="sharded-to-all", group=group
|
||||
)
|
||||
|
||||
layer.self_attn.num_attention_heads //= N
|
||||
layer.self_attn.num_key_value_heads //= N
|
||||
layer.self_attn.num_key_value_groups = (
|
||||
layer.self_attn.num_attention_heads
|
||||
// layer.self_attn.num_key_value_heads
|
||||
)
|
||||
|
||||
layer.self_attn.sinks = layer.self_attn.sinks[
|
||||
layer.self_attn.num_attention_heads
|
||||
* R : layer.self_attn.num_attention_heads
|
||||
* (R + 1)
|
||||
]
|
||||
|
||||
shard_inplace(layer.mlp.experts.gate_proj, "all-to-sharded", group=group)
|
||||
shard_inplace(layer.mlp.experts.down_proj, "sharded-to-all", group=group)
|
||||
layer.mlp.experts.down_proj.bias /= N
|
||||
shard_inplace(
|
||||
layer.mlp.experts.up_proj, sharding="all-to-sharded", group=group
|
||||
)
|
||||
|
||||
layer.mlp.sharding_group = group
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -104,7 +105,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -75,7 +76,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 = hidden_states * nn.silu(gate)
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
@@ -119,21 +120,36 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
self.intermediate_size, self.hidden_size, bias=args.mamba_proj_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[MambaCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
if cache is None or cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
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)
|
||||
@@ -144,8 +160,8 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
@@ -154,26 +170,33 @@ 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,
|
||||
self.D.astype(hidden_states.dtype),
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array],
|
||||
cache: Optional[MambaCache] = None,
|
||||
) -> mx.array:
|
||||
|
||||
@@ -184,11 +207,7 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
@@ -197,10 +216,9 @@ class GraniteMoeHybridMamba2Mixer(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
y = self._ssm(hidden_states_ssm, B, C, dt, cache, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
@@ -320,7 +338,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(nn.silu(gate) * up)
|
||||
return self.output_linear(swiglu(gate, up))
|
||||
|
||||
|
||||
class GraniteMoeHybridMLP(nn.Module):
|
||||
@@ -335,7 +353,7 @@ class GraniteMoeHybridMLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class GraniteMoeHybridLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -92,7 +93,7 @@ class HeliumMLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class HeliumDecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Tuple, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -148,7 +149,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Gate(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -144,7 +145,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -156,7 +157,7 @@ class MLP(nn.Module):
|
||||
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
||||
return self.w2(swiglu(self.w1(x), self.w3(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -154,7 +155,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _compute_gate(query: mx.array, weight: mx.array, bias: mx.array) -> mx.array:
|
||||
gate_logits = query @ weight[:, None, :].swapaxes(-1, -2)
|
||||
gate_logits = gate_logits + bias[..., None, None]
|
||||
return mx.sigmoid(gate_logits)
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _silu_mul(gate: mx.array, up: mx.array) -> mx.array:
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def _mix_attention(
|
||||
gate: mx.array, attn_global: mx.array, attn_local: mx.array
|
||||
) -> mx.array:
|
||||
return gate * attn_global + (1 - gate) * attn_local
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
head_dim: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int = 131072
|
||||
attention_bias: bool = False
|
||||
mlp_bias: bool = False
|
||||
rope_theta: float = 500000.0
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
loop_num: int = 2
|
||||
loop_window_size: int = 64
|
||||
|
||||
|
||||
class LoopGateProjection(nn.Module):
|
||||
def __init__(self, num_heads: int, head_dim: int):
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = head_dim
|
||||
self.weight = mx.zeros((num_heads, head_dim))
|
||||
self.bias = mx.zeros((num_heads,))
|
||||
|
||||
def __call__(self, query: mx.array) -> mx.array:
|
||||
return _compute_gate(query, self.weight, self.bias)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = head_dim = args.head_dim
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=args.attention_bias)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=args.attention_bias)
|
||||
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=args.attention_bias)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
head_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def get_qkv(
|
||||
self, x: mx.array, offset: int = 0
|
||||
) -> Tuple[mx.array, mx.array, mx.array]:
|
||||
B, L, _ = x.shape
|
||||
queries = self.q_proj(x).reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = self.k_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = self.v_proj(x).reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
queries = self.rope(queries, offset=offset)
|
||||
keys = self.rope(keys, offset=offset)
|
||||
|
||||
return queries, keys, values
|
||||
|
||||
def attention(
|
||||
self,
|
||||
queries: mx.array,
|
||||
keys: mx.array,
|
||||
values: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
return scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(_silu_mul(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
|
||||
|
||||
class IQuestLoopCoderModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
assert args.loop_num == 2, f"Only loop_num=2 is supported, got {args.loop_num}"
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.gate_projections = [
|
||||
LoopGateProjection(args.num_attention_heads, args.head_dim)
|
||||
for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.loop_num = args.loop_num
|
||||
self.loop_window_size = args.loop_window_size
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
):
|
||||
B, L = inputs.shape[:2]
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * (2 * len(self.layers))
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
window_mask = create_attention_mask(
|
||||
h, cache[len(self.layers)], window_size=self.loop_window_size
|
||||
)
|
||||
|
||||
loop1_kv = []
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q1, k1, v1 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
|
||||
if c is not None:
|
||||
k1, v1 = c.update_and_fetch(k1, v1)
|
||||
loop1_kv.append((k1, v1))
|
||||
|
||||
out = layer.self_attn.attention(q1, k1, v1, mask, cache=c)
|
||||
r = layer.self_attn.o_proj(out.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
for layer, gate_proj, c, (k1, v1) in zip(
|
||||
self.layers, self.gate_projections, cache[len(self.layers) :], loop1_kv
|
||||
):
|
||||
h_norm = layer.input_layernorm(h)
|
||||
offset = c.offset if c is not None else 0
|
||||
q2, k2, v2 = layer.self_attn.get_qkv(h_norm, offset)
|
||||
gate = gate_proj(q2)
|
||||
attn_global = layer.self_attn.attention(q2, k1, v1, mask, cache=c)
|
||||
|
||||
if c is not None:
|
||||
k2, v2 = c.update_and_fetch(k2, v2)
|
||||
attn_local = layer.self_attn.attention(
|
||||
q2,
|
||||
k2,
|
||||
v2,
|
||||
window_mask,
|
||||
cache=c,
|
||||
)
|
||||
|
||||
mixed = _mix_attention(gate, attn_global, attn_local)
|
||||
r = layer.self_attn.o_proj(mixed.transpose(0, 2, 1, 3).reshape(B, L, -1))
|
||||
h = h + r
|
||||
r = layer.mlp(layer.post_attention_layernorm(h))
|
||||
h = h + r
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = IQuestLoopCoderModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def shard(self, group: Optional[mx.distributed.Group] = None):
|
||||
group = group or mx.distributed.init()
|
||||
N = group.size()
|
||||
rank = group.rank()
|
||||
|
||||
for i, layer in enumerate(self.model.layers):
|
||||
layer.self_attn.q_proj = shard_linear(
|
||||
layer.self_attn.q_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.k_proj = shard_linear(
|
||||
layer.self_attn.k_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.v_proj = shard_linear(
|
||||
layer.self_attn.v_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.self_attn.o_proj = shard_linear(
|
||||
layer.self_attn.o_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.self_attn.n_heads //= N
|
||||
layer.self_attn.n_kv_heads //= N
|
||||
|
||||
layer.mlp.gate_proj = shard_linear(
|
||||
layer.mlp.gate_proj, "all-to-sharded", group=group
|
||||
)
|
||||
layer.mlp.down_proj = shard_linear(
|
||||
layer.mlp.down_proj, "sharded-to-all", group=group
|
||||
)
|
||||
layer.mlp.up_proj = shard_linear(
|
||||
layer.mlp.up_proj, "all-to-sharded", group=group
|
||||
)
|
||||
|
||||
gate_proj = self.model.gate_projections[i]
|
||||
heads_per_rank = gate_proj.num_heads // N
|
||||
start = rank * heads_per_rank
|
||||
end = start + heads_per_rank
|
||||
gate_proj.weight = gate_proj.weight[start:end, :]
|
||||
gate_proj.bias = gate_proj.bias[start:end]
|
||||
gate_proj.num_heads = heads_per_rank
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [KVCache() for _ in self.layers] + [
|
||||
RotatingKVCache(max_size=self.args.loop_window_size) for _ in self.layers
|
||||
]
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -65,7 +66,7 @@ class JambaMLP(nn.Module):
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class JambaAttention(nn.Module):
|
||||
@@ -205,7 +206,7 @@ class JambaMambaMixer(nn.Module):
|
||||
x = nn.silu(conv_out)
|
||||
A = -mx.exp(self.A_log)
|
||||
y, ssm_state = self.ssm_step(x, A, ssm_state)
|
||||
z = self.out_proj(nn.silu(z) * y)
|
||||
z = self.out_proj(swiglu(z, y))
|
||||
return z, (conv_state, ssm_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -68,7 +69,7 @@ class KimiMLP(nn.Module):
|
||||
self.down_proj = nn.Linear(hidden, dim, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
@mx.compile
|
||||
@@ -259,18 +260,30 @@ class ShortConv1d(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self, x: mx.array, cache: Optional[mx.array]
|
||||
self,
|
||||
x: mx.array,
|
||||
state: Optional[mx.array],
|
||||
mask: Optional[mx.array],
|
||||
lengths: Optional[mx.array],
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
if cache is None:
|
||||
pad = mx.zeros(
|
||||
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
|
||||
)
|
||||
else:
|
||||
pad = cache
|
||||
conv_input = mx.concatenate([pad, x], axis=1)
|
||||
conv_input = mx.concatenate([state, x], axis=1)
|
||||
out = nn.silu(self.conv(conv_input))
|
||||
new_cache = conv_input[:, -self.kernel_size + 1 :, :]
|
||||
return out, new_cache
|
||||
n_keep = self.kernel_size - 1
|
||||
if lengths is not None:
|
||||
ends = mx.clip(cache.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 = conv_input[:, -n_keep:, :]
|
||||
|
||||
return out, new_state
|
||||
|
||||
|
||||
class KimiDeltaAttention(nn.Module):
|
||||
@@ -323,9 +336,11 @@ class KimiDeltaAttention(nn.Module):
|
||||
|
||||
if cache is not None:
|
||||
conv_state, ssm_state = cache
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
conv_state = None
|
||||
ssm_state = None
|
||||
lengths = None
|
||||
|
||||
if conv_state is None:
|
||||
s = mx.zeros((B, self.conv_kernel - 1, self.projection_dim), dtype=dtype)
|
||||
@@ -335,9 +350,9 @@ class KimiDeltaAttention(nn.Module):
|
||||
else:
|
||||
q_state, k_state, v_state = conv_state
|
||||
|
||||
q_conv, q_state = self.q_conv(self.q_proj(x), q_state)
|
||||
k_conv, k_state = self.k_conv(self.k_proj(x), k_state)
|
||||
v_conv, v_state = self.v_conv(self.v_proj(x), v_state)
|
||||
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, k_state, v_state)
|
||||
@@ -374,6 +389,7 @@ class KimiDeltaAttention(nn.Module):
|
||||
|
||||
if cache is not None:
|
||||
cache[1] = ssm_state
|
||||
cache.advance(T)
|
||||
|
||||
gate = self.g_b_proj(self.g_a_proj(x)).reshape(
|
||||
B, T, self.num_heads, self.head_dim
|
||||
|
||||
+22
-10
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -138,17 +139,28 @@ class ShortConv(nn.Module):
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
state = None
|
||||
if cache is not None:
|
||||
state = cache[0]
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
|
||||
)
|
||||
|
||||
Bx = mx.concatenate([state, Bx], axis=-2)
|
||||
if cache is not None:
|
||||
cache[0] = Bx[:, -(self.L_cache - 1) :]
|
||||
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
|
||||
@@ -176,7 +188,7 @@ class MLP(nn.Module):
|
||||
self.w2 = nn.Linear(ff_dim, dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
|
||||
return self.w2(swiglu(self.w1(x), self.w3(x)))
|
||||
|
||||
|
||||
class Lfm2DecoderLayer(nn.Module):
|
||||
|
||||
+22
-10
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -139,17 +140,28 @@ class ShortConv(nn.Module):
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
state = None
|
||||
if cache is not None:
|
||||
state = cache[0]
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
|
||||
)
|
||||
|
||||
Bx = mx.concatenate([state, Bx], axis=-2)
|
||||
if cache is not None:
|
||||
cache[0] = Bx[:, -(self.L_cache - 1) :]
|
||||
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
|
||||
@@ -168,7 +180,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Lfm2MoeSparseMoeBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -87,7 +88,7 @@ class Lille130mMLP(nn.Module):
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
h = self.norm(x)
|
||||
return self.down_proj(nn.silu(self.gate_proj(h)) * self.up_proj(h))
|
||||
return self.down_proj(swiglu(self.gate_proj(h), self.up_proj(h)))
|
||||
|
||||
|
||||
class Lille130Block(nn.Module):
|
||||
|
||||
+34
-1
@@ -5,7 +5,9 @@ from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.nn.layers.distributed import shard_linear
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -116,7 +118,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -226,6 +228,37 @@ 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
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import ChunkedKVCache, KVCache
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -145,7 +146,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class MoE(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -95,7 +96,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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -4,9 +4,12 @@ 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 .rope_utils import initialize_rope
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@@ -38,6 +41,7 @@ class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool
|
||||
norm_topk_prob: bool = False
|
||||
router_bias: bool = False
|
||||
rope_scaling: Optional[Dict] = None
|
||||
|
||||
|
||||
class LongcatFlashMLA(nn.Module):
|
||||
@@ -93,8 +97,20 @@ class LongcatFlashMLA(nn.Module):
|
||||
if args.mla_scale_kv_lora:
|
||||
self.mla_scale_kv_lora = (args.hidden_size / self.kv_lora_rank) ** 0.5
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
dims=self.qk_rope_head_dim, base=args.rope_theta, traditional=True
|
||||
if args.rope_scaling is not None:
|
||||
mscale_all_dim = args.rope_scaling.get("mscale_all_dim", 0)
|
||||
if mscale_all_dim:
|
||||
scaling_factor = args.rope_scaling["factor"]
|
||||
if scaling_factor > 1:
|
||||
s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0
|
||||
self.scale = self.scale * s * s
|
||||
|
||||
self.rope = initialize_rope(
|
||||
dims=self.qk_rope_head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=True,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
@@ -168,7 +184,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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class LongcatFlashTopkRouter(nn.Module):
|
||||
@@ -223,8 +239,11 @@ 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)
|
||||
|
||||
@@ -236,14 +255,20 @@ class LongcatFlashMoE(nn.Module):
|
||||
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
|
||||
|
||||
weighted_outputs = regular_outputs * regular_weights[..., None]
|
||||
|
||||
# Add identity expert contribution if needed
|
||||
assert self.zero_expert_type == "identity"
|
||||
identity_weights = mx.where(mask, topk_weights, 0.0)
|
||||
identity_outputs = hidden_states[..., None, :] * identity_weights[..., None]
|
||||
weighted_outputs = weighted_outputs + identity_outputs
|
||||
|
||||
final_output = mx.sum(weighted_outputs, axis=-2)
|
||||
|
||||
if self.sharding_group is not None:
|
||||
final_output = mx.distributed.all_sum(
|
||||
final_output, group=self.sharding_group
|
||||
)
|
||||
|
||||
# Add identity expert contribution after all_sum to avoid summing it N times
|
||||
assert self.zero_expert_type == "identity"
|
||||
identity_weights_sum = mx.sum(
|
||||
mx.where(mask, topk_weights, 0.0), axis=-1, keepdims=True
|
||||
)
|
||||
final_output = final_output + hidden_states * identity_weights_sum
|
||||
|
||||
return final_output
|
||||
|
||||
|
||||
@@ -379,3 +404,37 @@ 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()
|
||||
|
||||
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.kv_b_proj = shard_linear(
|
||||
attn.kv_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
|
||||
|
||||
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)
|
||||
|
||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs
|
||||
from .cache import MambaCache
|
||||
|
||||
@@ -139,7 +140,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(nn.silu(z) * y)
|
||||
z = self.out_proj(swiglu(z, y))
|
||||
return z, (new_conv_cache, current_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
|
||||
+32
-13
@@ -7,6 +7,7 @@ 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 MambaCache
|
||||
from .ssm import ssm_update
|
||||
@@ -48,7 +49,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 = hidden_states * nn.silu(gate)
|
||||
hidden_states = swiglu(gate, hidden_states)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
|
||||
|
||||
@@ -93,9 +94,15 @@ class Mamba2Block(nn.Module):
|
||||
self.intermediate_size, self.hidden_size, bias=args.use_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[MambaCache],
|
||||
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(
|
||||
@@ -105,7 +112,14 @@ class Mamba2Block(nn.Module):
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 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)]
|
||||
@@ -120,8 +134,8 @@ class Mamba2Block(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
hidden_states = hidden_states.reshape(
|
||||
@@ -129,6 +143,11 @@ 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,
|
||||
@@ -140,8 +159,11 @@ class Mamba2Block(nn.Module):
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
lengths,
|
||||
)
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
if cache:
|
||||
cache[1] = state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -155,9 +177,7 @@ class Mamba2Block(nn.Module):
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
@@ -166,10 +186,9 @@ class Mamba2Block(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states, B, C, dt, state, mask=mask)
|
||||
y = self._ssm(hidden_states, B, C, dt, cache, mask=mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -90,7 +91,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
# 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
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -38,7 +39,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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import SuScaledRoPE
|
||||
|
||||
@@ -156,7 +157,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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -118,8 +119,12 @@ class MiniMaxSparseMoeBlock(nn.Module):
|
||||
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)
|
||||
@@ -135,6 +140,10 @@ class MiniMaxSparseMoeBlock(nn.Module):
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -218,7 +227,8 @@ class Model(nn.Module):
|
||||
"""Dequantize FP8 weights and restructure MoE experts."""
|
||||
|
||||
def dequant(weight, scale_inv):
|
||||
dtype = weight.dtype
|
||||
dtype = mx.bfloat16
|
||||
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
|
||||
bs = 128 # block size
|
||||
m, n = weight.shape
|
||||
pad_bottom = (-m) % bs
|
||||
@@ -266,6 +276,53 @@ class Model(nn.Module):
|
||||
|
||||
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.weight = layer.self_attn.q_norm.weight.split(
|
||||
N, axis=-1
|
||||
)[rank]
|
||||
layer.self_attn.k_norm.weight = layer.self_attn.k_norm.weight.split(
|
||||
N, axis=-1
|
||||
)[rank]
|
||||
|
||||
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
|
||||
|
||||
+83
-14
@@ -5,9 +5,12 @@ 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
|
||||
|
||||
|
||||
@@ -36,13 +39,17 @@ class ModelArgs(BaseModelArgs):
|
||||
self.layer_types = ["full_attention"] * self.num_hidden_layers
|
||||
|
||||
|
||||
def _get_llama_4_attn_scale(
|
||||
start: int, stop: int, beta: float, max_position_embeddings: int
|
||||
):
|
||||
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(start, stop) / max_position_embeddings)
|
||||
1 + mx.floor((mx.arange(size) + offset) / max_position_embeddings)
|
||||
)
|
||||
return scaling[:, None]
|
||||
if scaling.ndim == 2:
|
||||
return scaling[:, None, :, None]
|
||||
else:
|
||||
return scaling[:, None]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
@@ -115,7 +122,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -146,7 +153,7 @@ class TransformerBlock(nn.Module):
|
||||
return out
|
||||
|
||||
|
||||
class LanguageModel(nn.Module):
|
||||
class LanguageModel(PipelineMixin, nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
@@ -167,6 +174,18 @@ class LanguageModel(nn.Module):
|
||||
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,
|
||||
@@ -178,28 +197,47 @@ class LanguageModel(nn.Module):
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
pipeline_rank = self.pipeline_rank
|
||||
pipeline_size = self.pipeline_size
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
cache = [None] * len(self.pipeline_layers)
|
||||
offset = 0
|
||||
else:
|
||||
offset = cache[0].offset
|
||||
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
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,
|
||||
offset + inputs.shape[1],
|
||||
self.args.rope_parameters["llama_4_scaling_beta"],
|
||||
self.args.rope_parameters["original_max_position_embeddings"],
|
||||
).astype(h.dtype)
|
||||
|
||||
for layer, cache in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, attn_scale, mask, cache=cache)
|
||||
# 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)
|
||||
|
||||
@@ -249,9 +287,40 @@ class Model(nn.Module):
|
||||
|
||||
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
|
||||
return self.model.pipeline_layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@@ -329,6 +330,9 @@ class NemotronNASModel(nn.Module):
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.num_attn_layers = sum(
|
||||
1 for layer in self.layers if layer.self_attn is not None
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -338,11 +342,17 @@ class NemotronNASModel(nn.Module):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
cache = [None] * self.num_attn_layers
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
cache_idx = 0
|
||||
for layer in self.layers:
|
||||
if layer.self_attn is not None:
|
||||
c = cache[cache_idx]
|
||||
cache_idx += 1
|
||||
else:
|
||||
c = None
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
@@ -380,3 +390,6 @@ class Model(nn.Module):
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [KVCache() for layer in self.layers if layer.self_attn is not None]
|
||||
|
||||
+169
-28
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -15,6 +16,7 @@ from .base import (
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .ssm import ssm_update
|
||||
from .switch_layers import SwitchMLP
|
||||
|
||||
|
||||
@dataclass()
|
||||
@@ -37,24 +39,34 @@ class ModelArgs(BaseModelArgs):
|
||||
time_step_limit: Tuple[float, float]
|
||||
mlp_bias: bool
|
||||
layer_norm_epsilon: float
|
||||
rms_norm_eps: float
|
||||
use_bias: bool
|
||||
use_conv_bias: bool
|
||||
residual_in_fp32: bool
|
||||
hybrid_override_pattern: List[str]
|
||||
head_dim: Optional[int] = None
|
||||
moe_intermediate_size: Optional[int] = None
|
||||
moe_shared_expert_intermediate_size: Optional[int] = None
|
||||
n_group: Optional[int] = None
|
||||
n_routed_experts: Optional[int] = None
|
||||
n_shared_experts: Optional[int] = None
|
||||
topk_group: Optional[int] = None
|
||||
num_experts_per_tok: Optional[int] = None
|
||||
norm_topk_prob: Optional[bool] = None
|
||||
routed_scaling_factor: Optional[float] = None
|
||||
|
||||
|
||||
class MambaRMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
def __init__(self, hidden_size: int, eps: float, group_size: int):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = mx.ones(hidden_size)
|
||||
self.group_size = group_size
|
||||
|
||||
def __call__(self, hidden_states: mx.array, gate: mx.array = None) -> mx.array:
|
||||
def __call__(self, x: mx.array, gate: mx.array = None) -> mx.array:
|
||||
if gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
return mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
x = swiglu(gate, x)
|
||||
x = mx.unflatten(x, axis=-1, shape=(-1, self.group_size))
|
||||
x = mx.fast.rms_norm(x, weight=None, eps=self.eps)
|
||||
return self.weight * x.flatten(-2)
|
||||
|
||||
|
||||
class NemotronHMamba2Mixer(nn.Module):
|
||||
@@ -90,16 +102,25 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
self.A_log = mx.log(mx.arange(1, self.num_heads + 1, dtype=mx.float32))
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
group_size = self.intermediate_size // self.n_groups
|
||||
self.norm = MambaRMSNormGated(
|
||||
self.intermediate_size, eps=args.layer_norm_epsilon
|
||||
self.intermediate_size,
|
||||
eps=args.layer_norm_epsilon,
|
||||
group_size=group_size,
|
||||
)
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.mamba_proj_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[MambaCache],
|
||||
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(
|
||||
@@ -109,11 +130,19 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 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)
|
||||
|
||||
@@ -123,8 +152,8 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array],
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[MambaCache],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
@@ -133,21 +162,28 @@ class NemotronHMamba2Mixer(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,
|
||||
self.D.astype(hidden_states.dtype),
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -163,11 +199,7 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
|
||||
conv_output = self._conv(conv_input, cache, mask)
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
@@ -176,10 +208,9 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
y = self._ssm(hidden_states_ssm, B, C, dt, cache, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
cache.advance(y.shape[1])
|
||||
y = self.norm(y, gate)
|
||||
return self.out_proj(y)
|
||||
|
||||
@@ -245,24 +276,113 @@ class NemotronHAttention(nn.Module):
|
||||
|
||||
|
||||
class NemotronHMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
def __init__(self, args: ModelArgs, intermediate_size=None):
|
||||
super().__init__()
|
||||
intermediate_size = intermediate_size or args.intermediate_size
|
||||
|
||||
self.up_proj = nn.Linear(
|
||||
args.hidden_size, args.intermediate_size, bias=args.mlp_bias
|
||||
args.hidden_size, intermediate_size, bias=args.mlp_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
args.intermediate_size, args.hidden_size, bias=args.mlp_bias
|
||||
intermediate_size, args.hidden_size, bias=args.mlp_bias
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
return self.down_proj(nn.relu2(self.up_proj(x)))
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
orig_scores = scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
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,))
|
||||
|
||||
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 NemotronHMoE(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 = SwitchMLP(
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.n_routed_experts,
|
||||
activation=nn.ReLU2(),
|
||||
)
|
||||
|
||||
self.gate = MoEGate(config)
|
||||
if config.n_shared_experts is not None:
|
||||
intermediate_size = config.moe_shared_expert_intermediate_size
|
||||
self.shared_experts = NemotronHMLP(
|
||||
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 NemotronHBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs, block_type: str):
|
||||
super().__init__()
|
||||
self.residual_in_fp32 = args.residual_in_fp32
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.layer_norm_epsilon)
|
||||
|
||||
self.block_type = block_type
|
||||
|
||||
@@ -272,6 +392,8 @@ class NemotronHBlock(nn.Module):
|
||||
self.mixer = NemotronHAttention(args)
|
||||
elif self.block_type == "-":
|
||||
self.mixer = NemotronHMLP(args)
|
||||
elif self.block_type == "E":
|
||||
self.mixer = NemotronHMoE(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -296,7 +418,7 @@ class NemotronHModel(nn.Module):
|
||||
NemotronHBlock(args, block_type)
|
||||
for block_type in args.hybrid_override_pattern
|
||||
]
|
||||
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.norm_f = nn.RMSNorm(args.hidden_size, eps=args.layer_norm_epsilon)
|
||||
self.fa_idx = 0
|
||||
self.ssm_idx = 0
|
||||
for b in args.hybrid_override_pattern:
|
||||
@@ -372,4 +494,23 @@ class Model(nn.Module):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
# Stack experts
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"backbone.layers.{l}.mixer"
|
||||
for m, n in [("down_proj", "fc2"), ("up_proj", "fc1")]:
|
||||
if f"{prefix}.experts.0.{m}.weight" in weights:
|
||||
to_join = [
|
||||
weights.pop(f"{prefix}.experts.{e}.{m}.weight")
|
||||
for e in range(self.args.n_routed_experts)
|
||||
]
|
||||
weights[f"{prefix}.switch_mlp.{n}.weight"] = mx.stack(to_join)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "e_score_correction_bias" not in k and "A_log" not in k
|
||||
|
||||
return predicate
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Optional
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask
|
||||
|
||||
try:
|
||||
@@ -105,7 +106,7 @@ class TransformerBlock(nn.Module):
|
||||
|
||||
x1, x2 = mx.split(self.ff_proj(self.ff_norm(h)), 2, axis=-1)
|
||||
|
||||
out = h + self.ff_out(nn.silu(x2) * x1)
|
||||
out = h + self.ff_out(swiglu(x2, x1))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -115,7 +116,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -131,7 +132,7 @@ class Olmo3MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Olmo3DecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
|
||||
|
||||
@@ -136,7 +137,7 @@ class MLP(nn.Module):
|
||||
def __call__(self, x) -> mx.array:
|
||||
x = self.proj_1(x)
|
||||
gate, x = mx.split(x, 2, axis=-1)
|
||||
return self.proj_2(nn.silu(gate) * x)
|
||||
return self.proj_2(swiglu(gate, x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, 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 .rope_utils import SuScaledRoPE
|
||||
|
||||
@@ -126,7 +127,7 @@ class MLP(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(nn.silu(gate) * x)
|
||||
return self.down_proj(swiglu(gate, x))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -7,6 +7,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import numpy as np
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@@ -115,7 +116,7 @@ class MLP(nn.Module):
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x)) # type: ignore
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x))) # type: ignore
|
||||
|
||||
|
||||
class PlamoDecoderLayer(nn.Module):
|
||||
|
||||
+61
-37
@@ -9,6 +9,7 @@ import mlx.nn as nn
|
||||
|
||||
from mlx_lm.models.base import BaseModelArgs, create_attention_mask, create_ssm_mask
|
||||
|
||||
from .activations import swiglu
|
||||
from .cache import KVCache, MambaCache
|
||||
from .ssm import ssm_update
|
||||
|
||||
@@ -54,27 +55,13 @@ class RMSNorm(nn.Module):
|
||||
)
|
||||
|
||||
|
||||
def causal_conv1d_update(conv_state, x, weight) -> tuple[mx.array, mx.array]:
|
||||
dim = x.shape[-1]
|
||||
state_len = conv_state.shape[-2]
|
||||
x = mx.concatenate([conv_state, x], axis=-2)
|
||||
conv_state = x[:, -state_len:]
|
||||
out = mx.conv1d(
|
||||
x,
|
||||
weight,
|
||||
padding=0,
|
||||
groups=dim,
|
||||
)
|
||||
return nn.silu(out), conv_state
|
||||
|
||||
|
||||
class Mamba(nn.Module):
|
||||
def __init__(self, config: ModelArgs) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.d_state = config.mamba_d_state
|
||||
self.d_conv = config.mamba_d_conv
|
||||
self.conv_kernel_size = config.mamba_d_conv
|
||||
self.chunk_size = config.mamba_chunk_size
|
||||
self.num_heads = config.mamba_num_heads
|
||||
self.hidden_size_per_head = config.hidden_size_per_head
|
||||
@@ -88,7 +75,7 @@ class Mamba(nn.Module):
|
||||
in_channels=self.intermediate_size,
|
||||
out_channels=self.intermediate_size,
|
||||
bias=False,
|
||||
kernel_size=self.d_conv,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.intermediate_size,
|
||||
padding=0,
|
||||
)
|
||||
@@ -111,20 +98,63 @@ class Mamba(nn.Module):
|
||||
|
||||
self.out_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def _conv(
|
||||
self,
|
||||
conv_input: mx.array,
|
||||
cache: Optional[MambaCache],
|
||||
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.intermediate_size,
|
||||
),
|
||||
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,
|
||||
x: mx.array,
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any],
|
||||
mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = x.shape
|
||||
|
||||
x = x.reshape(batch_size, seq_len, self.num_heads, self.hidden_size_per_head)
|
||||
B = B.reshape(batch_size, seq_len, 1, self.d_state)
|
||||
C = C.reshape(batch_size, seq_len, 1, self.d_state)
|
||||
if cache:
|
||||
state = cache[1]
|
||||
lengths = cache.lengths
|
||||
else:
|
||||
state, lengths = None, None
|
||||
|
||||
y, state = ssm_update(
|
||||
x,
|
||||
@@ -136,8 +166,11 @@ class Mamba(nn.Module):
|
||||
self.dt_bias,
|
||||
state,
|
||||
mask=mask,
|
||||
lengths=lengths,
|
||||
)
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
if cache:
|
||||
cache[1] = state
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -147,14 +180,6 @@ class Mamba(nn.Module):
|
||||
):
|
||||
bsize, length, _ = hidden_states.shape
|
||||
|
||||
if cache is not None and cache[0] is not None:
|
||||
conv_state = cache[0]
|
||||
else:
|
||||
conv_state = mx.zeros(
|
||||
(bsize, self.d_conv - 1, self.intermediate_size),
|
||||
dtype=hidden_states.dtype,
|
||||
)
|
||||
|
||||
zx = self.in_proj(hidden_states)
|
||||
zx = zx.reshape(bsize, length, self.num_heads, -1)
|
||||
# z: (bsize, length, num_heads, hidden_size_per_head)
|
||||
@@ -168,9 +193,8 @@ class Mamba(nn.Module):
|
||||
)
|
||||
|
||||
x = x.reshape(bsize, -1, self.num_heads * self.hidden_size_per_head)
|
||||
if mask is not None:
|
||||
x = mx.where(mask[..., None], x, 0)
|
||||
x, conv_state = causal_conv1d_update(conv_state, x, self.conv1d.weight)
|
||||
x = self._conv(x, cache, mask)
|
||||
|
||||
BCdt = self.bcdt_proj(x)
|
||||
B, C, dt = mx.split(BCdt, [self.d_state, self.d_state * 2], axis=-1)
|
||||
|
||||
@@ -181,18 +205,18 @@ class Mamba(nn.Module):
|
||||
|
||||
# (bsize, length, num_heads)
|
||||
dt = self.dt_proj(dt)
|
||||
out, ssm_state = self._ssm(
|
||||
out = self._ssm(
|
||||
x,
|
||||
B,
|
||||
C,
|
||||
dt,
|
||||
cache[1] if cache else None,
|
||||
cache,
|
||||
mask,
|
||||
)
|
||||
out = out * nn.silu(z.flatten(-2))
|
||||
if cache is not None:
|
||||
cache[0] = conv_state
|
||||
cache[1] = ssm_state
|
||||
if cache:
|
||||
cache.advance(out.shape[1])
|
||||
|
||||
out = swiglu(z.flatten(-2), out)
|
||||
return self.out_proj(out)
|
||||
|
||||
|
||||
@@ -282,7 +306,7 @@ class MLP(nn.Module):
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
h = self.gate_up_proj(x)
|
||||
hs = mx.split(h, 2, axis=-1)
|
||||
return self.down_proj(nn.silu(hs[0]) * hs[1])
|
||||
return self.down_proj(swiglu(hs[0], hs[1]))
|
||||
|
||||
|
||||
class PlamoDecoderLayer(nn.Module):
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
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
|
||||
|
||||
|
||||
@@ -89,7 +90,7 @@ class MLP(nn.Module):
|
||||
def __call__(self, x):
|
||||
a1 = self.w1(x)
|
||||
a2 = self.w2(x)
|
||||
return self.c_proj(a1 * nn.silu(a2))
|
||||
return self.c_proj(swiglu(a2, a1))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
+34
-1
@@ -5,7 +5,9 @@ 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 .rope_utils import initialize_rope
|
||||
|
||||
@@ -90,7 +92,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -183,6 +185,37 @@ class Model(nn.Module):
|
||||
k: v for k, v in weights.items() if "self_attn.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()
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@@ -103,7 +104,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Qwen2MoeSparseMoeBlock(nn.Module):
|
||||
|
||||
+34
-1
@@ -5,7 +5,9 @@ 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 .rope_utils import initialize_rope
|
||||
|
||||
@@ -95,7 +97,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
@@ -185,6 +187,37 @@ 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
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 .switch_layers import SwitchGLU
|
||||
|
||||
@@ -103,7 +104,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Qwen3MoeSparseMoeBlock(nn.Module):
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
@@ -44,10 +45,10 @@ class ModelArgs(BaseModelArgs):
|
||||
rope_theta: float
|
||||
partial_rotary_factor: float
|
||||
max_position_embeddings: int
|
||||
head_dim: int
|
||||
norm_topk_prob: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
attention_bias: bool = False
|
||||
head_dim: Optional[int] = None
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
full_attention_interval: int = 4
|
||||
|
||||
@@ -63,7 +64,7 @@ class Qwen3NextRMSNormGated(nn.Module):
|
||||
) -> mx.array:
|
||||
x = mx.fast.rms_norm(hidden_states, self.weight, self.eps)
|
||||
if gate is not None:
|
||||
x = x * nn.silu(gate)
|
||||
x = swiglu(gate, x)
|
||||
return x
|
||||
|
||||
|
||||
@@ -155,7 +156,7 @@ class Qwen3NextMLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class Qwen3NextGatedDeltaNet(nn.Module):
|
||||
@@ -247,8 +248,16 @@ class Qwen3NextGatedDeltaNet(nn.Module):
|
||||
if mask is not None:
|
||||
mixed_qkv = mx.where(mask[..., None], mixed_qkv, 0)
|
||||
conv_input = mx.concatenate([conv_state, mixed_qkv], axis=1)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = conv_input[:, -(self.conv_kernel_size - 1) :]
|
||||
n_keep = self.conv_kernel_size - 1
|
||||
if cache.lengths is not None:
|
||||
ends = mx.clip(cache.lengths, 0, S)
|
||||
positions = (ends[:, None] + mx.arange(n_keep))[..., None]
|
||||
cache[0] = mx.take_along_axis(conv_input, positions, axis=1)
|
||||
else:
|
||||
cache[0] = conv_input[:, -n_keep:, :]
|
||||
|
||||
conv_out = nn.silu(self.conv1d(conv_input))
|
||||
|
||||
q, k, v = [
|
||||
@@ -280,6 +289,7 @@ class Qwen3NextGatedDeltaNet(nn.Module):
|
||||
|
||||
if cache is not None:
|
||||
cache[1] = state
|
||||
cache.advance(S)
|
||||
|
||||
out = self.norm(out, z)
|
||||
return self.out_proj(out.reshape(B, S, -1))
|
||||
|
||||
+14
-10
@@ -43,18 +43,22 @@ class SuScaledRoPE(nn.Module):
|
||||
long_mscale (float, optional): Scale the input prior to embedding.
|
||||
"""
|
||||
super().__init__()
|
||||
freqs = base ** (mx.arange(0, dims, 2, dtype=mx.float32) / dims)
|
||||
self._freqs = mx.array(long_factor, dtype=mx.float32) * freqs
|
||||
self.original_max_position_embeddings = original_max_position_embeddings
|
||||
self.scale = long_mscale or math.sqrt(
|
||||
1
|
||||
+ math.log(max_position_embeddings / original_max_position_embeddings)
|
||||
/ math.log(original_max_position_embeddings)
|
||||
)
|
||||
self.dim = dims
|
||||
|
||||
def __call__(self, x, offset: int = 0):
|
||||
x[..., : self.dim] = self.scale * x[..., : self.dim]
|
||||
freqs = base ** (mx.arange(0, dims, 2, dtype=mx.float32) / dims)
|
||||
self._freqs = mx.array(long_factor, dtype=mx.float32) * freqs
|
||||
|
||||
def default_scale(factor):
|
||||
return math.sqrt(
|
||||
1 + math.log(factor) / math.log(original_max_position_embeddings)
|
||||
)
|
||||
|
||||
factor = max_position_embeddings / original_max_position_embeddings
|
||||
self._scale = long_mscale or (1.0 if factor <= 1.0 else default_scale(factor))
|
||||
|
||||
def __call__(self, x, offset: Union[int, mx.array] = 0):
|
||||
x[..., : self.dim] = self._scale * x[..., : self.dim]
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
self.dim,
|
||||
@@ -217,7 +221,7 @@ def initialize_rope(
|
||||
base=base,
|
||||
scaling_config=scaling_config,
|
||||
)
|
||||
elif rope_type == "yarn":
|
||||
elif rope_type in ("yarn", "deepseek_yarn"):
|
||||
scaling_factor = scaling_config["factor"]
|
||||
rope_kwargs = {
|
||||
key: scaling_config[key]
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .cache import ArraysCache
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
norm_eps: float
|
||||
head_dim: int
|
||||
num_hidden_layers: int
|
||||
a_low_rank_dim: int
|
||||
v_low_rank_dim: int
|
||||
gate_low_rank_dim: int
|
||||
decay_low_rank_dim: int
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def addcmul(x, y, z):
|
||||
return x + y * z
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def l2_norm(x):
|
||||
return x / mx.maximum(mx.linalg.norm(x, axis=-1, keepdims=True), 1e-7)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def _wkv7_step_ops(r, w, k, v, a, b, state):
|
||||
sab = (state @ a[..., None]) @ b[..., None, :]
|
||||
state = state * w[:, :, None, :] + v[..., None] @ k[..., None, :] + sab
|
||||
y = state @ r[..., None]
|
||||
return y, state
|
||||
|
||||
|
||||
def _make_wkv7_kernel():
|
||||
if not mx.metal.is_available():
|
||||
return None
|
||||
source = f"""
|
||||
auto n = thread_position_in_grid.z;
|
||||
auto b_idx = n / H;
|
||||
auto h_idx = n % H;
|
||||
constexpr int n_per_t = D / 32;
|
||||
|
||||
// [B, T, H, D]
|
||||
auto r_ = r + b_idx * T * H * D + h_idx * D;
|
||||
auto w_ = w + b_idx * T * H * D + h_idx * D;
|
||||
auto k_ = k + b_idx * T * H * D + h_idx * D;
|
||||
auto v_ = v + b_idx * T * H * D + h_idx * D;
|
||||
auto a_ = a + b_idx * T * H * D + h_idx * D;
|
||||
auto b_ = b + b_idx * T * H * D + h_idx * D;
|
||||
y += b_idx * T * H * D + h_idx * D;
|
||||
|
||||
auto dk_idx = thread_position_in_threadgroup.x;
|
||||
auto dv_idx = thread_position_in_grid.y;
|
||||
|
||||
// state_in, state_out: [B, H, D, D]
|
||||
auto i_state = state_in + (n * D + dv_idx) * D;
|
||||
auto o_state = state_out + (n * D + dv_idx) * D;
|
||||
|
||||
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 t = 0; t < T; ++t) {{
|
||||
float sa = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
sa += state[i] * a_[s_idx];
|
||||
state[i] = state[i] * w_[s_idx];
|
||||
}}
|
||||
sa = simd_sum(sa);
|
||||
|
||||
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] * v_[dv_idx] + sa * b_[s_idx];
|
||||
out += state[i] * r_[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
|
||||
r_ += H * D;
|
||||
w_ += H * D;
|
||||
k_ += H * D;
|
||||
v_ += H * D;
|
||||
a_ += H * D;
|
||||
b_ += H * D;
|
||||
y += H * D;
|
||||
}}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
o_state[s_idx] = static_cast<InT>(state[i]);
|
||||
}}
|
||||
"""
|
||||
inputs = ["r", "w", "k", "v", "a", "b", "state_in", "T"]
|
||||
return mx.fast.metal_kernel(
|
||||
name="wkv7_kernel",
|
||||
input_names=inputs,
|
||||
output_names=["y", "state_out"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
_wkv7_kernel = _make_wkv7_kernel()
|
||||
|
||||
|
||||
def wkv7_kernel(
|
||||
r: mx.array,
|
||||
w: mx.array,
|
||||
k: mx.array,
|
||||
v: mx.array,
|
||||
a: mx.array,
|
||||
b: mx.array,
|
||||
state: mx.array,
|
||||
):
|
||||
B, T, H, D = r.shape
|
||||
input_dtype = r.dtype
|
||||
|
||||
return _wkv7_kernel(
|
||||
inputs=[r, w, k, v, a, b, state, T],
|
||||
template=[
|
||||
("InT", input_dtype),
|
||||
("H", H),
|
||||
("D", D),
|
||||
],
|
||||
grid=(32, D, B * H),
|
||||
threadgroup=(32, 4, 1),
|
||||
output_shapes=[(B, T, H, D), state.shape],
|
||||
output_dtypes=[input_dtype, input_dtype],
|
||||
)
|
||||
|
||||
|
||||
class LayerNormPerHead(nn.Module):
|
||||
def __init__(self, head_dim, num_heads, eps):
|
||||
super().__init__()
|
||||
self.weight = mx.zeros((num_heads, head_dim))
|
||||
self.bias = mx.zeros((num_heads, head_dim))
|
||||
self.eps = eps
|
||||
|
||||
def __call__(self, x):
|
||||
return self.weight * mx.fast.layer_norm(x, None, None, self.eps) + self.bias
|
||||
|
||||
|
||||
class LoRA(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
output_dim: int,
|
||||
low_rank_dim: int,
|
||||
bias: Optional[bool] = True,
|
||||
activation: Optional[str] = "tanh",
|
||||
):
|
||||
super().__init__()
|
||||
self.input_dim = input_dim
|
||||
self.output_dim = output_dim
|
||||
self.low_rank_dim = low_rank_dim
|
||||
self.bias = bias
|
||||
|
||||
if activation is None:
|
||||
self.activation = nn.Identity()
|
||||
elif activation == "sigmoid":
|
||||
self.activation = nn.Sigmoid()
|
||||
elif activation == "tanh":
|
||||
self.activation = nn.Tanh()
|
||||
elif activation == "relu":
|
||||
self.activation = nn.ReLU()
|
||||
else:
|
||||
raise ValueError(f"Unsupported activation type: {activation}.")
|
||||
|
||||
self.lora = [
|
||||
nn.Linear(self.input_dim, self.low_rank_dim, bias=False),
|
||||
self.activation,
|
||||
nn.Linear(self.low_rank_dim, self.output_dim, bias=self.bias),
|
||||
]
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.lora[2](self.lora[1](self.lora[0](x)))
|
||||
|
||||
|
||||
class TokenShift(nn.Module):
|
||||
def __call__(self, x, state):
|
||||
B, L, D = x.shape
|
||||
if state is None:
|
||||
state = mx.zeros((B, 1, D), x.dtype)
|
||||
if L == 1:
|
||||
return state
|
||||
else:
|
||||
return mx.concatenate([state, x[:, :-1, :]], axis=1)
|
||||
|
||||
|
||||
class Rwkv7ChannelMixing(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
hidden_dim = args.hidden_size
|
||||
intermediate_size = args.intermediate_size
|
||||
|
||||
self.key = nn.Linear(hidden_dim, intermediate_size, bias=False)
|
||||
self.value = nn.Linear(intermediate_size, hidden_dim, bias=False)
|
||||
|
||||
self.x_k = mx.zeros((hidden_dim))
|
||||
|
||||
self.token_shift = TokenShift()
|
||||
|
||||
def __call__(self, x, cache) -> mx.array:
|
||||
state = cache[2] if cache is not None else None
|
||||
x_prev = self.token_shift(x, state)
|
||||
xx = addcmul(x, x_prev - x, self.x_k)
|
||||
if cache is not None:
|
||||
cache[2] = x[:, -1:, :]
|
||||
return self.value(nn.relu2(self.key(xx)))
|
||||
|
||||
|
||||
class Rwkv7TimeMixing(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
self.args = args
|
||||
self.hidden_size = args.hidden_size
|
||||
self.head_dim = args.head_dim
|
||||
self.num_heads = self.hidden_size // self.head_dim
|
||||
self.a_low_rank_dim = args.a_low_rank_dim
|
||||
self.v_low_rank_dim = args.v_low_rank_dim
|
||||
self.gate_low_rank_dim = args.gate_low_rank_dim
|
||||
self.decay_low_rank_dim = args.decay_low_rank_dim
|
||||
|
||||
self.token_shift = TokenShift()
|
||||
|
||||
self.x_r = mx.zeros((1, 1, self.hidden_size))
|
||||
self.x_w = mx.zeros((1, 1, self.hidden_size))
|
||||
self.x_k = mx.zeros((1, 1, self.hidden_size))
|
||||
self.x_v = mx.zeros((1, 1, self.hidden_size))
|
||||
self.x_a = mx.zeros((1, 1, self.hidden_size))
|
||||
self.x_g = mx.zeros((1, 1, self.hidden_size))
|
||||
|
||||
self.k_k = mx.zeros((self.num_heads, self.head_dim))
|
||||
self.k_a = mx.zeros((self.num_heads, self.head_dim))
|
||||
self.r_k = mx.zeros((self.num_heads, self.head_dim))
|
||||
|
||||
self.r_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
self.k_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
self.v_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
self.o_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
|
||||
self.g_norm = LayerNormPerHead(self.head_dim, self.num_heads, eps=64e-5)
|
||||
|
||||
self.w_lora = LoRA(
|
||||
self.hidden_size,
|
||||
self.hidden_size,
|
||||
low_rank_dim=self.decay_low_rank_dim,
|
||||
activation="tanh",
|
||||
)
|
||||
|
||||
if self.layer_idx > 0:
|
||||
self.v_lora = LoRA(
|
||||
self.hidden_size,
|
||||
self.hidden_size,
|
||||
low_rank_dim=self.v_low_rank_dim,
|
||||
activation=None,
|
||||
)
|
||||
|
||||
self.a_lora = LoRA(
|
||||
self.hidden_size,
|
||||
self.hidden_size,
|
||||
low_rank_dim=self.a_low_rank_dim,
|
||||
activation=None,
|
||||
)
|
||||
|
||||
self.g_lora = LoRA(
|
||||
self.hidden_size,
|
||||
self.hidden_size,
|
||||
low_rank_dim=self.gate_low_rank_dim,
|
||||
activation="sigmoid",
|
||||
bias=False,
|
||||
)
|
||||
|
||||
def _wkv7(self, r, w, k, v, a, b, state):
|
||||
B, L, _, _ = r.shape
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(B, self.num_heads, self.head_dim, self.head_dim), dtype=r.dtype
|
||||
)
|
||||
|
||||
if mx.default_device() == mx.gpu and mx.metal.is_available():
|
||||
return wkv7_kernel(r, w, k, v, a, b, state)
|
||||
else:
|
||||
ys = []
|
||||
for t in range(L):
|
||||
y, state = _wkv7_step_ops(
|
||||
r[:, t], w[:, t], k[:, t], v[:, t], a[:, t], b[:, t], state
|
||||
)
|
||||
ys.append(y)
|
||||
|
||||
y = mx.stack(ys, axis=1).astype(r.dtype)
|
||||
return y, state
|
||||
|
||||
def __call__(self, x, v_first, cache):
|
||||
if cache is None:
|
||||
token_shift_cache, state_cache = None, None
|
||||
else:
|
||||
token_shift_cache, state_cache = cache[0], cache[1]
|
||||
|
||||
B, L, D = x.shape
|
||||
x_prev = self.token_shift(x, token_shift_cache)
|
||||
xx = x_prev - x
|
||||
|
||||
xr = addcmul(x, xx, self.x_r)
|
||||
xw = addcmul(x, xx, self.x_w)
|
||||
xk = addcmul(x, xx, self.x_k)
|
||||
xv = addcmul(x, xx, self.x_v)
|
||||
xa = addcmul(x, xx, self.x_a)
|
||||
xg = addcmul(x, xx, self.x_g)
|
||||
|
||||
key = self.k_proj(xk).reshape(B, L, self.num_heads, self.head_dim)
|
||||
value = self.v_proj(xv).reshape(B, L, self.num_heads, self.head_dim)
|
||||
receptance = self.r_proj(xr).reshape(B, L, self.num_heads, self.head_dim)
|
||||
iclr = mx.sigmoid(self.a_lora(xa)).reshape(B, L, self.num_heads, self.head_dim)
|
||||
gate = self.g_lora(xg)
|
||||
|
||||
if self.layer_idx == 0:
|
||||
v_first = value
|
||||
else:
|
||||
vv = mx.sigmoid(self.v_lora(xv)).reshape(
|
||||
B, L, self.num_heads, self.head_dim
|
||||
)
|
||||
value = addcmul(value, v_first - value, vv)
|
||||
|
||||
decay = mx.sigmoid(
|
||||
self.w_lora(xw).reshape(B, L, self.num_heads, self.head_dim)
|
||||
).astype(mx.float32)
|
||||
decay = mx.exp(-0.606531 * decay).astype(receptance.dtype)
|
||||
kk = l2_norm((key * self.k_k))
|
||||
key = key * (1 + (iclr - 1) * self.k_a)
|
||||
a = -kk
|
||||
b = kk * iclr
|
||||
|
||||
out, new_state_cache = self._wkv7(
|
||||
receptance, decay, key, value, a, b, state_cache
|
||||
)
|
||||
out = self.g_norm(out.reshape(B, L, self.num_heads, self.head_dim))
|
||||
out = (
|
||||
out + (receptance * key * self.r_k).sum(axis=-1, keepdims=True) * value
|
||||
).reshape([B, L, D])
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = x[:, -1:, :]
|
||||
cache[1] = new_state_cache
|
||||
|
||||
out = self.o_proj(out * gate)
|
||||
return out, v_first
|
||||
|
||||
|
||||
class Rwkv7Layer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.layer_idx = layer_idx
|
||||
if self.layer_idx == 0:
|
||||
self.pre_norm = nn.LayerNorm(args.hidden_size, eps=args.norm_eps)
|
||||
self.attn = Rwkv7TimeMixing(args, layer_idx=self.layer_idx)
|
||||
self.ffn = Rwkv7ChannelMixing(args)
|
||||
self.attn_norm = nn.LayerNorm(args.hidden_size, eps=args.norm_eps)
|
||||
self.ffn_norm = nn.LayerNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
def __call__(self, x, v_first, cache):
|
||||
if self.layer_idx == 0:
|
||||
x = self.pre_norm(x)
|
||||
|
||||
h, v_first = self.attn(self.attn_norm(x), v_first, cache)
|
||||
h = x + h
|
||||
out = h + self.ffn(self.ffn_norm(h), cache)
|
||||
return out, v_first
|
||||
|
||||
|
||||
class Rwkv7Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embeddings = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Rwkv7Layer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.LayerNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
def __call__(self, x: mx.array, cache):
|
||||
x = self.embeddings(x)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
v_first = None
|
||||
for layer, c in zip(self.layers, cache):
|
||||
x, v_first = layer(x, v_first, c)
|
||||
return self.norm(x)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Rwkv7Model(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):
|
||||
x = self.model(inputs, cache)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
logits = self.model.embeddings.as_linear(x)
|
||||
else:
|
||||
logits = self.lm_head(x)
|
||||
|
||||
return logits
|
||||
|
||||
def make_cache(self):
|
||||
return [ArraysCache(size=3) for _ in range(len(self.layers))]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "k_k" in k or "k_a" in k or "g_norm" in k:
|
||||
weights[k] = weights[k].reshape(
|
||||
self.args.hidden_size // self.args.head_dim, self.args.head_dim
|
||||
)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if "lora.2" in path or "embeddings" in path:
|
||||
return {"bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Union
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
@@ -96,7 +97,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright © 2026 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .glm4_moe import Model # noqa: F401
|
||||
|
||||
|
||||
@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
|
||||
n_shared_experts: int
|
||||
n_routed_experts: int
|
||||
routed_scaling_factor: float
|
||||
num_experts_per_tok: int
|
||||
first_k_dense_replace: int
|
||||
norm_topk_prob: bool
|
||||
max_position_embeddings: int
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
tie_word_embeddings: bool
|
||||
partial_rotary_factor: float
|
||||
rope_scaling: Optional[Dict] = None
|
||||
attention_bias: bool = False
|
||||
use_qk_norm: bool = False
|
||||
n_group: int = 1
|
||||
topk_group: int = 1
|
||||
scoring_func: str = "sigmoid"
|
||||
topk_method: str = "noaux_tc"
|
||||
+23
-4
@@ -114,6 +114,7 @@ def ssm_attn(
|
||||
state: Optional[mx.array] = None,
|
||||
time_step_limit: Tuple[float, float] = (0.001, 100.0),
|
||||
mask: Optional[mx.array] = None,
|
||||
lengths: Optional[mx.array] = None,
|
||||
step: int = 256,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""SSD-SSM forward pass.
|
||||
@@ -128,6 +129,7 @@ def ssm_attn(
|
||||
dt_bias: Bias for time deltas of shape (num_heads,).
|
||||
time_step_limit: Minimum and maximum value for time deltas.
|
||||
mask: Optional multiplicative mask.
|
||||
lengths: Optional lenghts of sequences, assumed to be the full length if unspecified.
|
||||
step: Step size for processing x.
|
||||
|
||||
Code modified from
|
||||
@@ -139,7 +141,7 @@ def ssm_attn(
|
||||
|
||||
dt = compute_dt(dt, dt_bias, time_step_limit)
|
||||
repeats = h // g
|
||||
A = -mx.exp(A_log)
|
||||
A = -mx.exp(A_log).astype(dt.dtype)
|
||||
dtA = dt * A.reshape(1, 1, -1)
|
||||
dtx = dt.reshape(b, l, h, 1) * x
|
||||
|
||||
@@ -157,7 +159,14 @@ def ssm_attn(
|
||||
y = surrogate_attention_matrix @ dtx.swapaxes(1, 2)
|
||||
y = mx.swapaxes(y, 1, 2)
|
||||
|
||||
decay = decay[:, :, -1:, :].transpose(0, 3, 1, 2)
|
||||
if lengths is not None:
|
||||
pos = mx.maximum(mx.minimum(lengths, step) - 1, 0)
|
||||
pos = mx.expand_dims(pos, (1, 2, 3))
|
||||
decay = mx.take_along_axis(decay, pos, axis=2)
|
||||
else:
|
||||
decay = decay[:, :, -1:, :]
|
||||
|
||||
decay = decay.transpose(0, 3, 1, 2)
|
||||
B = mx.repeat(B, h // g, axis=1).swapaxes(2, 3)
|
||||
dtxdecay = dtx * decay
|
||||
dtxdecay = dtxdecay.swapaxes(1, 2).swapaxes(2, 3)
|
||||
@@ -167,10 +176,16 @@ def ssm_attn(
|
||||
if state is not None:
|
||||
exp_dtA_cumsum = mx.exp(mx.cumsum(dtA, axis=-2))
|
||||
next_state += exp_dtA_cumsum[:, -1, :, None, None] * state
|
||||
state = state.reshape((b, 1, g, repeats, dh, d))
|
||||
C = C.reshape(b, s, g, 1, d, 1)
|
||||
y_prev = (state @ C).squeeze(-1).flatten(2, 3)
|
||||
y_prev = (
|
||||
(state.reshape((b, 1, g, repeats, dh, d)) @ C).squeeze(-1).flatten(2, 3)
|
||||
)
|
||||
y += exp_dtA_cumsum[..., None] * y_prev
|
||||
if lengths is not None and state is not None:
|
||||
next_state = mx.where(
|
||||
mx.expand_dims(lengths < 0, (1, 2, 3)), state, next_state
|
||||
)
|
||||
|
||||
return y, next_state
|
||||
|
||||
ys = []
|
||||
@@ -183,6 +198,8 @@ def ssm_attn(
|
||||
state,
|
||||
None if mask is None else mask[..., i : i + step],
|
||||
)
|
||||
if lengths is not None:
|
||||
lengths = lengths - step
|
||||
ys.append(y)
|
||||
y = mx.concatenate(ys, axis=1) + x * D.reshape(1, 1, h, 1)
|
||||
return y, state
|
||||
@@ -199,6 +216,7 @@ def ssm_update(
|
||||
state: Optional[mx.array] = None,
|
||||
time_step_limit: Tuple[float, float] = (0.001, 100.0),
|
||||
mask: Optional[mx.array] = None,
|
||||
lengths: Optional[mx.array] = None,
|
||||
):
|
||||
seq_len = hidden_states.shape[1]
|
||||
if (
|
||||
@@ -218,6 +236,7 @@ def ssm_update(
|
||||
state,
|
||||
time_step_limit,
|
||||
mask=mask,
|
||||
lengths=lengths,
|
||||
)
|
||||
else:
|
||||
return ssm_update_kernel(
|
||||
|
||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
||||
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
|
||||
|
||||
|
||||
@@ -135,7 +136,7 @@ class MLP(nn.Module):
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
|
||||
@@ -6,6 +6,8 @@ from functools import partial
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .activations import swiglu
|
||||
|
||||
|
||||
def _gather_sort(x, indices):
|
||||
*_, M = indices.shape
|
||||
@@ -147,17 +149,12 @@ class SwitchLinear(nn.Module):
|
||||
return ql
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(x, gate):
|
||||
return nn.silu(gate) * x
|
||||
|
||||
|
||||
class SwiGLU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, x, gate):
|
||||
return swiglu(x, gate)
|
||||
return swiglu(gate, x)
|
||||
|
||||
|
||||
class SwitchGLU(nn.Module):
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
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 .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "youtu_llm"
|
||||
vocab_size: int = 128256
|
||||
hidden_size: int = 2048
|
||||
intermediate_size: int = 6144
|
||||
num_hidden_layers: int = 32
|
||||
num_attention_heads: int = 16
|
||||
num_key_value_heads: int = 16
|
||||
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
|
||||
max_position_embeddings: int = 131072
|
||||
rms_norm_eps: float = 1e-6
|
||||
rope_theta: float = 1600000
|
||||
rope_traditional: bool = True
|
||||
rope_scaling: Optional[Dict] = None
|
||||
attention_bias: bool = False
|
||||
mlp_bias: bool = False
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
|
||||
class YoutuLLMAttention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = config.hidden_size
|
||||
self.num_heads = config.num_attention_heads
|
||||
self.max_position_embeddings = config.max_position_embeddings
|
||||
self.rope_theta = config.rope_theta
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.qk_rope_head_dim = config.qk_rope_head_dim
|
||||
self.kv_lora_rank = config.kv_lora_rank
|
||||
self.v_head_dim = config.v_head_dim
|
||||
self.qk_nope_head_dim = config.qk_nope_head_dim
|
||||
self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
|
||||
|
||||
self.scale = self.q_head_dim**-0.5
|
||||
|
||||
if self.q_lora_rank is None:
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.q_head_dim, bias=False
|
||||
)
|
||||
else:
|
||||
self.q_a_proj = nn.Linear(
|
||||
self.hidden_size, self.q_lora_rank, bias=config.attention_bias
|
||||
)
|
||||
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=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)
|
||||
self.kv_b_proj = nn.Linear(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads
|
||||
* (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
|
||||
bias=False,
|
||||
)
|
||||
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.v_head_dim,
|
||||
self.hidden_size,
|
||||
bias=config.attention_bias,
|
||||
)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.qk_rope_head_dim,
|
||||
base=self.rope_theta,
|
||||
traditional=config.rope_traditional,
|
||||
scaling_config=config.rope_scaling,
|
||||
max_position_embeddings=config.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
|
||||
|
||||
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 = self.kv_b_proj(self.kv_a_layernorm(compressed_kv))
|
||||
kv = kv.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
k_nope, values = mx.split(kv, [self.qk_nope_head_dim], axis=-1)
|
||||
|
||||
if cache is not None:
|
||||
q_pe = self.rope(q_pe, cache.offset)
|
||||
k_pe = self.rope(k_pe, cache.offset)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys, values = cache.update_and_fetch(
|
||||
mx.concatenate([k_nope, k_pe], axis=-1), values
|
||||
)
|
||||
else:
|
||||
q_pe = self.rope(q_pe)
|
||||
k_pe = self.rope(k_pe)
|
||||
k_pe = mx.repeat(k_pe, self.num_heads, axis=1)
|
||||
keys = mx.concatenate([k_nope, k_pe], axis=-1)
|
||||
|
||||
queries = mx.concatenate([q_nope, q_pe], axis=-1)
|
||||
|
||||
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 YoutuLLMMLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(
|
||||
config.hidden_size, config.intermediate_size, bias=config.mlp_bias
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
config.hidden_size, config.intermediate_size, bias=config.mlp_bias
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
config.intermediate_size, config.hidden_size, bias=config.mlp_bias
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class YoutuLLMDecoderLayer(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
self.self_attn = YoutuLLMAttention(config)
|
||||
self.mlp = YoutuLLMMLP(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))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class YoutuLLMModel(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.vocab_size = config.vocab_size
|
||||
self.num_hidden_layers = config.num_hidden_layers
|
||||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||||
self.layers = [
|
||||
YoutuLLMDecoderLayer(config=config) for _ in range(config.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.model_type = config.model_type
|
||||
self.model = YoutuLLMModel(config)
|
||||
if not config.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
if self.config.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def sanitize(self, weights):
|
||||
if self.config.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
+178
-79
@@ -34,7 +34,11 @@ from huggingface_hub import scan_cache_dir
|
||||
|
||||
from ._version import __version__
|
||||
from .generate import BatchGenerator, stream_generate
|
||||
from .models.cache import can_trim_prompt_cache, make_prompt_cache, trim_prompt_cache
|
||||
from .models.cache import (
|
||||
can_trim_prompt_cache,
|
||||
make_prompt_cache,
|
||||
trim_prompt_cache,
|
||||
)
|
||||
from .sample_utils import make_logits_processors, make_sampler
|
||||
from .utils import load
|
||||
|
||||
@@ -52,9 +56,9 @@ class StopCondition(NamedTuple):
|
||||
|
||||
def stopping_criteria(
|
||||
tokens: List[int],
|
||||
eos_token_ids: set,
|
||||
stop_id_sequences: List[List[int]],
|
||||
stop_words: List[str],
|
||||
eos_token_id: Union[int, None],
|
||||
) -> StopCondition:
|
||||
"""
|
||||
Determines whether the token generation should stop based on predefined
|
||||
@@ -62,14 +66,14 @@ def stopping_criteria(
|
||||
|
||||
Args:
|
||||
tokens (List[int]): The current sequence of generated tokens.
|
||||
eos_token_ids (set): The token IDs that represents the
|
||||
end-of-sequence. If the last token in ``tokens`` is in the set,
|
||||
the generation should stop.
|
||||
stop_id_sequences (List[List[[int]]): A list of integer lists, each
|
||||
representing a sequence of token IDs. If the end of the `tokens`
|
||||
list matches any of these sequences, the generation should stop.
|
||||
stop_words (List[str]): The stop words that correspond to the
|
||||
``stop_id_sequences``.
|
||||
eos_token_id (Union[int, None]): The token ID that represents the
|
||||
end-of-sequence. If the last token in `tokens` matches this, the
|
||||
generation should stop.
|
||||
|
||||
Returns:
|
||||
StopCondition: A named tuple indicating whether the stop condition has
|
||||
@@ -77,7 +81,7 @@ def stopping_criteria(
|
||||
end if it has (`trim_length`) as well as the text that should be
|
||||
trimmed.
|
||||
"""
|
||||
if tokens and tokens[-1] == eos_token_id:
|
||||
if tokens and tokens[-1] in eos_token_ids:
|
||||
return StopCondition(stop_met=True, trim_length=0, trim_text_length=0)
|
||||
|
||||
for stop_ids, stop_word in zip(stop_id_sequences, stop_words):
|
||||
@@ -158,6 +162,11 @@ def process_message_content(messages):
|
||||
message["content"] = "".join(text_fragments)
|
||||
elif content is None:
|
||||
message["content"] = ""
|
||||
if tool_calls := message.get("tool_calls", False):
|
||||
for tool_call in tool_calls:
|
||||
if func := tool_call.get("function", False):
|
||||
if args := func.get("arguments", False):
|
||||
func["arguments"] = json.loads(args)
|
||||
|
||||
|
||||
class LRUPromptCache:
|
||||
@@ -351,7 +360,12 @@ class GenerationContext:
|
||||
has_tool_calling: bool
|
||||
tool_call_start: str
|
||||
tool_call_end: str
|
||||
eos_token_id: int
|
||||
tool_parser: Callable[[str, Any], Dict]
|
||||
has_thinking: bool
|
||||
think_start_id: int
|
||||
think_end_id: int
|
||||
think_end: str
|
||||
eos_token_ids: set
|
||||
stop_token_sequences: List[List[int]]
|
||||
prompt: List[int]
|
||||
|
||||
@@ -378,6 +392,7 @@ class ModelProvider:
|
||||
self.model = None
|
||||
self.tokenizer = None
|
||||
self.draft_model = None
|
||||
self.is_batchable = False
|
||||
|
||||
# Preload the default model if it is provided
|
||||
self.default_model_map = {}
|
||||
@@ -448,9 +463,38 @@ class ModelProvider:
|
||||
elif draft_model_path is not None and draft_model_path != "default_model":
|
||||
self.draft_model, draft_tokenizer = load(draft_model_path)
|
||||
validate_draft_tokenizer(draft_tokenizer)
|
||||
|
||||
if self.draft_model is None:
|
||||
self.is_batchable = all(
|
||||
hasattr(c, "merge") for c in make_prompt_cache(self.model)
|
||||
)
|
||||
|
||||
return self.model, self.tokenizer
|
||||
|
||||
|
||||
def _make_sampler(args, tokenizer):
|
||||
return make_sampler(
|
||||
args.sampling.temperature,
|
||||
top_p=args.sampling.top_p,
|
||||
top_k=args.sampling.top_k,
|
||||
min_p=args.sampling.min_p,
|
||||
xtc_probability=args.sampling.xtc_probability,
|
||||
xtc_threshold=args.sampling.xtc_threshold,
|
||||
xtc_special_tokens=[
|
||||
tokenizer.eos_token_id,
|
||||
tokenizer.encode("\n"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _make_logits_processors(args):
|
||||
return make_logits_processors(
|
||||
args.logits.logit_bias,
|
||||
args.logits.repetition_penalty,
|
||||
args.logits.repetition_context_size,
|
||||
)
|
||||
|
||||
|
||||
class ResponseGenerator:
|
||||
def __init__(self, model_provider: ModelProvider, prompt_cache: LRUPromptCache):
|
||||
self.model_provider = model_provider
|
||||
@@ -471,12 +515,20 @@ class ResponseGenerator:
|
||||
tools = request.tools
|
||||
role_mapping = request.role_mapping
|
||||
|
||||
if tokenizer.chat_template:
|
||||
if tokenizer.has_chat_template:
|
||||
process_message_content(messages)
|
||||
if tools and not tokenizer.has_tool_calling:
|
||||
logging.warning(
|
||||
"Received tools but model does not support tool calling. "
|
||||
"If you think this is an error, file an issue here: "
|
||||
"https://github.com/ml-explore/mlx-lm/issues"
|
||||
)
|
||||
|
||||
return tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tools,
|
||||
tools=tools,
|
||||
add_generation_prompt=True,
|
||||
tokenize=True,
|
||||
**self.model_provider.cli_args.chat_template_args,
|
||||
)
|
||||
else:
|
||||
@@ -485,16 +537,7 @@ class ResponseGenerator:
|
||||
return tokenizer.encode(request.prompt)
|
||||
|
||||
def _is_batchable(self, args):
|
||||
if (
|
||||
args.model.draft != "default_model"
|
||||
or self.model_provider.cli_args.draft_model is not None
|
||||
):
|
||||
return False
|
||||
if args.logits.logit_bias is not None:
|
||||
return False
|
||||
if args.logits.repetition_penalty != 0:
|
||||
return False
|
||||
if args.logprobs > 0:
|
||||
if not self.model_provider.is_batchable:
|
||||
return False
|
||||
if args.seed is not None:
|
||||
return False
|
||||
@@ -512,12 +555,15 @@ class ResponseGenerator:
|
||||
|
||||
unprocessed_requests = []
|
||||
|
||||
def get_next_request():
|
||||
def get_next_request(timeout=None):
|
||||
if unprocessed_requests:
|
||||
return unprocessed_requests.pop()
|
||||
else:
|
||||
try:
|
||||
return self.requests.get_nowait()
|
||||
if timeout is not None:
|
||||
return self.requests.get(timeout=timeout)
|
||||
else:
|
||||
return self.requests.get_nowait()
|
||||
except QueueEmpty:
|
||||
return None
|
||||
|
||||
@@ -529,7 +575,12 @@ class ResponseGenerator:
|
||||
while not self._stop:
|
||||
request = None
|
||||
if not drain_batch:
|
||||
request = get_next_request()
|
||||
timeout = (
|
||||
None
|
||||
if (batch_generator is not None and len(batch_results) > 0)
|
||||
else 0.1
|
||||
)
|
||||
request = get_next_request(timeout=timeout)
|
||||
|
||||
# We got a request
|
||||
if request is not None:
|
||||
@@ -541,7 +592,6 @@ class ResponseGenerator:
|
||||
if (
|
||||
batch_generator is not None
|
||||
and current_model == args.model
|
||||
and current_sampling == args.sampling
|
||||
and is_batchable
|
||||
):
|
||||
prompt = self._tokenize(current_tokenizer, request)
|
||||
@@ -549,7 +599,12 @@ class ResponseGenerator:
|
||||
has_tool_calling=tokenizer.has_tool_calling,
|
||||
tool_call_start=tokenizer.tool_call_start,
|
||||
tool_call_end=tokenizer.tool_call_end,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
tool_parser=tokenizer.tool_parser,
|
||||
has_thinking=tokenizer.has_thinking,
|
||||
think_start_id=tokenizer.think_start_id,
|
||||
think_end=tokenizer.think_end,
|
||||
think_end_id=tokenizer.think_end_id,
|
||||
eos_token_ids=tokenizer.eos_token_ids,
|
||||
stop_token_sequences=[
|
||||
tokenizer.encode(stop_word, add_special_tokens=False)
|
||||
for stop_word in args.stop_words
|
||||
@@ -565,7 +620,11 @@ class ResponseGenerator:
|
||||
cache = make_prompt_cache(self.model_provider.model)
|
||||
|
||||
(uid,) = batch_generator.insert(
|
||||
[rest], args.max_tokens, caches=[cache]
|
||||
[rest],
|
||||
args.max_tokens,
|
||||
caches=[cache],
|
||||
samplers=[_make_sampler(args, tokenizer)],
|
||||
logits_processors=[_make_logits_processors(args)],
|
||||
)
|
||||
batch_results[uid] = {
|
||||
"ctx": ctx,
|
||||
@@ -592,25 +651,12 @@ class ResponseGenerator:
|
||||
continue
|
||||
|
||||
current_model = args.model
|
||||
current_sampling = args.sampling
|
||||
current_tokenizer = tokenizer
|
||||
current_model_key = self.model_provider.model_key
|
||||
batch_results = {}
|
||||
batch_generator = BatchGenerator(
|
||||
model,
|
||||
stop_tokens=tokenizer.eos_token_ids,
|
||||
sampler=make_sampler(
|
||||
args.sampling.temperature,
|
||||
top_p=args.sampling.top_p,
|
||||
top_k=args.sampling.top_k,
|
||||
min_p=args.sampling.min_p,
|
||||
xtc_probability=args.sampling.xtc_probability,
|
||||
xtc_threshold=args.sampling.xtc_threshold,
|
||||
xtc_special_tokens=[
|
||||
tokenizer.eos_token_id,
|
||||
tokenizer.encode("\n"),
|
||||
],
|
||||
),
|
||||
prompt_progress_callback=progress_callback,
|
||||
)
|
||||
unprocessed_requests.append((rqueue, request, args))
|
||||
@@ -650,7 +696,8 @@ class ResponseGenerator:
|
||||
for r in responses:
|
||||
result = batch_results[r.uid]
|
||||
result["cache_key"].append(r.token)
|
||||
result["detokenizer"].add_token(r.token)
|
||||
if r.finish_reason != "stop":
|
||||
result["detokenizer"].add_token(r.token)
|
||||
|
||||
top_tokens = None
|
||||
if args.logprobs > 0:
|
||||
@@ -708,7 +755,12 @@ class ResponseGenerator:
|
||||
has_tool_calling=tokenizer.has_tool_calling,
|
||||
tool_call_start=tokenizer.tool_call_start,
|
||||
tool_call_end=tokenizer.tool_call_end,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
tool_parser=tokenizer.tool_parser,
|
||||
has_thinking=tokenizer.has_thinking,
|
||||
think_start_id=tokenizer.think_start_id,
|
||||
think_end=tokenizer.think_end,
|
||||
think_end_id=tokenizer.think_end_id,
|
||||
eos_token_ids=tokenizer.eos_token_ids,
|
||||
stop_token_sequences=[
|
||||
tokenizer.encode(stop_word, add_special_tokens=False)
|
||||
for stop_word in args.stop_words
|
||||
@@ -722,23 +774,8 @@ class ResponseGenerator:
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
# Make the sampler and logit processor
|
||||
sampler = make_sampler(
|
||||
args.sampling.temperature,
|
||||
top_p=args.sampling.top_p,
|
||||
top_k=args.sampling.top_k,
|
||||
min_p=args.sampling.min_p,
|
||||
xtc_probability=args.sampling.xtc_probability,
|
||||
xtc_threshold=args.sampling.xtc_threshold,
|
||||
xtc_special_tokens=[
|
||||
tokenizer.eos_token_id,
|
||||
tokenizer.encode("\n"),
|
||||
],
|
||||
)
|
||||
logits_processors = make_logits_processors(
|
||||
args.logits.logit_bias,
|
||||
args.logits.repetition_penalty,
|
||||
args.logits.repetition_context_size,
|
||||
)
|
||||
sampler = _make_sampler(args, tokenizer)
|
||||
logits_processors = _make_logits_processors(args)
|
||||
|
||||
# Load the KV cache
|
||||
cache, rest = self.prompt_cache.fetch_nearest_cache(
|
||||
@@ -921,7 +958,7 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
self.top_p = self.body.get("top_p", self.response_generator.cli_args.top_p)
|
||||
self.top_k = self.body.get("top_k", self.response_generator.cli_args.top_k)
|
||||
self.min_p = self.body.get("min_p", self.response_generator.cli_args.min_p)
|
||||
self.repetition_penalty = self.body.get("repetition_penalty", 1.0)
|
||||
self.repetition_penalty = self.body.get("repetition_penalty", 0.0)
|
||||
self.repetition_context_size = self.body.get("repetition_context_size", 20)
|
||||
self.xtc_probability = self.body.get("xtc_probability", 0.0)
|
||||
self.xtc_threshold = self.body.get("xtc_threshold", 0.0)
|
||||
@@ -1015,6 +1052,7 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
top_tokens: Optional[List[Dict[int, float]]] = None,
|
||||
tokens: Optional[List[int]] = None,
|
||||
tool_calls: Optional[List[str]] = None,
|
||||
reasoning_text: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Generate a single response packet based on response type (stream or
|
||||
@@ -1034,6 +1072,7 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
tokens to logprobs for the top N tokens at each token position.
|
||||
tokens (Optional[List[int]]): List of tokens to return with logprobs structure
|
||||
tool_calls (Optional[List[str]]): List of tool calls.
|
||||
reasoning_text (Optional[str]): The reasoning text generated by the model.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the response, in the same format as
|
||||
@@ -1043,17 +1082,6 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
top_logprobs = top_tokens or []
|
||||
tool_calls = tool_calls or []
|
||||
|
||||
def parse_function(tool_text):
|
||||
tool_call = json.loads(tool_text.strip())
|
||||
return {
|
||||
"function": {
|
||||
"name": tool_call.get("name", None),
|
||||
"arguments": json.dumps(tool_call.get("arguments", "")),
|
||||
},
|
||||
"type": "function",
|
||||
"id": None,
|
||||
}
|
||||
|
||||
# Static response
|
||||
response = {
|
||||
"id": self.request_id,
|
||||
@@ -1099,7 +1127,8 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
choice[key_name] = {
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
"tool_calls": [parse_function(tool_text) for tool_text in tool_calls],
|
||||
"reasoning": reasoning_text,
|
||||
"tool_calls": tool_calls,
|
||||
}
|
||||
elif self.object_type == "text_completion":
|
||||
choice.update(text=text)
|
||||
@@ -1184,8 +1213,43 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
# Variables to save the tool calls in as they are being generated by
|
||||
# the model.
|
||||
in_tool_call = False
|
||||
made_tool_call = False
|
||||
tool_calls = []
|
||||
tool_text = ""
|
||||
tool_idx = 0
|
||||
|
||||
def parse_single_tool(tool_text):
|
||||
nonlocal tool_idx
|
||||
tool_call = ctx.tool_parser(tool_text, request.tools)
|
||||
tool_call["arguments"] = json.dumps(
|
||||
tool_call["arguments"], ensure_ascii=False
|
||||
)
|
||||
out = {
|
||||
"function": tool_call,
|
||||
"type": "function",
|
||||
"id": str(uuid.uuid4()),
|
||||
}
|
||||
if self.stream:
|
||||
out["index"] = tool_idx
|
||||
tool_idx += 1
|
||||
return out
|
||||
|
||||
def parse_tools(tool_calls):
|
||||
if not tool_calls:
|
||||
return []
|
||||
return [parse_single_tool(tool_text) for tool_text in tool_calls]
|
||||
|
||||
# Start out in reasoning if the model is a reasoning model and the
|
||||
# prompt has an open think token but no closing think token
|
||||
in_reasoning = False
|
||||
if ctx.has_thinking:
|
||||
for i in range(len(ctx.prompt) - 1, -1, -1):
|
||||
if ctx.prompt[i] == ctx.think_end_id:
|
||||
break
|
||||
elif ctx.prompt[i] == ctx.think_start_id:
|
||||
in_reasoning = True
|
||||
break
|
||||
reasoning_text = ""
|
||||
|
||||
# Variables to save the generated tokens and the corresponding probs
|
||||
tokens = []
|
||||
@@ -1198,13 +1262,18 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# Well finally save the reason for stopping
|
||||
finish_reason = "length"
|
||||
|
||||
# Process the generated tokens
|
||||
for gen in response:
|
||||
logging.debug(gen.text)
|
||||
|
||||
# Gather the text in tool calling or text variables
|
||||
if ctx.has_tool_calling and gen.text == ctx.tool_call_start:
|
||||
if in_reasoning:
|
||||
if gen.text == ctx.think_end:
|
||||
in_reasoning = False
|
||||
else:
|
||||
reasoning_text += gen.text
|
||||
elif ctx.has_tool_calling and gen.text == ctx.tool_call_start:
|
||||
made_tool_call = True
|
||||
in_tool_call = True
|
||||
elif in_tool_call:
|
||||
if gen.text == ctx.tool_call_end:
|
||||
@@ -1227,10 +1296,13 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# Check if we should stop early
|
||||
stop_condition = stopping_criteria(
|
||||
tokens, ctx.stop_token_sequences, stop_words, ctx.eos_token_id
|
||||
tokens,
|
||||
ctx.eos_token_ids,
|
||||
ctx.stop_token_sequences,
|
||||
stop_words,
|
||||
)
|
||||
if stop_condition.stop_met:
|
||||
finish_reason = "stop"
|
||||
finish_reason = "tool_call" if made_tool_call else "stop"
|
||||
ctx.stop()
|
||||
tokens = tokens[: len(tokens) - stop_condition.trim_length]
|
||||
text = text[: len(text) - stop_condition.trim_text_length]
|
||||
@@ -1247,12 +1319,16 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
):
|
||||
continue
|
||||
elif segment or tool_calls:
|
||||
elif segment or tool_calls or reasoning_text:
|
||||
response = self.generate_response(
|
||||
segment, None, tool_calls=tool_calls
|
||||
segment,
|
||||
None,
|
||||
tool_calls=parse_tools(tool_calls),
|
||||
reasoning_text=reasoning_text,
|
||||
)
|
||||
self.wfile.write(f"data: {json.dumps(response)}\n\n".encode())
|
||||
self.wfile.flush()
|
||||
reasoning_text = ""
|
||||
segment = ""
|
||||
tool_calls = []
|
||||
|
||||
@@ -1261,7 +1337,10 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
if self.stream:
|
||||
response = self.generate_response(
|
||||
segment, finish_reason, tool_calls=tool_calls
|
||||
segment,
|
||||
finish_reason,
|
||||
tool_calls=parse_tools(tool_calls),
|
||||
reasoning_text=reasoning_text,
|
||||
)
|
||||
self.wfile.write(f"data: {json.dumps(response)}\n\n".encode())
|
||||
self.wfile.flush()
|
||||
@@ -1280,7 +1359,8 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
token_logprobs=token_logprobs,
|
||||
top_tokens=top_tokens,
|
||||
tokens=tokens,
|
||||
tool_calls=tool_calls,
|
||||
reasoning_text=reasoning_text,
|
||||
tool_calls=parse_tools(tool_calls),
|
||||
)
|
||||
response_json = json.dumps(response).encode()
|
||||
indent = "\t" # Backslashes can't be inside of f-strings
|
||||
@@ -1416,6 +1496,18 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
for repo in downloaded_models
|
||||
]
|
||||
|
||||
if self.response_generator.cli_args.model:
|
||||
model_path = Path(self.response_generator.cli_args.model)
|
||||
if model_path.exists():
|
||||
model_id = str(model_path.resolve())
|
||||
models.append(
|
||||
{
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": self.created,
|
||||
}
|
||||
)
|
||||
|
||||
response = {"object": "list", "data": models}
|
||||
|
||||
response_json = json.dumps(response).encode()
|
||||
@@ -1450,7 +1542,11 @@ def run(
|
||||
"it only implements basic security checks."
|
||||
)
|
||||
logging.info(f"Starting httpd at {host} on port {port}...")
|
||||
httpd.serve_forever()
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
httpd.shutdown()
|
||||
response_generator.stop_and_join()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -1550,6 +1646,9 @@ def main():
|
||||
default="{}",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if mx.metal.is_available():
|
||||
wired_limit = mx.metal.device_info()["max_recommended_working_set_size"]
|
||||
mx.set_wired_limit(wired_limit)
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, args.log_level.upper(), None),
|
||||
|
||||
+108
-33
@@ -1,4 +1,6 @@
|
||||
import importlib
|
||||
import json
|
||||
import warnings
|
||||
from functools import partial
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -89,11 +91,7 @@ class NaiveStreamingDetokenizer(StreamingDetokenizer):
|
||||
def text(self):
|
||||
if self._current_tokens:
|
||||
self._current_text = self._tokenizer.decode(self._current_tokens)
|
||||
if self._current_text.endswith("\ufffd") or (
|
||||
self._tokenizer.clean_up_tokenization_spaces
|
||||
and len(self._current_text) > 0
|
||||
and self._current_text[-1] == " "
|
||||
):
|
||||
if self._current_text.endswith("\ufffd"):
|
||||
self._current_text = self._current_text[:-1]
|
||||
if self._current_text and self._current_text[-1] == "\n":
|
||||
self._text += self._current_text
|
||||
@@ -161,8 +159,6 @@ class BPEStreamingDetokenizer(StreamingDetokenizer):
|
||||
_space_matches = (".", "?", "!", ",", "n't", "'m", "'s", "'ve", "'re")
|
||||
|
||||
def __init__(self, tokenizer):
|
||||
self.clean_spaces = tokenizer.clean_up_tokenization_spaces
|
||||
|
||||
# Extract the tokens in a list from id to text
|
||||
self.tokenmap = [None] * len(tokenizer.vocab)
|
||||
for value, tokenid in tokenizer.vocab.items():
|
||||
@@ -197,8 +193,6 @@ class BPEStreamingDetokenizer(StreamingDetokenizer):
|
||||
return current_text
|
||||
elif not self.text:
|
||||
return current_text[1:]
|
||||
elif self.clean_spaces and current_text[1:].startswith(self._space_matches):
|
||||
return current_text[1:]
|
||||
return current_text
|
||||
|
||||
def add_token(self, token):
|
||||
@@ -208,10 +202,7 @@ class BPEStreamingDetokenizer(StreamingDetokenizer):
|
||||
text = self._decode_bytes(self._unflushed)
|
||||
|
||||
# For multi-byte utf-8 wait until they are complete
|
||||
# For single spaces wait until the next token to clean it if needed
|
||||
if not text.endswith("\ufffd") and not (
|
||||
len(v) == 1 and self._byte_decoder.get(v[0]) == 32
|
||||
):
|
||||
if not text.endswith("\ufffd"):
|
||||
self.text += self._maybe_trim_space(text)
|
||||
self._unflushed = ""
|
||||
|
||||
@@ -259,7 +250,14 @@ class TokenizerWrapper:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, tokenizer, detokenizer_class=NaiveStreamingDetokenizer, eos_token_ids=None
|
||||
self,
|
||||
tokenizer,
|
||||
detokenizer_class=NaiveStreamingDetokenizer,
|
||||
eos_token_ids=None,
|
||||
chat_template=None,
|
||||
tool_call_start=None,
|
||||
tool_call_end=None,
|
||||
tool_parser=None,
|
||||
):
|
||||
self._tokenizer = tokenizer
|
||||
self._detokenizer_class = detokenizer_class
|
||||
@@ -270,24 +268,44 @@ class TokenizerWrapper:
|
||||
)
|
||||
self._think_start = None
|
||||
self._think_end = None
|
||||
self._tool_call_start = None
|
||||
self._tool_call_end = None
|
||||
self._think_start_id = None
|
||||
self._think_end_id = None
|
||||
|
||||
THINK_TOKENS = [("<think>", "</think>")]
|
||||
TOOL_CALL_TOKENS = [("<tool_call>", "</tool_call>")]
|
||||
self._chat_template = chat_template
|
||||
self.has_chat_template = (
|
||||
tokenizer.chat_template is not None or chat_template is not None
|
||||
)
|
||||
self._tool_parser = tool_parser
|
||||
self._tool_call_start = tool_call_start
|
||||
self._tool_call_end = tool_call_end
|
||||
|
||||
vocab = tokenizer.get_vocab()
|
||||
THINK_TOKENS = [("<think>", "</think>")]
|
||||
for think_start, think_end in THINK_TOKENS:
|
||||
if think_start in vocab and think_end in vocab:
|
||||
self._think_start = think_start
|
||||
self._think_end = think_end
|
||||
self._think_start_id = vocab[think_start]
|
||||
self._think_end_id = vocab[think_end]
|
||||
break
|
||||
if tokenizer.chat_template and '"tool"' in tokenizer.chat_template:
|
||||
for tool_call_start, tool_call_end in TOOL_CALL_TOKENS:
|
||||
if tool_call_start in vocab and tool_call_end in vocab:
|
||||
self._tool_call_start = tool_call_start
|
||||
self._tool_call_end = tool_call_end
|
||||
break
|
||||
|
||||
# Disable tool calling if tool call tokens aren't in vocab
|
||||
if (tool_call_start and tool_call_start not in vocab) or (
|
||||
tool_call_end and tool_call_end not in vocab
|
||||
):
|
||||
self._tool_call_start = None
|
||||
self._tool_call_end = None
|
||||
self._tool_parser = None
|
||||
|
||||
def apply_chat_template(self, *args, tokenize=True, **kwargs):
|
||||
if self._chat_template is not None:
|
||||
out = self._chat_template(*args, **kwargs)
|
||||
if tokenize:
|
||||
out = self._tokenizer.encode(out, add_special_tokens=False)
|
||||
return out
|
||||
|
||||
kwargs["return_dict"] = False
|
||||
return self._tokenizer.apply_chat_template(*args, tokenize=tokenize, **kwargs)
|
||||
|
||||
def add_eos_token(self, token: str):
|
||||
token_id = None
|
||||
@@ -309,10 +327,18 @@ class TokenizerWrapper:
|
||||
def think_start(self):
|
||||
return self._think_start
|
||||
|
||||
@property
|
||||
def think_start_id(self):
|
||||
return self._think_start_id
|
||||
|
||||
@property
|
||||
def think_end(self):
|
||||
return self._think_end
|
||||
|
||||
@property
|
||||
def think_end_id(self):
|
||||
return self._think_end_id
|
||||
|
||||
@property
|
||||
def has_tool_calling(self):
|
||||
return self._tool_call_start is not None
|
||||
@@ -325,6 +351,10 @@ class TokenizerWrapper:
|
||||
def tool_call_end(self):
|
||||
return self._tool_call_end
|
||||
|
||||
@property
|
||||
def tool_parser(self):
|
||||
return self._tool_parser
|
||||
|
||||
@property
|
||||
def detokenizer(self):
|
||||
"""
|
||||
@@ -423,10 +453,26 @@ def _is_bpe_decoder(decoder):
|
||||
return isinstance(decoder, dict) and decoder.get("type", None) == "ByteLevel"
|
||||
|
||||
|
||||
def _infer_tool_parser(chat_template):
|
||||
"""Attempt to auto-infer a tool parser from the chat template."""
|
||||
if not isinstance(chat_template, str):
|
||||
return None
|
||||
elif "<minimax:tool_call>" in chat_template:
|
||||
return "minimax_m2"
|
||||
elif "<start_function_call>" in chat_template:
|
||||
return "function_gemma"
|
||||
elif "<arg_key>" in chat_template:
|
||||
return "glm47"
|
||||
elif "<tool_call>\n<function=" in chat_template:
|
||||
return "qwen3_coder"
|
||||
elif "<tool_call>" in chat_template and "tool_call.name" in chat_template:
|
||||
return "json_tools"
|
||||
return None
|
||||
|
||||
|
||||
def load(
|
||||
model_path,
|
||||
tokenizer_config_extra: Optional[Dict[str, Any]] = None,
|
||||
return_tokenizer=True,
|
||||
eos_token_ids=None,
|
||||
) -> TokenizerWrapper:
|
||||
"""Load a huggingface tokenizer and try to infer the type of streaming
|
||||
@@ -457,15 +503,44 @@ def load(
|
||||
if isinstance(eos_token_ids, int):
|
||||
eos_token_ids = [eos_token_ids]
|
||||
|
||||
if return_tokenizer:
|
||||
kwargs = tokenizer_config_extra or {}
|
||||
return TokenizerWrapper(
|
||||
AutoTokenizer.from_pretrained(model_path, **kwargs),
|
||||
detokenizer_class,
|
||||
eos_token_ids=eos_token_ids,
|
||||
)
|
||||
tokenizer_config_file = model_path / "tokenizer_config.json"
|
||||
chat_template = None
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_path, **(tokenizer_config_extra or {})
|
||||
)
|
||||
|
||||
tokenizer_config = tokenizer.init_kwargs
|
||||
|
||||
if chat_template_type := tokenizer_config.get("chat_template_type", False):
|
||||
chat_template = importlib.import_module(
|
||||
f"mlx_lm.chat_templates.{chat_template_type}"
|
||||
).apply_chat_template
|
||||
|
||||
tool_parser_type = tokenizer_config.get(
|
||||
"tool_parser_type", _infer_tool_parser(tokenizer.chat_template)
|
||||
)
|
||||
|
||||
if tool_parser_type is not None:
|
||||
tool_module = importlib.import_module(f"mlx_lm.tool_parsers.{tool_parser_type}")
|
||||
tool_parser = tool_module.parse_tool_call
|
||||
tool_call_start = tool_module.tool_call_start
|
||||
tool_call_end = tool_module.tool_call_end
|
||||
tokenizer_config["tool_parser_type"] = tool_parser_type
|
||||
else:
|
||||
return detokenizer_class
|
||||
tool_parser = None
|
||||
tool_call_start = None
|
||||
tool_call_end = None
|
||||
|
||||
return TokenizerWrapper(
|
||||
tokenizer,
|
||||
detokenizer_class,
|
||||
eos_token_ids=eos_token_ids,
|
||||
chat_template=chat_template,
|
||||
tool_parser=tool_parser,
|
||||
tool_call_start=tool_call_start,
|
||||
tool_call_end=tool_call_end,
|
||||
)
|
||||
|
||||
|
||||
def no_bos_or_eos(sequence: List, bos: int, eos: int) -> List:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
import regex as re
|
||||
|
||||
_tool_call_regex = re.compile(r"call:(\w+)\{(.*?)\}", re.DOTALL)
|
||||
|
||||
|
||||
def parse_tool_call(text: str, _: Optional[Any] = None):
|
||||
match = _tool_call_regex.findall(text)
|
||||
if not match:
|
||||
raise ValueError("No function provided.")
|
||||
func_name = match[0][0]
|
||||
args_str = match[0][1]
|
||||
arguments = {}
|
||||
escape = "<escape>"
|
||||
while args_str:
|
||||
split = args_str.index(":")
|
||||
key = args_str[:split]
|
||||
args_str = args_str[split + 1 :]
|
||||
# Parse a string
|
||||
if args_str.startswith(escape):
|
||||
args_str = args_str[len(escape) :]
|
||||
split = args_str.index(escape)
|
||||
arguments[key] = args_str[:split]
|
||||
args_str = args_str[split + len(escape) + 1 :]
|
||||
continue
|
||||
if "," in args_str:
|
||||
split = args_str.index(",")
|
||||
else:
|
||||
split = len(args_str)
|
||||
|
||||
value = args_str[:split]
|
||||
args_str = args_str[split + 1 :]
|
||||
|
||||
try:
|
||||
arguments[key] = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
arguments[key] = value
|
||||
|
||||
return dict(name=func_name, arguments=arguments)
|
||||
|
||||
|
||||
tool_call_start = "<start_function_call>"
|
||||
tool_call_end = "<end_function_call>"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user