Skip to content

Commit 79a0721

Browse files
authored
Model parallel generation (#676)
1 parent cc3264c commit 79a0721

11 files changed

Lines changed: 483 additions & 60 deletions

File tree

mlx_lm/benchmark.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from mlx_lm import batch_generate, load, stream_generate
88
from mlx_lm.generate import DEFAULT_MODEL
9-
from mlx_lm.utils import pipeline_load
9+
from mlx_lm.utils import pipeline_load, sharded_load
1010

1111

1212
def setup_arg_parser():
@@ -49,6 +49,11 @@ def setup_arg_parser():
4949
help="Number of timing trials",
5050
type=int,
5151
)
52+
parser.add_argument(
53+
"--pipeline",
54+
action="store_true",
55+
help="Use pipelining instead of tensor parallelism",
56+
)
5257
return parser
5358

5459

@@ -59,6 +64,8 @@ def main():
5964

6065
group = mx.distributed.init()
6166
rank = group.rank()
67+
pipeline_group = group if args.pipeline else None
68+
tensor_group = group if not args.pipeline else None
6269

6370
def rprint(*args, **kwargs):
6471
if rank == 0:
@@ -67,7 +74,9 @@ def rprint(*args, **kwargs):
6774
model_path = args.model or DEFAULT_MODEL
6875

6976
if group.size() > 1:
70-
model, tokenizer, config = pipeline_load(args.model, return_config=True)
77+
model, tokenizer, config = sharded_load(
78+
args.model, pipeline_group, tensor_group, return_config=True
79+
)
7180
else:
7281
model, tokenizer, config = load(
7382
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}

mlx_lm/chat.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .generate import stream_generate
88
from .models.cache import make_prompt_cache
99
from .sample_utils import make_sampler
10-
from .utils import load
10+
from .utils import load, sharded_load
1111

1212
DEFAULT_TEMP = 0.0
1313
DEFAULT_TOP_P = 1.0
@@ -79,35 +79,54 @@ def setup_arg_parser():
7979
default=None,
8080
help="System prompt to be used for the chat template",
8181
)
82+
parser.add_argument(
83+
"--pipeline",
84+
action="store_true",
85+
help="Use pipelining instead of tensor parallelism",
86+
)
8287
return parser
8388

8489

8590
def main():
8691
parser = setup_arg_parser()
8792
args = parser.parse_args()
8893

94+
group = mx.distributed.init()
95+
rank = group.rank()
96+
pipeline_group = group if args.pipeline else None
97+
tensor_group = group if not args.pipeline else None
98+
99+
def rprint(*args, **kwargs):
100+
if rank == 0:
101+
print(*args, **kwargs)
102+
89103
if args.seed is not None:
90104
mx.random.seed(args.seed)
91105

92-
model, tokenizer = load(
93-
args.model,
94-
adapter_path=args.adapter_path,
95-
tokenizer_config={
96-
"trust_remote_code": True if args.trust_remote_code else None
97-
},
98-
)
106+
if group.size() > 1:
107+
if args.adapter_path:
108+
parser.error("Adapters not supported in distributed mode")
109+
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
110+
else:
111+
model, tokenizer = load(
112+
args.model,
113+
adapter_path=args.adapter_path,
114+
tokenizer_config={
115+
"trust_remote_code": True if args.trust_remote_code else None
116+
},
117+
)
99118

100119
def print_help():
101-
print("The command list:")
102-
print("- 'q' to exit")
103-
print("- 'r' to reset the chat")
104-
print("- 'h' to display these commands")
120+
rprint("The command list:")
121+
rprint("- 'q' to exit")
122+
rprint("- 'r' to reset the chat")
123+
rprint("- 'h' to display these commands")
105124

106-
print(f"[INFO] Starting chat session with {args.model}.")
125+
rprint(f"[INFO] Starting chat session with {args.model}.")
107126
print_help()
108127
prompt_cache = make_prompt_cache(model, args.max_kv_size)
109128
while True:
110-
query = input(">> ")
129+
query = input(">> " if rank == 0 else "")
111130
if query == "q":
112131
break
113132
if query == "r":
@@ -139,8 +158,8 @@ def print_help():
139158
),
140159
prompt_cache=prompt_cache,
141160
):
142-
print(response.text, flush=True, end="")
143-
print()
161+
rprint(response.text, flush=True, end="")
162+
rprint()
144163

145164

146165
if __name__ == "__main__":
Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
# Copyright © 2024 Apple Inc.
1+
# Copyright © 2025 Apple Inc.
22

33
"""
44
Run with:
55
66
```
77
mlx.launch \
8-
--hostfile /path/to/hosts.json \
9-
/path/to/pipeline_generate.py \
10-
--prompt "hello world"
8+
--backend jaccl \
9+
--env MLX_METAL_FAST_SYNCH=1 \
10+
--hostfile /path/to/hosts.json \
11+
/path/to/sharded_generate.py \
12+
--prompt 'Hello world'
1113
```
1214
13-
Make sure you can run MLX over MPI on two hosts. For more information see the
14-
documentation:
15+
For more information on running distributed programs with MLX see the documentation:
1516
16-
https://ml-explore.github.io/mlx/build/html/usage/distributed.html).
17+
https://ml-explore.github.io/mlx/build/html/usage/distributed.html .
1718
"""
1819

