from typing import Callable, List, Optional # noqa: UP035 import numpy as np from mlc_llm.protocol.generation_config import GenerationConfig from mlc_llm.serve import Request, RequestStreamOutput, data from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine from mlc_llm.testing import require_test_model prompts = [ "What is the meaning of life?", "Introduce the history of Pittsburgh to me. Please elaborate in detail.", "Write a three-day Seattle travel plan. Please elaborate in detail.", "What is Alaska famous of? Please elaborate in detail.", "What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501 "What are the necessary components to assemble a desktop computer? Please elaborate in detail.", "Why is Vitamin D important to human beings? Please elaborate in detail.", "Where is milk tea originated from? Please elaborate in detail.", "Where is the southernmost place in United States? Please elaborate in detail.", "Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501 ] def create_requests( engine: SyncMLCEngine, num_requests: int, stop_token_id: Optional[int] = None, temperature: float = 0.8, repetition_penalty: float = 1.0, max_tokens_low: int = 256, max_tokens_high: int = 257, ) -> List[Request]: # noqa: UP006 assert num_requests >= 0 and num_requests <= len(prompts) stop_token_ids = [stop_token_id] if stop_token_id is not None else [] requests = [] for req_id, prompt in zip(range(num_requests), prompts): max_tokens = np.random.randint(max_tokens_low, max_tokens_high) requests.append( engine.create_request( request_id=str(req_id), inputs=data.TextData(prompt), generation_config=GenerationConfig( temperature=temperature, repetition_penalty=repetition_penalty, max_tokens=max_tokens, stop_token_ids=stop_token_ids, ), ) ) return requests @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_basic(model: str): """Test engine **without continuous batching**. - Add all requests to the engine altogether in the beginning. - All requests have the same max_tokens. This means all requests will end together. - Engine keeps running `step` for estimated number of steps (number of requests + max_tokens - 1). Then check the output of each request. """ # Hyperparameters for tests (you can try different combinations). num_requests = 10 # [4, 8, 10] temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1] repetition_penalty = 1.0 # [1.0, 1.01] max_tokens: int = 256 # [32, 128, 256] np.random.seed(0) # Output list outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006 # Define the callback function for request generation results def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006 for delta_output in delta_outputs: request_id, stream_outputs = delta_output.unpack() assert len(stream_outputs) == 1 outputs[int(request_id)] += stream_outputs[0].delta_token_ids # Create engine engine = SyncMLCEngine( model=model, mode="server", request_stream_callback=fcallback, ) # Create requests requests = create_requests( engine, num_requests, temperature=temperature, repetition_penalty=repetition_penalty, max_tokens_low=max_tokens, max_tokens_high=max_tokens + 1, ) # Add all requests to engine for request in requests: engine.add_request(request) num_steps = num_requests + max_tokens - 1 # Run steps for step in range(num_steps): engine.step() for req_id, output in enumerate(outputs): print(f"Prompt {req_id}: {requests[req_id].inputs[0]}") print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n") @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_continuous_batching_1(model: str): """Test engine **with continuous batching**. - Add all requests to the engine altogether in the beginning. - All requests have a random maximum generation length. So each request keeps generating until reaching the maximum length. - Engine keeps running `step` for estimated number of steps (number of requests + the maximum max_tokens - 1). Then check the output of each request. """ # Hyperparameters for tests (you can try different combinations) num_requests = 10 # [4, 8, 10] temperature = 0.9 # [0.8, 0.9, 1.0, 1.1] repetition_penalty = 1.00 # [1.0, 1.01] max_tokens_low = 128 max_tokens_high = 384 np.random.seed(0) # Output list outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006 finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006 # Define the callback class for request generation results class CallbackTimer: timer: int = -1 def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006 def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006 for delta_output in delta_outputs: request_id, stream_outputs = delta_output.unpack() assert len(stream_outputs) == 1 if stream_outputs[0].finish_reason is not None: print(f"Request {request_id} finished at step {self.timer}.") outputs[int(request_id)] += stream_outputs[0].delta_token_ids finish_time[int(request_id)] = self.timer return fcallback def step(self) -> None: self.timer += 1 # Create engine timer = CallbackTimer() engine = SyncMLCEngine( model=model, mode="server", request_stream_callback=timer.callback_getter(), ) # Create requests requests = create_requests( engine, num_requests, temperature=temperature, repetition_penalty=repetition_penalty, max_tokens_low=max_tokens_low, max_tokens_high=max_tokens_high, ) # Add all requests to engine for request in requests: engine.add_request(request) num_steps = num_requests + max(request.generation_config.max_tokens for request in requests) - 1 # Run steps for step in range(num_steps): timer.step() assert timer.timer == step engine.step() for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)): print(f"Prompt {req_id}: {request.inputs[0]}") print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n") assert fin_time == request.generation_config.max_tokens - 1, ( f"finish time = {fin_time}, max tokens = {request.generation_config.max_tokens - 1}" ) @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_continuous_batching_2(model: str): """Test engine **with continuous batching**. - Add all requests to the engine altogether in the beginning. - All requests have the stop token. So each request keeps generating until having the stop token or reaching the maximum length. - Engine keeps running `step` for estimated number of steps (number of requests + the maximum max_tokens - 1). Then check the output of each request. """ # Hyperparameters for tests (you can try different combinations) num_requests = 10 # [4, 8, 10] temperature = 0.9 # [0.8, 0.9, 1.0, 1.1] repetition_penalty = 1.00 # [1.0, 1.01] stop_token_id = 2 max_tokens = 512 np.random.seed(0) # Output list outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006 finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006 # Define the callback class for request generation results class CallbackTimer: timer: int = -1 def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006 def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006 for delta_output in delta_outputs: request_id, stream_outputs = delta_output.unpack() assert len(stream_outputs) == 1 if stream_outputs[0].finish_reason is not None: print(f"Request {request_id} finished at step {self.timer}.") outputs[int(request_id)] += stream_outputs[0].delta_token_ids finish_time[int(request_id)] = self.timer return fcallback def step(self) -> None: self.timer += 1 # Create engine timer = CallbackTimer() engine = SyncMLCEngine( model=model, mode="server", request_stream_callback=timer.callback_getter(), ) # Create requests requests = create_requests( engine, num_requests, stop_token_id=stop_token_id, temperature=temperature, repetition_penalty=repetition_penalty, max_tokens_low=max_tokens, max_tokens_high=max_tokens + 1, ) # Add all requests to engine for request in requests: engine.add_request(request) num_steps = num_requests + max_tokens - 1 # Run steps for step in range(num_steps): timer.step() assert timer.timer == step engine.step() for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)): print(f"Prompt {req_id}: {request.inputs[0]}") if fin_time < num_requests + max_tokens - 2: print(f"Request {req_id} ends early on the stop token") print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n") @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_continuous_batching_3(model: str): """Test engine **with continuous batching**. - Add requests randomly between time [0, 200). - All requests have a random maximum generation length. So each request keeps generating until reaching the maximum length. - Engine keeps running `step` until all requests finish. Then check the output of each request. """ # Hyperparameters for tests (you can try different combinations) num_requests = 10 # [4, 8, 10] temperature = 0.9 # [0.8, 0.9, 1.0, 1.1] repetition_penalty = 1.00 # [1.0, 1.01] stop_token_id = 2 max_tokens_low = 64 max_tokens_high = 192 np.random.seed(0) # Output list outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006 finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006 # Define the callback class for request generation results class CallbackTimer: timer: int = -1 finished_requests: int = 0 def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006 def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006 for delta_output in delta_outputs: request_id, stream_outputs = delta_output.unpack() assert len(stream_outputs) == 1 if stream_outputs[0].finish_reason is not None: print(f"Request {request_id} finished at step {self.timer}.") self.finished_requests += 1 outputs[int(request_id)] += stream_outputs[0].delta_token_ids finish_time[int(request_id)] = self.timer return fcallback def step(self) -> None: self.timer += 1 def all_finished(self) -> bool: return self.finished_requests == num_requests # Create engine timer = CallbackTimer() engine = SyncMLCEngine( model=model, mode="server", request_stream_callback=timer.callback_getter(), ) # Create requests requests = create_requests( engine, num_requests, stop_token_id=stop_token_id, temperature=temperature, repetition_penalty=repetition_penalty, max_tokens_low=max_tokens_low, max_tokens_high=max_tokens_high, ) # Assign the time to add requests to engine request_add_time = [np.random.randint(0, 200) for _ in range(num_requests)] # Run steps while not timer.all_finished(): timer.step() # Add requests to engine for req_id, add_time in enumerate(request_add_time): if add_time == timer.timer: print(f"add request {req_id} at step {timer.timer}") engine.add_request(requests[req_id]) engine.step() for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)): print(f"Prompt {req_id}: {request.inputs[0]}") print(f"Finish time: {fin_time}") print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n") @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_generate(model: str): # Create engine engine = SyncMLCEngine( model=model, mode="server", engine_config=EngineConfig(max_total_sequence_length=4096), ) num_requests = 10 max_tokens = 256 # Generate output. output_texts, _ = engine.generate( prompts[:num_requests], GenerationConfig(max_tokens=max_tokens, n=7) ) for req_id, outputs in enumerate(output_texts): print(f"Prompt {req_id}: {prompts[req_id]}") if len(outputs) == 1: print(f"Output {req_id}:{outputs[0]}\n") else: for i, output in enumerate(outputs): print(f"Output {req_id}({i}):{output}\n") @require_test_model("Llama-2-7b-chat-hf-q0f16-MLC") def test_engine_hybrid_prefill(model: str): """Test engine **with hybrid prefill**. - Add each single request step by step. - All requests have the same generation length. But due to hybrid prefill, the earlier request will decode with later request prefill, in single step. So each request lasts the same steps, and stops generation step by step as well. - Engine keeps running `step` for the generation length, to finish the last request. Then check the output of each request. """ # Hyperparameters for tests (you can try different combinations) num_requests = 10 # [4, 8, 10] temperature = 0.9 # [0.8, 0.9, 1.0, 1.1] repetition_penalty = 1.00 # [1.0, 1.01] max_tokens = 15 np.random.seed(0) # Output list outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006 finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006 # Define the callback class for request generation results class CallbackTimer: timer: int = -1 def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006 def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006 for delta_output in delta_outputs: request_id, stream_outputs = delta_output.unpack() assert len(stream_outputs) == 1 if stream_outputs[0].finish_reason is not None: print(f"Request {request_id} finished at step {self.timer}.") outputs[int(request_id)] += stream_outputs[0].delta_token_ids finish_time[int(request_id)] = self.timer return fcallback def step(self) -> None: self.timer += 1 # Create engine timer = CallbackTimer() engine = SyncMLCEngine( model=model, mode="server", request_stream_callback=timer.callback_getter(), engine_config=EngineConfig(prefill_mode="hybrid"), ) # Create requests requests = create_requests( engine, num_requests, temperature=temperature, repetition_penalty=repetition_penalty, max_tokens_low=max_tokens, max_tokens_high=max_tokens + 1, ) # Add all requests to engine step by step for step, request in enumerate(requests): engine.add_request(request) timer.step() assert timer.timer == step engine.step() # Run steps for step in range(max_tokens): timer.step() assert timer.timer == step + num_requests engine.step() for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)): print(f"Prompt {req_id}: {request.inputs[0]}") print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n") assert fin_time == req_id + request.generation_config.max_tokens - 1, ( f"finish time = {fin_time}, max tokens = {req_id + request.generation_config.max_tokens - 1}" # noqa: E501 ) if __name__ == "__main__": test_engine_basic() test_engine_continuous_batching_1() test_engine_continuous_batching_2() test_engine_continuous_batching_3() test_engine_generate() test_engine_hybrid_prefill()