| import atexit |
| from dataclasses import fields |
| from time import perf_counter |
| from tqdm.auto import tqdm |
| from transformers import AutoTokenizer |
| import torch.multiprocessing as mp |
|
|
| from ..config import Config |
| from ..sampling_params import SamplingParams |
| from .sequence import Sequence |
| from .scheduler import Scheduler |
| from .block_manager import BlockManager |
| from .model_runner import ModelRunner |
|
|
|
|
| class LLMEngine: |
|
|
| def __init__(self, model, **kwargs): |
| model_object = kwargs.get("model_object", None) |
| graph_pool_handle = kwargs.get("graph_pool_handle", None) |
| config_fields = {field.name for field in fields(Config)} |
| config_kwargs = {k: v for k, v in kwargs.items() if k in config_fields} |
| config = Config(model, **config_kwargs) |
| self.config = config |
| self.ps = [] |
| self.events = [] |
| ctx = mp.get_context("spawn") |
| for i in range(1, config.tensor_parallel_size): |
| event = ctx.Event() |
| process = ctx.Process(target=ModelRunner, args=(config, i, event)) |
| process.start() |
| self.ps.append(process) |
| self.events.append(event) |
| self.model_runner = ModelRunner(config, 0, self.events, model_object=model_object, graph_pool_handle=graph_pool_handle) |
| tokenizer = kwargs.get("tokenizer", None) |
| if tokenizer is not None: |
| self.tokenizer = tokenizer |
| else: |
| self.tokenizer = AutoTokenizer.from_pretrained(config.model, use_fast=True) |
| config.eos = self.tokenizer.eos_token_id |
| self.scheduler = Scheduler(config) |
| self._exit_registered = False |
| self._closed = False |
| self._exited = False |
| self._atexit_callback = self.exit |
| atexit.register(self._atexit_callback) |
| self._exit_registered = True |
|
|
| def exit(self): |
| if self._exited: |
| return |
| self._exited = True |
| runner = getattr(self, "model_runner", None) |
| if runner is not None: |
| try: |
| runner.call("exit") |
| except Exception: |
| pass |
| try: |
| del self.model_runner |
| except Exception: |
| pass |
| for p in list(getattr(self, "ps", [])): |
| try: |
| p.join() |
| except Exception: |
| pass |
| self.ps = [] |
| self.events = [] |
|
|
| def close(self): |
| if self._closed: |
| return |
| self._closed = True |
| try: |
| self.reset_runtime_state() |
| except Exception: |
| pass |
| try: |
| self.clear_graph_cache() |
| except Exception: |
| pass |
| try: |
| self.exit() |
| except Exception: |
| pass |
| if self._exit_registered: |
| try: |
| atexit.unregister(self._atexit_callback) |
| except Exception: |
| pass |
| self._exit_registered = False |
| self._atexit_callback = None |
| self.scheduler = None |
| self.tokenizer = None |
|
|
| def __del__(self): |
| try: |
| self.close() |
| except Exception: |
| pass |
|
|
| def reset_runtime_state(self): |
| runner = getattr(self, "model_runner", None) |
| if runner is None: |
| return |
| runner.reset_runtime_state() |
| |
| |
| try: |
| self.reset() |
| except Exception: |
| pass |
| if self.scheduler is None: |
| return |
| self.scheduler.waiting.clear() |
| self.scheduler.running.clear() |
| self.scheduler.block_manager = BlockManager( |
| self.config.num_kvcache_blocks, |
| self.config.kvcache_block_size, |
| ) |
|
|
| def clear_graph_cache(self): |
| runner = getattr(self, "model_runner", None) |
| if runner is None: |
| return |
| runner.clear_graph_cache() |
|
|
| def add_request( |
| self, |
| prompt: str | list[int], |
| sampling_params: SamplingParams, |
| unconditional_prompt: str | list[int] | None = None, |
| prompt_embeds=None, |
| prompt_position_ids=None, |
| position_offset: int = 0, |
| unconditional_prompt_embeds=None, |
| unconditional_prompt_position_ids=None, |
| unconditional_position_offset: int = 0, |
| ): |
| if isinstance(prompt, str): |
| prompt = self.tokenizer.encode(prompt) |
| |
| if sampling_params.cfg_scale > 1.0: |
| if unconditional_prompt is None: |
| |
| |
| if isinstance(prompt, list): |
| |
| |
| unconditional_prompt = prompt |
| else: |
| unconditional_prompt = prompt |
| if isinstance(unconditional_prompt, str): |
| unconditional_prompt = self.tokenizer.encode(unconditional_prompt) |
| |
| uncond_seq = Sequence( |
| unconditional_prompt, |
| sampling_params, |
| is_unconditional=True, |
| prompt_embeds=unconditional_prompt_embeds, |
| prompt_position_ids=unconditional_prompt_position_ids, |
| position_offset=unconditional_position_offset, |
| ) |
| |
| cond_seq = Sequence( |
| prompt, |
| sampling_params, |
| is_unconditional=False, |
| conditional_seq=uncond_seq, |
| prompt_embeds=prompt_embeds, |
| prompt_position_ids=prompt_position_ids, |
| position_offset=position_offset, |
| ) |
| uncond_seq.paired_seq = cond_seq |
| |
| self.scheduler.add(cond_seq) |
| self.scheduler.add(uncond_seq) |
| else: |
| seq = Sequence( |
| prompt, |
| sampling_params, |
| prompt_embeds=prompt_embeds, |
| prompt_position_ids=prompt_position_ids, |
| position_offset=position_offset, |
| ) |
| self.scheduler.add(seq) |
|
|
| def step(self): |
| seqs, is_prefill = self.scheduler.schedule() |
| token_ids = self.model_runner.call("run", seqs, is_prefill) |
| self.scheduler.postprocess(seqs, token_ids) |
| |
| output_seqs = [seq for seq in seqs if seq.is_finished and (seq.cfg_scale <= 1.0 or not seq.is_unconditional)] |
| outputs = [(seq.seq_id, seq.completion_token_ids) for seq in output_seqs] |
| num_tokens = sum(len(seq) for seq in seqs) if is_prefill else -len([s for s in seqs if not s.is_unconditional]) |
| return outputs, num_tokens |
|
|
| def is_finished(self): |
| if self.scheduler is None: |
| return True |
| return self.scheduler.is_finished() |
|
|
| def reset(self): |
| """ |
| Reset the scheduler state and release all allocated blocks. |
| This should be called when an exception occurs during generation to prevent |
| KV cache block leaks that can cause 'deque index out of range' errors. |
| """ |
| |
| if self.scheduler is None: |
| return |
| while self.scheduler.running: |
| seq = self.scheduler.running.popleft() |
| if seq.block_table: |
| self.scheduler.block_manager.deallocate(seq) |
| |
| |
| while self.scheduler.waiting: |
| seq = self.scheduler.waiting.popleft() |
| if seq.block_table: |
| self.scheduler.block_manager.deallocate(seq) |
|
|
| def generate( |
| self, |
| prompts: list[str] | list[list[int]], |
| sampling_params: SamplingParams | list[SamplingParams], |
| use_tqdm: bool = True, |
| unconditional_prompts: list[str] | list[list[int]] | None = None, |
| ) -> list[str]: |
| if self.scheduler is None: |
| raise RuntimeError("LLM engine is closed.") |
| |
| self.model_runner.ensure_runtime_ready() |
| if (self.config.num_kvcache_blocks > 0 and |
| len(self.scheduler.block_manager.blocks) != self.config.num_kvcache_blocks): |
| self.scheduler.block_manager = BlockManager( |
| self.config.num_kvcache_blocks, |
| self.config.kvcache_block_size, |
| ) |
| |
| |
| if not self.is_finished(): |
| self.reset() |
| self.model_runner.reset_generation_state() |
| |
| if use_tqdm: |
| pbar = tqdm(total=len(prompts), desc="Generating", dynamic_ncols=True) |
| if not isinstance(sampling_params, list): |
| sampling_params = [sampling_params] * len(prompts) |
| |
| seed_to_apply = None |
| for sp in sampling_params: |
| seed_val = getattr(sp, "seed", None) |
| if seed_val is not None: |
| try: |
| seed_to_apply = int(seed_val) |
| break |
| except Exception: |
| seed_to_apply = None |
| self.model_runner.call("set_sampling_seed", seed_to_apply) |
| if unconditional_prompts is None: |
| unconditional_prompts = [None] * len(prompts) |
| for prompt, sp, uncond_prompt in zip(prompts, sampling_params, unconditional_prompts): |
| self.add_request(prompt, sp, uncond_prompt) |
| outputs = {} |
| prefill_throughput = decode_throughput = 0. |
| try: |
| while not self.is_finished(): |
| t = perf_counter() |
| output, num_tokens = self.step() |
| if use_tqdm: |
| if num_tokens > 0: |
| prefill_throughput = num_tokens / (perf_counter() - t) |
| else: |
| decode_throughput = -num_tokens / (perf_counter() - t) |
| pbar.set_postfix({ |
| "Prefill": f"{int(prefill_throughput)}tok/s", |
| "Decode": f"{int(decode_throughput)}tok/s", |
| }) |
| for seq_id, token_ids in output: |
| outputs[seq_id] = token_ids |
| if use_tqdm: |
| pbar.update(1) |
| except Exception: |
| |
| self.reset() |
| raise |
| finally: |
| if use_tqdm: |
| pbar.close() |
| |
| outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())] |
| outputs = [{"text": self.tokenizer.decode(token_ids), "token_ids": token_ids} for token_ids in outputs] |
| return outputs |
|
|
| def generate_embedded( |
| self, |
| prompts: list[list[int]], |
| prompt_embeds: list, |
| prompt_position_ids: list | None, |
| sampling_params: SamplingParams | list[SamplingParams], |
| position_offsets: list[int] | None = None, |
| use_tqdm: bool = True, |
| ): |
| if self.scheduler is None: |
| raise RuntimeError("LLM engine is closed.") |
| self.model_runner.ensure_runtime_ready() |
| if ( |
| self.config.num_kvcache_blocks > 0 |
| and len(self.scheduler.block_manager.blocks) != self.config.num_kvcache_blocks |
| ): |
| self.scheduler.block_manager = BlockManager( |
| self.config.num_kvcache_blocks, |
| self.config.kvcache_block_size, |
| ) |
| if not self.is_finished(): |
| self.reset() |
| self.model_runner.reset_generation_state() |
| if not isinstance(sampling_params, list): |
| sampling_params = [sampling_params] * len(prompts) |
| if prompt_position_ids is None: |
| prompt_position_ids = [None] * len(prompts) |
| if position_offsets is None: |
| position_offsets = [0] * len(prompts) |
| if use_tqdm: |
| pbar = tqdm(total=len(prompts), desc="Generating", dynamic_ncols=True) |
| seed_to_apply = None |
| for sp in sampling_params: |
| seed_val = getattr(sp, "seed", None) |
| if seed_val is not None: |
| try: |
| seed_to_apply = int(seed_val) |
| break |
| except Exception: |
| seed_to_apply = None |
| self.model_runner.call("set_sampling_seed", seed_to_apply) |
| for prompt, embeds, pos_ids, pos_offset, sp in zip(prompts, prompt_embeds, prompt_position_ids, position_offsets, sampling_params): |
| self.add_request( |
| prompt, |
| sp, |
| prompt_embeds=embeds, |
| prompt_position_ids=pos_ids, |
| position_offset=pos_offset, |
| ) |
| outputs = {} |
| prefill_throughput = decode_throughput = 0.0 |
| try: |
| while not self.is_finished(): |
| t = perf_counter() |
| output, num_tokens = self.step() |
| if use_tqdm: |
| if num_tokens > 0: |
| prefill_throughput = num_tokens / (perf_counter() - t) |
| else: |
| decode_throughput = -num_tokens / (perf_counter() - t) |
| pbar.set_postfix({ |
| "Prefill": f"{int(prefill_throughput)}tok/s", |
| "Decode": f"{int(decode_throughput)}tok/s", |
| }) |
| for seq_id, token_ids in output: |
| outputs[seq_id] = token_ids |
| if use_tqdm: |
| pbar.update(1) |
| except Exception: |
| self.reset() |
| raise |
| finally: |
| if use_tqdm: |
| pbar.close() |
| outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())] |
| outputs = [{"text": self.tokenizer.decode(token_ids), "token_ids": token_ids} for token_ids in outputs] |
| return outputs |
|
|