1920
import argparse
2021

2122
import mlx.core as mx
2223

2324
from mlx_lm import stream_generate
24-
from mlx_lm.utils import pipeline_load
25+
from mlx_lm.utils import sharded_load
2526

2627
if __name__ == "__main__":
27-
parser = argparse.ArgumentParser(description="LLM pipelined inference example")
28+
parser = argparse.ArgumentParser(description="LLM distributed inference example")
2829
parser.add_argument(
2930
"--model",
30-
default="mlx-community/DeepSeek-R1-3bit",
31+
default="mlx-community/Llama-3.3-70B-Instruct-4bit",
3132
help="HF repo or path to local model.",
3233
)
3334
parser.add_argument(
@@ -43,16 +44,23 @@
4344
default=256,
4445
help="Maximum number of tokens to generate",
4546
)
47+
parser.add_argument(
48+
"--pipeline",
49+
action="store_true",
50+
help="Use pipelining instead of tensor parallelism",
51+
)
4652
args = parser.parse_args()
4753

4854
group = mx.distributed.init()
4955
rank = group.rank()
56+
pipeline_group = group if args.pipeline else None
57+
tensor_group = group if not args.pipeline else None
5058

5159
def rprint(*args, **kwargs):
5260
if rank == 0:
5361
print(*args, **kwargs)
5462

55-
model, tokenizer = pipeline_load(args.model)
63+
model, tokenizer = sharded_load(args.model, pipeline_group, tensor_group)
5664

5765
messages = [{"role": "user", "content": args.prompt}]
5866
prompt = tokenizer.apply_chat_template(

mlx_lm/models/deepseek_v2.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import mlx.core as mx
88
import mlx.nn as nn
9+
from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients
910

1011
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
1112
from .pipeline import PipelineMixin
@@ -315,13 +316,21 @@ def __init__(self, config: ModelArgs):
315316
config=config, intermediate_size=intermediate_size
316317
)
317318

319+
self.sharding_group = None
320+
318321
def __call__(self, x):
322+
if self.sharding_group is not None:
323+
x = sum_gradients(self.sharding_group)(x)
324+
319325
inds, scores = self.gate(x)
320326
y = self.switch_mlp(x, inds)
321327
y = (y * scores[..., None]).sum(axis=-2)
322328
if self.config.n_shared_experts is not None:
323329
y = y + self.shared_experts(x)
324330

331+
if self.sharding_group is not None:
332+
y = mx.distributed.all_sum(y, group=self.sharding_group)
333+
325334
return y
326335

327336

@@ -395,7 +404,8 @@ def __call__(
395404
cache[-1].keys = mx.depends(cache[-1].keys, h)
396405

397406
# Broadcast h while keeping it in the graph
398-
h = mx.distributed.all_gather(h)[: h.shape[0]]
407+
if pipeline_size > 1:
408+
h = mx.distributed.all_gather(h)[: h.shape[0]]
399409

400410
return self.norm(h)
401411

@@ -429,6 +439,62 @@ def sanitize(self, weights):
429439
weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
430440
return weights
431441

442+
def shard(self, group: Optional[mx.distributed.Group] = None):
443+
group = group or mx.distributed.init()
444+
N = group.size()
445+
for layer in self.model.layers:
446+
# Shard the self attention
447+
if layer.self_attn.q_lora_rank is None:
448+
layer.self_attn.q_proj = shard_linear(
449+
layer.self_attn.q_proj, "all-to-sharded", group=group
450+
)
451+
else:
452+
layer.self_attn.q_b_proj = shard_linear(
453+
layer.self_attn.q_b_proj, "all-to-sharded", group=group
454+
)
455+
layer.self_attn.kv_b_proj = shard_linear(
456+
layer.self_attn.kv_b_proj, "all-to-sharded", group=group
457+
)
458+
layer.self_attn.o_proj = shard_linear(
459+
layer.self_attn.o_proj, "sharded-to-all", group=group
460+
)
461+
layer.self_attn.num_heads //= N
462+
463+
# Shard the MLP
464+
if isinstance(layer.mlp, DeepseekV2MLP):
465+
layer.mlp.gate_proj = shard_linear(
466+
layer.mlp.gate_proj, "all-to-sharded", group=group
467+
)
468+
layer.mlp.down_proj = shard_linear(
469+
layer.mlp.down_proj, "sharded-to-all", group=group
470+
)
471+
layer.mlp.up_proj = shard_linear(
472+
layer.mlp.up_proj, "all-to-sharded", group=group
473+
)
474+
475+
# Shard the MoE. Shard in place since the MoE should be responsible
476+
# for aggregating the results.
477+
else:
478+
layer.mlp.sharding_group = group
479+
shard_inplace(
480+
layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group
481+
)
482+
shard_inplace(
483+
layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group
484+
)
485+
shard_inplace(
486+
layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group
487+
)
488+
shard_inplace(
489+
layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group
490+
)
491+
shard_inplace(
492+
layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group
493+
)
494+
shard_inplace(
495+
layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group
496+
)
497+
432498
@property
433499
def layers(self):
434500
return self.model.pipeline_layers

0 commit comments

Comments
 (0)