Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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):
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.29.0"
|
||||
__version__ = "0.30.0"
|
||||
|
||||
+11
-2
@@ -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,7 +74,9 @@ 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(
|
||||
args.model, pipeline_group, tensor_group, return_config=True
|
||||
)
|
||||
else:
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
|
||||
@@ -114,7 +114,9 @@ def main():
|
||||
if not args.ignore_chat_template and tokenizer.chat_template is not None:
|
||||
messages = [{"role": "user", "content": args.prompt}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=False, continue_final_message=True
|
||||
messages,
|
||||
add_generation_prompt=False,
|
||||
continue_final_message=True,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
+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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
+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(
|
||||
|
||||
+1
-1
@@ -874,7 +874,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)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
@@ -315,13 +316,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 +404,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 +439,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,6 +7,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .pipeline import PipelineMixin
|
||||
@@ -256,13 +257,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 +344,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)
|
||||
|
||||
@@ -419,6 +429,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 = 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,6 +6,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import CacheList, KVCache
|
||||
@@ -222,6 +223,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
|
||||
)
|
||||
@@ -328,13 +334,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 +442,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)
|
||||
|
||||
@@ -500,6 +515,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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
@@ -226,6 +227,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
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
# 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 .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(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return down_proj
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
top_k,
|
||||
n_group,
|
||||
topk_group,
|
||||
routed_scaling_factor,
|
||||
norm_topk_prob,
|
||||
):
|
||||
|
||||
scores = mx.sigmoid(gates.astype(mx.float32))
|
||||
orig_scores = scores
|
||||
scores = scores + e_score_correction_bias
|
||||
if n_group > 1:
|
||||
scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1))
|
||||
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
scores = scores / (denominator + 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 = weight.dtype
|
||||
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
|
||||
+81
-13
@@ -5,9 +5,11 @@ 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 .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 +38,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):
|
||||
@@ -146,7 +152,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 +173,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 +196,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 +286,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 [
|
||||
|
||||
+138
-14
@@ -15,6 +15,7 @@ from .base import (
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .ssm import ssm_update
|
||||
from .switch_layers import SwitchMLP
|
||||
|
||||
|
||||
@dataclass()
|
||||
@@ -37,24 +38,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 = x * nn.silu(gate)
|
||||
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,8 +101,11 @@ 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
|
||||
@@ -139,7 +153,7 @@ class NemotronHMamba2Mixer(nn.Module):
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
self.D.astype(hidden_states.dtype),
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
@@ -245,24 +259,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 +375,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 +401,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 +477,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
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -183,6 +184,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
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -185,6 +186,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
|
||||
|
||||
@@ -43,18 +43,36 @@ 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
|
||||
|
||||
freqs = base ** (mx.arange(0, dims, 2, dtype=mx.float32) / dims)
|
||||
self._short_freqs = mx.array(short_factor, dtype=mx.float32) * freqs
|
||||
self._long_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._short_scale = short_mscale or (
|
||||
1.0 if factor <= 1.0 else default_scale(factor)
|
||||
)
|
||||
self._long_scale = long_mscale or (
|
||||
1.0 if factor <= 1.0 else default_scale(factor)
|
||||
)
|
||||
|
||||
def __call__(self, x, offset: int = 0):
|
||||
x[..., : self.dim] = self.scale * x[..., : self.dim]
|
||||
seq_len = offset + x.shape[-2]
|
||||
if seq_len > self.original_max_position_embeddings:
|
||||
freqs = self._long_freqs
|
||||
scale = self._long_scale
|
||||
else:
|
||||
freqs = self._short_freqs
|
||||
scale = self._short_scale
|
||||
|
||||
x[..., : self.dim] = scale * x[..., : self.dim]
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
self.dim,
|
||||
@@ -62,7 +80,7 @@ class SuScaledRoPE(nn.Module):
|
||||
base=None,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
freqs=self._freqs,
|
||||
freqs=freqs,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -139,7 +139,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
|
||||
|
||||
|
||||
+29
-5
@@ -34,7 +34,13 @@ 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 (
|
||||
KVCache,
|
||||
RotatingKVCache,
|
||||
can_trim_prompt_cache,
|
||||
make_prompt_cache,
|
||||
trim_prompt_cache,
|
||||
)
|
||||
from .sample_utils import make_logits_processors, make_sampler
|
||||
from .utils import load
|
||||
|
||||
@@ -378,6 +384,7 @@ class ModelProvider:
|
||||
self.model = None
|
||||
self.tokenizer = None
|
||||
self.draft_model = None
|
||||
self.cache_types = set()
|
||||
|
||||
# Preload the default model if it is provided
|
||||
self.default_model_map = {}
|
||||
@@ -448,6 +455,15 @@ 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)
|
||||
|
||||
# Figure out the cache types and save them in a set for anybody that
|
||||
# wants to make a decision based on those.
|
||||
for c in make_prompt_cache(self.model):
|
||||
self.cache_types.add(type(c))
|
||||
if self.draft_model is not None:
|
||||
for c in make_prompt_cache(self.draft_model):
|
||||
self.cache_types.add(type(c))
|
||||
|
||||
return self.model, self.tokenizer
|
||||
|
||||
|
||||
@@ -477,6 +493,7 @@ class ResponseGenerator:
|
||||
messages,
|
||||
tools,
|
||||
add_generation_prompt=True,
|
||||
tokenize=True,
|
||||
**self.model_provider.cli_args.chat_template_args,
|
||||
)
|
||||
else:
|
||||
@@ -490,6 +507,9 @@ class ResponseGenerator:
|
||||
or self.model_provider.cli_args.draft_model is not None
|
||||
):
|
||||
return False
|
||||
for c in self.model_provider.cache_types:
|
||||
if c not in (KVCache, RotatingKVCache):
|
||||
return False
|
||||
if args.logits.logit_bias is not None:
|
||||
return False
|
||||
if args.logits.repetition_penalty != 0:
|
||||
@@ -512,12 +532,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 +552,8 @@ class ResponseGenerator:
|
||||
while not self._stop:
|
||||
request = None
|
||||
if not drain_batch:
|
||||
request = get_next_request()
|
||||
timeout = 0.1 if batch_generator is None else None
|
||||
request = get_next_request(timeout=timeout)
|
||||
|
||||
# We got a request
|
||||
if request is not None:
|
||||
@@ -921,7 +945,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)
|
||||
|
||||
@@ -89,11 +89,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 +157,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 +191,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 +200,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 = ""
|
||||
|
||||
@@ -289,6 +278,10 @@ class TokenizerWrapper:
|
||||
self._tool_call_end = tool_call_end
|
||||
break
|
||||
|
||||
def apply_chat_template(self, *args, **kwargs):
|
||||
kwargs["return_dict"] = False
|
||||
return self._tokenizer.apply_chat_template(*args, **kwargs)
|
||||
|
||||
def add_eos_token(self, token: str):
|
||||
token_id = None
|
||||
try:
|
||||
|
||||
@@ -57,7 +57,11 @@ class ChatDataset:
|
||||
def process(self, d):
|
||||
messages = d[self.chat_key]
|
||||
tools = d.get("tools", None)
|
||||
tokens = self.tokenizer.apply_chat_template(messages, tools=tools)
|
||||
tokens = self.tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tools=tools,
|
||||
return_dict=False,
|
||||
)
|
||||
if self.mask_prompt:
|
||||
add_generation_prompt = messages[-1].get("role") == "assistant"
|
||||
offset = len(
|
||||
@@ -65,6 +69,7 @@ class ChatDataset:
|
||||
messages[:-1],
|
||||
tools=tools,
|
||||
add_generation_prompt=add_generation_prompt,
|
||||
return_dict=False,
|
||||
)
|
||||
)
|
||||
return (tokens, offset)
|
||||
@@ -105,11 +110,16 @@ class CompletionsDataset:
|
||||
{"role": "user", "content": d[self.prompt_key]},
|
||||
{"role": "assistant", "content": d[self.completion_key]},
|
||||
]
|
||||
tokens = self.tokenizer.apply_chat_template(messages, tools=tools)
|
||||
tokens = self.tokenizer.apply_chat_template(
|
||||
messages, tools=tools, return_dict=False
|
||||
)
|
||||
if self.mask_prompt:
|
||||
offset = len(
|
||||
self.tokenizer.apply_chat_template(
|
||||
messages[0], tools=tools, add_generation_prompt=True
|
||||
messages[0],
|
||||
tools=tools,
|
||||
add_generation_prompt=True,
|
||||
return_dict=False,
|
||||
)
|
||||
)
|
||||
return (tokens, offset)
|
||||
|
||||
+54
-19
@@ -333,7 +333,12 @@ def load(
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def pipeline_load(repo, return_config=False):
|
||||
def sharded_load(
|
||||
repo,
|
||||
pipeline_group: Optional[mx.distributed.Group] = None,
|
||||
tensor_group: Optional[mx.distributed.Group] = None,
|
||||
return_config: bool = False,
|
||||
):
|
||||
# Get model path with everything but weight safetensors
|
||||
model_path = _download(
|
||||
repo,
|
||||
@@ -349,27 +354,50 @@ def pipeline_load(repo, return_config=False):
|
||||
],
|
||||
)
|
||||
|
||||
# Lazy load and shard model to figure out which weights we need
|
||||
# Lazy load model to figure out what type of sharding we can do and which
|
||||
# weights we need to download.
|
||||
model, config = load_model(model_path, lazy=True, strict=False)
|
||||
|
||||
group = mx.distributed.init()
|
||||
rank = group.rank()
|
||||
model.model.pipeline(group)
|
||||
has_pipelining = hasattr(model.model, "pipeline")
|
||||
has_tensor_parallel = hasattr(model, "shard")
|
||||
|
||||
# Figure out which files we need for the local shard
|
||||
with open(model_path / "model.safetensors.index.json", "r") as fid:
|
||||
weight_index = json.load(fid)["weight_map"]
|
||||
if pipeline_group is not None and not has_pipelining:
|
||||
raise ValueError(
|
||||
"The model does not support pipelining but a pipeline_group was provided"
|
||||
)
|
||||
if tensor_group is not None and not has_tensor_parallel:
|
||||
raise ValueError(
|
||||
"The model does not support tensor parallelism but a tensor_group was provided"
|
||||
)
|
||||
if not has_pipelining and not has_tensor_parallel:
|
||||
raise ValueError("The model does not support any sharding")
|
||||
|
||||
local_files = set()
|
||||
for k, _ in tree_flatten(model.parameters()):
|
||||
if file_name := weight_index.get(k, None) is None:
|
||||
raise ValueError(
|
||||
"Pipeline loading is only supported for MLX converted models."
|
||||
)
|
||||
local_files.add(weight_index[k])
|
||||
if pipeline_group is tensor_group is None:
|
||||
if has_tensor_parallel:
|
||||
tensor_group = mx.distributed.init()
|
||||
elif has_pipelining:
|
||||
pipeline_group = mx.distributed.init()
|
||||
|
||||
# Download weights for local shard
|
||||
_download(repo, allow_patterns=local_files)
|
||||
# If pipelining then figure out which files we need for the local shard
|
||||
if pipeline_group is not None:
|
||||
model.model.pipeline(pipeline_group)
|
||||
|
||||
# Figure out which files we need for the local shard
|
||||
with open(model_path / "model.safetensors.index.json", "r") as fid:
|
||||
weight_index = json.load(fid)["weight_map"]
|
||||
|
||||
local_files = set()
|
||||
for k, _ in tree_flatten(model.parameters()):
|
||||
if file_name := weight_index.get(k, None) is None:
|
||||
raise ValueError(
|
||||
"Pipeline loading is only supported for MLX converted models."
|
||||
)
|
||||
local_files.add(weight_index[k])
|
||||
|
||||
# Download weights for local shard
|
||||
_download(repo, allow_patterns=local_files)
|
||||
else:
|
||||
_download(repo)
|
||||
|
||||
# Load and shard the model, and load the weights
|
||||
tokenizer = load_tokenizer(
|
||||
@@ -378,7 +406,10 @@ def pipeline_load(repo, return_config=False):
|
||||
eos_token_ids=config.get("eos_token_id", None),
|
||||
)
|
||||
model, _ = load_model(model_path, lazy=True, strict=False)
|
||||
model.model.pipeline(group)
|
||||
if tensor_group is not None:
|
||||
model.shard(tensor_group)
|
||||
if pipeline_group is not None:
|
||||
model.model.pipeline(pipeline_group)
|
||||
mx.eval(model.parameters())
|
||||
|
||||
# Synchronize processes to avoid timeout
|
||||
@@ -389,6 +420,10 @@ def pipeline_load(repo, return_config=False):
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def pipeline_load(repo, return_config=False):
|
||||
return sharded_load(repo, mx.distributed.init(), None, return_config)
|
||||
|
||||
|
||||
def make_shards(weights: dict, max_file_size_gb: int = MAX_FILE_SIZE_GB) -> list:
|
||||
"""
|
||||
Splits the weights into smaller shards.
|
||||
@@ -486,7 +521,7 @@ def upload_to_hub(path: str, upload_repo: str):
|
||||
if tokenizer.chat_template is not None:
|
||||
messages = [{{"role": "user", "content": prompt}}]
|
||||
prompt = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages, add_generation_prompt=True, return_dict=False,
|
||||
)
|
||||
|
||||
response = generate(model, tokenizer, prompt=prompt, verbose=True)
|
||||
|
||||
@@ -26,7 +26,7 @@ setup(
|
||||
install_requires=[
|
||||
f"mlx>={MIN_MLX_VERSION}; platform_system == 'Darwin'",
|
||||
"numpy",
|
||||
"transformers>=4.39.3",
|
||||
"transformers==5.0.0rc1",
|
||||
"sentencepiece",
|
||||
"protobuf",
|
||||
"pyyaml",
|
||||
|
||||
@@ -21,6 +21,8 @@ class TestDatasets(unittest.TestCase):
|
||||
cls.test_dir = cls.test_dir_fid.name
|
||||
if not os.path.isdir(cls.test_dir):
|
||||
os.mkdir(cls.test_dir_fid.name)
|
||||
# Only one HF request
|
||||
AutoTokenizer.from_pretrained(HF_MODEL_PATH)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@@ -37,7 +39,7 @@ class TestDatasets(unittest.TestCase):
|
||||
data = {"text": "This is an example for the model."}
|
||||
self.save_data(4 * [data])
|
||||
args = types.SimpleNamespace(train=True, test=False, data=self.test_dir)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH, local_files_only=True)
|
||||
train, valid, test = datasets.load_dataset(args, tokenizer)
|
||||
self.assertEqual(len(train), 4)
|
||||
self.assertEqual(len(valid), 4)
|
||||
@@ -50,7 +52,7 @@ class TestDatasets(unittest.TestCase):
|
||||
data = {"prompt": "What is the capital of France?", "completion": "Paris."}
|
||||
self.save_data(4 * [data])
|
||||
args = types.SimpleNamespace(train=True, test=False, data=self.test_dir)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH, local_files_only=True)
|
||||
train, valid, test = datasets.load_dataset(args, tokenizer)
|
||||
self.assertEqual(len(train), 4)
|
||||
self.assertEqual(len(valid), 4)
|
||||
@@ -69,7 +71,7 @@ class TestDatasets(unittest.TestCase):
|
||||
}
|
||||
self.save_data(4 * [data])
|
||||
args = types.SimpleNamespace(train=True, test=False, data=self.test_dir)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH, local_files_only=True)
|
||||
train, valid, test = datasets.load_dataset(args, tokenizer)
|
||||
self.assertEqual(len(train), 4)
|
||||
self.assertEqual(len(valid), 4)
|
||||
@@ -91,7 +93,7 @@ class TestDatasets(unittest.TestCase):
|
||||
test=False,
|
||||
train=True,
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH)
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_PATH, local_files_only=True)
|
||||
train, valid, test = datasets.load_dataset(args, tokenizer)
|
||||
self.assertTrue(len(train) > 0)
|
||||
self.assertTrue(len(train[0]) > 0)
|
||||
|
||||
@@ -81,7 +81,7 @@ class TestGenerate(unittest.TestCase):
|
||||
|
||||
def test_stream_generate_speculative(self):
|
||||
# Use same model as draft model, this is not a speed test
|
||||
draft_model, _ = load(self.HF_MODEL_PATH)
|
||||
draft_model = self.model
|
||||
|
||||
results: List[GenerationResponse] = []
|
||||
drafted: List[bool] = []
|
||||
@@ -90,7 +90,8 @@ class TestGenerate(unittest.TestCase):
|
||||
sampler = make_sampler(temp=0.0)
|
||||
messages = [{"role": "user", "content": "hello"}]
|
||||
prompt = self.tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
for generation_result in stream_generate(
|
||||
@@ -117,7 +118,8 @@ class TestGenerate(unittest.TestCase):
|
||||
# get prompt embeddings
|
||||
messages = [{"role": "user", "content": "Say 'TEST' and nothing else"}]
|
||||
prompt = self.tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
prompt_embeddings = self.model.model.embed_tokens(prompt)
|
||||
|
||||
@@ -140,7 +142,8 @@ class TestGenerate(unittest.TestCase):
|
||||
# get prompt embeddings
|
||||
messages = [{"role": "user", "content": "Say 'TEST' and nothing else"}]
|
||||
prompt = self.tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True
|
||||
messages,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
prompt_embeddings = self.model.model.embed_tokens(prompt)
|
||||
|
||||
|
||||
@@ -2045,6 +2045,41 @@ class TestModels(unittest.TestCase):
|
||||
"type": "yarn",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model_type": "mimo_v2_flash",
|
||||
"num_experts_per_tok": 2,
|
||||
"hybrid_layer_pattern": [0, 1, 0, 1],
|
||||
"moe_layer_freq": [0, 1, 0, 1],
|
||||
"add_swa_attention_sink_bias": True,
|
||||
"add_full_attention_sink_bias": False,
|
||||
"sliding_window_size": 32,
|
||||
"vocab_size": 1000,
|
||||
"hidden_size": 512,
|
||||
"intermediate_size": 512,
|
||||
"moe_intermediate_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 2,
|
||||
"n_shared_experts": 1,
|
||||
"n_routed_experts": 8,
|
||||
"routed_scaling_factor": None,
|
||||
"topk_method": "noaux_tc",
|
||||
"scoring_func": "sigmoid",
|
||||
"norm_topk_prob": True,
|
||||
"n_group": 2,
|
||||
"topk_group": 1,
|
||||
"max_position_embeddings": 1000,
|
||||
"layernorm_epsilon": 1e-5,
|
||||
"rope_theta": 1000.0,
|
||||
"swa_rope_theta": 1000.0,
|
||||
"swa_num_attention_heads": 4,
|
||||
"swa_num_key_value_heads": 2,
|
||||
"head_dim": 128,
|
||||
"v_head_dim": 64,
|
||||
"swa_head_dim": 128,
|
||||
"swa_v_head_dim": 64,
|
||||
"partial_rotary_factor": 0.5,
|
||||
},
|
||||
]
|
||||
for config in test_configs:
|
||||
model_type = config["model_type"]
|
||||
|
||||
@@ -34,6 +34,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
cls.test_dir_fid = tempfile.TemporaryDirectory()
|
||||
cls.test_dir = cls.test_dir_fid.name
|
||||
cls.model, cls.tokenizer = load(HF_MODEL_PATH)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@@ -132,7 +133,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
self.assertTrue(mx.array_equal(v, lv))
|
||||
|
||||
def test_cache_with_generate(self):
|
||||
model, tokenizer = load(HF_MODEL_PATH)
|
||||
model, tokenizer = self.model, self.tokenizer
|
||||
prompt = tokenizer.encode("this is a prompt", return_tensors="mlx")[0]
|
||||
results = list(generate_step(prompt, model, max_tokens=4))
|
||||
toks, all_logits = zip(*results)
|
||||
@@ -212,7 +213,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
self.assertEqual(num_trimmed, 3)
|
||||
|
||||
def test_trim_cache_with_generate(self):
|
||||
model, tokenizer = load(HF_MODEL_PATH)
|
||||
model, tokenizer = self.model, self.tokenizer
|
||||
prompt = tokenizer.encode("this is a prompt", return_tensors="mlx")[0]
|
||||
|
||||
prompt_cache = make_prompt_cache(model)
|
||||
@@ -289,7 +290,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
self.assertEqual(metadata, loaded_metadata)
|
||||
|
||||
def test_cache_to_quantized(self):
|
||||
model, tokenizer = load(HF_MODEL_PATH)
|
||||
model, tokenizer = self.model, self.tokenizer
|
||||
prompt = tokenizer.encode("this is a prompt", return_tensors="mlx")[0]
|
||||
results = zip(range(4), generate_step(prompt, model))
|
||||
toks, all_logits = zip(*(r[1] for r in results))
|
||||
|
||||
@@ -19,6 +19,7 @@ class DummyModelProvider:
|
||||
HF_MODEL_PATH = "mlx-community/Qwen1.5-0.5B-Chat-4bit"
|
||||
self.model, self.tokenizer = load(HF_MODEL_PATH)
|
||||
self.model_key = (HF_MODEL_PATH, None)
|
||||
self.cache_types = set([KVCache])
|
||||
|
||||
# Add draft model support
|
||||
self.draft_model = None
|
||||
|
||||
Reference in New Issue
Block a user