[MLX] Add native MLX execution backend for Apple Silicon Mac (#20342)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
This commit is contained in:
@@ -88,6 +88,7 @@ from sglang.srt.utils import (
|
||||
suppress_other_loggers,
|
||||
)
|
||||
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
||||
from sglang.srt.utils.tensor_bridge import use_mlx
|
||||
|
||||
|
||||
def start_profile(profile_activities, profile_record_shapes=False, rank_print=print):
|
||||
@@ -262,7 +263,7 @@ def load_model(server_args, port_args, gpu_id, tp_rank):
|
||||
moe_ep_rank = tp_rank // (server_args.tp_size // server_args.ep_size)
|
||||
|
||||
model_config = ModelConfig.from_server_args(server_args)
|
||||
model_runner = ModelRunner(
|
||||
runner_kwargs = dict(
|
||||
model_config=model_config,
|
||||
mem_fraction_static=server_args.mem_fraction_static,
|
||||
gpu_id=gpu_id,
|
||||
@@ -275,6 +276,16 @@ def load_model(server_args, port_args, gpu_id, tp_rank):
|
||||
nccl_port=port_args.nccl_port,
|
||||
server_args=server_args,
|
||||
)
|
||||
|
||||
_use_mlx = use_mlx()
|
||||
if _use_mlx:
|
||||
from sglang.srt.hardware_backend.mlx.model_runner_stub import (
|
||||
MlxModelRunnerStub,
|
||||
)
|
||||
|
||||
model_runner = MlxModelRunnerStub(**runner_kwargs)
|
||||
else:
|
||||
model_runner = ModelRunner(**runner_kwargs)
|
||||
rank_print(f"max_total_num_tokens={model_runner.max_total_num_tokens}")
|
||||
tokenizer = get_tokenizer(
|
||||
server_args.tokenizer_path,
|
||||
@@ -283,6 +294,12 @@ def load_model(server_args, port_args, gpu_id, tp_rank):
|
||||
)
|
||||
if server_args.tp_size > 1:
|
||||
dist.barrier()
|
||||
|
||||
if _use_mlx:
|
||||
model_runner = _MlxBenchRunner(model_runner, server_args)
|
||||
else:
|
||||
model_runner = _TorchBenchRunner(model_runner)
|
||||
|
||||
return model_runner, tokenizer
|
||||
|
||||
|
||||
@@ -337,11 +354,12 @@ def prepare_extend_inputs_for_correctness_test(
|
||||
for i in range(len(reqs)):
|
||||
req: Req = reqs[i]
|
||||
req.fill_ids += input_ids[i][bench_args.cut_len :]
|
||||
req.prefix_indices = model_runner.req_to_token_pool.req_to_token[
|
||||
i, : bench_args.cut_len
|
||||
].to(req.prefix_indices.dtype)
|
||||
req.logprob_start_len = -1
|
||||
req.set_extend_input_len(len(req.fill_ids) - len(req.prefix_indices))
|
||||
if model_runner is not None:
|
||||
req.prefix_indices = model_runner.req_to_token_pool.req_to_token[
|
||||
i, : bench_args.cut_len
|
||||
].to(req.prefix_indices.dtype)
|
||||
req.logprob_start_len = -1
|
||||
req.set_extend_input_len(len(req.fill_ids) - len(req.prefix_indices))
|
||||
return reqs
|
||||
|
||||
|
||||
@@ -445,6 +463,69 @@ def _maybe_prepare_mlp_sync_batch(batch: ScheduleBatch, model_runner):
|
||||
)
|
||||
|
||||
|
||||
class _TorchBenchRunner:
|
||||
"""Wraps ModelRunner for the standard PyTorch benchmark path."""
|
||||
|
||||
def __init__(self, model_runner):
|
||||
self.torch_runner = model_runner
|
||||
|
||||
def clear(self):
|
||||
self.torch_runner.req_to_token_pool.clear()
|
||||
self.torch_runner.token_to_kv_pool_allocator.clear()
|
||||
|
||||
def extend(self, reqs):
|
||||
return extend(reqs, self.torch_runner)
|
||||
|
||||
def decode(self, next_token_ids, batch):
|
||||
return decode(next_token_ids, batch, self.torch_runner)
|
||||
|
||||
def cleanup(self, batch):
|
||||
pass
|
||||
|
||||
def synchronize(self):
|
||||
synchronize(self.torch_runner.device)
|
||||
|
||||
def max_batch_size(self, input_len, output_len):
|
||||
return self.torch_runner.max_total_num_tokens // (input_len + output_len)
|
||||
|
||||
|
||||
class _MlxBenchRunner:
|
||||
"""Wraps MlxModelRunner for the MLX benchmark path."""
|
||||
|
||||
def __init__(self, model_runner, server_args):
|
||||
from sglang.srt.hardware_backend.mlx.model_runner import MlxModelRunner
|
||||
|
||||
self.mlx_runner = MlxModelRunner(
|
||||
model_path=server_args.model_path,
|
||||
trust_remote_code=server_args.trust_remote_code,
|
||||
)
|
||||
self.fake_torch_runner = model_runner
|
||||
|
||||
def clear(self):
|
||||
self.mlx_runner.clear()
|
||||
|
||||
def extend(self, reqs):
|
||||
req_ids = [str(req.rid) for req in reqs]
|
||||
token_ids_list = [[int(t) for t in req.fill_ids] for req in reqs]
|
||||
next_token_ids = self.mlx_runner.prefill_batch(req_ids, token_ids_list)
|
||||
return torch.tensor(next_token_ids), None, req_ids
|
||||
|
||||
def decode(self, next_token_ids, req_ids):
|
||||
next_token_ids = self.mlx_runner.decode_batch(req_ids)
|
||||
return torch.tensor(next_token_ids), None
|
||||
|
||||
def cleanup(self, batch):
|
||||
if isinstance(batch, list):
|
||||
for req_id in batch:
|
||||
self.mlx_runner.remove_request(req_id)
|
||||
|
||||
def synchronize(self):
|
||||
pass
|
||||
|
||||
def max_batch_size(self, input_len, output_len):
|
||||
return self.fake_torch_runner.max_total_num_tokens // (input_len + output_len)
|
||||
|
||||
|
||||
def _read_prompts_from_file(prompt_file, rank_print):
|
||||
"""Read custom prompts from the file specified by `--prompt-filename`."""
|
||||
if not prompt_file:
|
||||
@@ -504,26 +585,30 @@ def correctness_test(
|
||||
|
||||
if bench_args.cut_len > 0:
|
||||
# Prefill
|
||||
next_token_ids, next_token_logits, batch = extend(reqs, model_runner)
|
||||
next_token_ids, next_token_logits, batch = model_runner.extend(reqs)
|
||||
rank_print(f"prefill logits (first half): {next_token_logits} \n")
|
||||
|
||||
# Prepare extend inputs
|
||||
reqs = prepare_extend_inputs_for_correctness_test(
|
||||
bench_args, input_ids, reqs, model_runner
|
||||
)
|
||||
# Prepare extend inputs
|
||||
torch_runner = getattr(model_runner, "torch_runner", None)
|
||||
reqs = prepare_extend_inputs_for_correctness_test(
|
||||
bench_args, input_ids, reqs, torch_runner
|
||||
)
|
||||
|
||||
# Extend (prefill w/ KV cache)
|
||||
next_token_ids, next_token_logits, batch = extend(reqs, model_runner)
|
||||
next_token_ids, next_token_logits, batch = model_runner.extend(reqs)
|
||||
rank_print(f"prefill logits (final): {next_token_logits} \n")
|
||||
|
||||
# Decode
|
||||
output_ids = [input_ids[i] + [next_token_ids[i]] for i in range(len(input_ids))]
|
||||
for _ in range(bench_args.output_len[0] - 1):
|
||||
next_token_ids, _ = decode(next_token_ids, batch, model_runner)
|
||||
next_token_ids, _ = model_runner.decode(next_token_ids, batch)
|
||||
next_token_ids_list = next_token_ids.tolist()
|
||||
for i in range(len(reqs)):
|
||||
output_ids[i].append(next_token_ids_list[i])
|
||||
|
||||
# Clean up
|
||||
model_runner.cleanup(batch)
|
||||
|
||||
# Print output texts
|
||||
for i in range(len(reqs)):
|
||||
rank_print(f"========== Prompt {i} ==========")
|
||||
@@ -542,7 +627,6 @@ def latency_test_run_once(
|
||||
batch_size,
|
||||
input_len,
|
||||
output_len,
|
||||
device,
|
||||
log_decode_step,
|
||||
profile,
|
||||
profile_record_shapes,
|
||||
@@ -553,15 +637,14 @@ def latency_test_run_once(
|
||||
profile_start_step=None,
|
||||
profile_steps=None,
|
||||
):
|
||||
max_batch_size = model_runner.max_total_num_tokens // (input_len + output_len)
|
||||
max_batch_size = model_runner.max_batch_size(input_len, output_len)
|
||||
if batch_size > max_batch_size:
|
||||
rank_print(
|
||||
f"skipping ({batch_size}, {input_len}, {output_len}) due to max batch size limit"
|
||||
)
|
||||
return
|
||||
|
||||
model_runner.req_to_token_pool.clear()
|
||||
model_runner.token_to_kv_pool_allocator.clear()
|
||||
model_runner.clear()
|
||||
|
||||
measurement_results = {
|
||||
"run_name": run_name,
|
||||
@@ -581,10 +664,10 @@ def latency_test_run_once(
|
||||
rank_print=rank_print,
|
||||
)
|
||||
|
||||
synchronize(device)
|
||||
model_runner.synchronize()
|
||||
tic = time.perf_counter()
|
||||
next_token_ids, _, batch = extend(reqs, model_runner)
|
||||
synchronize(device)
|
||||
next_token_ids, _, batch = model_runner.extend(reqs)
|
||||
model_runner.synchronize()
|
||||
prefill_latency = time.perf_counter() - tic
|
||||
|
||||
if enable_profile_prefill:
|
||||
@@ -617,7 +700,7 @@ def latency_test_run_once(
|
||||
enable_profile_decode = profile and profile_stage in ["all", "decode"]
|
||||
profiler = None
|
||||
for i in range(output_len - 1):
|
||||
synchronize(device)
|
||||
model_runner.synchronize()
|
||||
# Start profiler at the specified step
|
||||
if enable_profile_decode and i == profile_start:
|
||||
profiler = start_profile(
|
||||
@@ -627,8 +710,8 @@ def latency_test_run_once(
|
||||
)
|
||||
|
||||
tic = time.perf_counter()
|
||||
next_token_ids, _ = decode(next_token_ids, batch, model_runner)
|
||||
synchronize(device)
|
||||
next_token_ids, _ = model_runner.decode(next_token_ids, batch)
|
||||
model_runner.synchronize()
|
||||
latency = time.perf_counter() - tic
|
||||
|
||||
# Stop profiler after the specified number of steps
|
||||
@@ -670,6 +753,8 @@ def latency_test_run_once(
|
||||
)
|
||||
measurement_results["total_latency"] = tot_latency
|
||||
measurement_results["overall_throughput"] = throughput
|
||||
|
||||
model_runner.cleanup(batch)
|
||||
return measurement_results
|
||||
|
||||
|
||||
@@ -712,7 +797,6 @@ def latency_test(
|
||||
bench_args.batch_size[0],
|
||||
bench_args.input_len[0],
|
||||
min(32, bench_args.output_len[0]), # shorter decoding to speed up the warmup
|
||||
server_args.device,
|
||||
log_decode_step=0,
|
||||
profile=False,
|
||||
profile_record_shapes=False,
|
||||
@@ -764,7 +848,6 @@ def latency_test(
|
||||
bs,
|
||||
il,
|
||||
ol,
|
||||
server_args.device,
|
||||
bench_args.log_decode_step,
|
||||
bench_args.profile if tp_rank == 0 else None,
|
||||
bench_args.profile_record_shapes if tp_rank == 0 else None,
|
||||
|
||||
Reference in New Issue
Block a user