CoolFace
Apppublic

FoundationVision/LlamaGen

sourceHugging Facemitupdated 2y agoView on Hugging Face
64likes
llm_engine.py671 linesDownload Raw Back to serve
1# Modified from:2#   vLLM:    https://github.com/vllm-project/vllm/blob/main/vllm/engine/llm_engine.py3import time4from typing import Iterable, List, Optional, Type, Union5import argparse6 7from transformers import GenerationConfig, PreTrainedTokenizer8 9import vllm10from vllm.config import (CacheConfig, DecodingConfig, DeviceConfig, LoadConfig,11                         LoRAConfig, ModelConfig, ParallelConfig,12                         SchedulerConfig, SpeculativeConfig,13                         VisionLanguageConfig)14from vllm.core.scheduler import Scheduler, SchedulerOutputs15from vllm.engine.arg_utils import EngineArgs16from vllm.engine.metrics import StatLogger, Stats17from vllm.engine.output_processor.interfaces import (18    SequenceGroupOutputProcessor)19from vllm.engine.output_processor.stop_checker import StopChecker20from vllm.engine.output_processor.util import create_output_by_sequence_group21from vllm.engine.ray_utils import initialize_ray_cluster22from vllm.executor.executor_base import ExecutorBase23from vllm.logger import init_logger24from vllm.lora.request import LoRARequest25from vllm.outputs import RequestOutput26from vllm.sampling_params import SamplingParams27from vllm.sequence import (MultiModalData, SamplerOutput, Sequence,28                           SequenceGroup)29from vllm.transformers_utils.detokenizer import Detokenizer30from vllm.transformers_utils.tokenizer_group import (BaseTokenizerGroup,31                                                     get_tokenizer_group)32from vllm.usage.usage_lib import (UsageContext, is_usage_stats_enabled,33                                  usage_message)34from vllm.utils import Counter35 36logger = init_logger(__name__)37_LOCAL_LOGGING_INTERVAL_SEC = 538 39 40def _load_generation_config_dict(model_config: ModelConfig):41    try:42        return GenerationConfig.from_pretrained(43            model_config.model,44            revision=model_config.revision,45        ).to_diff_dict()46    except OSError:47        # Not found.48        return {}49 50 51class LLMEngine:52    """An LLM engine that receives requests and generates texts.53 54    This is the main class for the vLLM engine. It receives requests55    from clients and generates texts from the LLM. It includes a tokenizer, a56    language model (possibly distributed across multiple GPUs), and GPU memory57    space allocated for intermediate states (aka KV cache). This class utilizes58    iteration-level scheduling and efficient memory management to maximize the59    serving throughput.60 61    The `LLM` class wraps this class for offline batched inference and the62    `AsyncLLMEngine` class wraps this class for online serving.63 64    NOTE: The config arguments are derived from the `EngineArgs` class. For the65    comprehensive list of arguments, see `EngineArgs`.66 67    Args:68        model_config: The configuration related to the LLM model.69        cache_config: The configuration related to the KV cache memory70            management.71        parallel_config: The configuration related to distributed execution.72        scheduler_config: The configuration related to the request scheduler.73        device_config: The configuration related to the device.74        lora_config (Optional): The configuration related to serving multi-LoRA.75        vision_language_config (Optional): The configuration related to vision76            language models.77        speculative_config (Optional): The configuration related to speculative78            decoding.79        executor_class: The model executor class for managing distributed80            execution.81        log_stats: Whether to log statistics.82        usage_context: Specified entry point, used for usage info collection83    """84 85    def __init__(86        self,87        args: argparse.ArgumentParser,88        model_config: ModelConfig,89        cache_config: CacheConfig,90        parallel_config: ParallelConfig,91        scheduler_config: SchedulerConfig,92        device_config: DeviceConfig,93        load_config: LoadConfig,94        lora_config: Optional[LoRAConfig],95        vision_language_config: Optional[VisionLanguageConfig],96        speculative_config: Optional[SpeculativeConfig],97        decoding_config: Optional[DecodingConfig],98        executor_class: Type[ExecutorBase],99        log_stats: bool,100        usage_context: UsageContext = UsageContext.ENGINE_CONTEXT,101    ) -> None:102        logger.info(103            f"Initializing an LLM engine (v{vllm.__version__}) with config: "104            f"model={model_config.model!r}, "105            f"speculative_config={speculative_config!r}, "106            f"tokenizer={model_config.tokenizer!r}, "107            f"skip_tokenizer_init={model_config.skip_tokenizer_init}, "108            f"tokenizer_mode={model_config.tokenizer_mode}, "109            f"revision={model_config.revision}, "110            f"tokenizer_revision={model_config.tokenizer_revision}, "111            f"trust_remote_code={model_config.trust_remote_code}, "112            f"dtype={model_config.dtype}, "113            f"max_seq_len={model_config.max_model_len}, "114            f"download_dir={load_config.download_dir!r}, "115            f"load_format={load_config.load_format}, "116            f"tensor_parallel_size={parallel_config.tensor_parallel_size}, "117            f"disable_custom_all_reduce="118            f"{parallel_config.disable_custom_all_reduce}, "119            f"quantization={model_config.quantization}, "120            f"enforce_eager={model_config.enforce_eager}, "121            f"kv_cache_dtype={cache_config.cache_dtype}, "122            f"quantization_param_path={model_config.quantization_param_path}, "123            f"device_config={device_config.device}, "124            f"decoding_config={decoding_config!r}, "125            f"seed={model_config.seed})")126        # TODO(woosuk): Print more configs in debug mode.127 128        self.model_config = model_config129        self.cache_config = cache_config130        self.lora_config = lora_config131        self.vision_language_config = vision_language_config132        self.parallel_config = parallel_config133        self.scheduler_config = scheduler_config134        self.device_config = device_config135        self.speculative_config = speculative_config136        self.load_config = load_config137        self.decoding_config = decoding_config or DecodingConfig()138        self.log_stats = log_stats139 140        if not self.model_config.skip_tokenizer_init:141            self.tokenizer: BaseTokenizerGroup142            self._init_tokenizer()143            self.detokenizer = Detokenizer(self.tokenizer)144        else:145            self.detokenizer = None146            self.tokenizer = None147 148        self.seq_counter = Counter()149        self.generation_config_fields = _load_generation_config_dict(150            model_config)151 152        self.model_executor = executor_class(153            args=args,154            model_config=model_config,155            cache_config=cache_config,156            parallel_config=parallel_config,157            scheduler_config=scheduler_config,158            device_config=device_config,159            lora_config=lora_config,160            vision_language_config=vision_language_config,161            speculative_config=speculative_config,162            load_config=load_config,163        )164 165        self._initialize_kv_caches()166 167        # If usage stat is enabled, collect relevant info.168        if is_usage_stats_enabled():169            from vllm.model_executor.model_loader import (170                get_architecture_class_name)171            usage_message.report_usage(172                get_architecture_class_name(model_config),173                usage_context,174                extra_kvs={175                    # Common configuration176                    "dtype":177                    str(model_config.dtype),178                    "tensor_parallel_size":179                    parallel_config.tensor_parallel_size,180                    "block_size":181                    cache_config.block_size,182                    "gpu_memory_utilization":183                    cache_config.gpu_memory_utilization,184 185                    # Quantization186                    "quantization":187                    model_config.quantization,188                    "kv_cache_dtype":189                    cache_config.cache_dtype,190 191                    # Feature flags192                    "enable_lora":193                    bool(lora_config),194                    "enable_prefix_caching":195                    cache_config.enable_prefix_caching,196                    "enforce_eager":197                    model_config.enforce_eager,198                    "disable_custom_all_reduce":199                    parallel_config.disable_custom_all_reduce,200                })201 202        if self.tokenizer:203            # Ping the tokenizer to ensure liveness if it runs in a204            # different process.205            self.tokenizer.ping()206 207        # Create the scheduler.208        # NOTE: the cache_config here have been updated with the numbers of209        # GPU and CPU blocks, which are profiled in the distributed executor.210        self.scheduler = Scheduler(scheduler_config, cache_config, lora_config)211 212        # Metric Logging.213        if self.log_stats:214            self.stat_logger = StatLogger(215                local_interval=_LOCAL_LOGGING_INTERVAL_SEC,216                labels=dict(model_name=model_config.model))217            self.stat_logger.info("cache_config", self.cache_config)218 219        # Create sequence output processor, e.g. for beam search or220        # speculative decoding.221        self.output_processor = (222            SequenceGroupOutputProcessor.create_output_processor(223                self.scheduler_config,224                self.detokenizer,225                self.scheduler,226                self.seq_counter,227                self.get_tokenizer_for_seq,228                stop_checker=StopChecker(229                    self.scheduler_config.max_model_len,230                    self.get_tokenizer_for_seq,231                ),232            ))233 234    def _initialize_kv_caches(self) -> None:235        """Initialize the KV cache in the worker(s).236 237        The workers will determine the number of blocks in both the GPU cache238        and the swap CPU cache.239        """240        num_gpu_blocks, num_cpu_blocks = (241            self.model_executor.determine_num_available_blocks())242 243        if self.cache_config.num_gpu_blocks_override is not None:244            num_gpu_blocks_override = self.cache_config.num_gpu_blocks_override245            logger.info(f"Overriding {num_gpu_blocks=} with "246                        f"{num_gpu_blocks_override=}")247            num_gpu_blocks = num_gpu_blocks_override248 249        self.cache_config.num_gpu_blocks = num_gpu_blocks250        self.cache_config.num_cpu_blocks = num_cpu_blocks251 252        self.model_executor.initialize_cache(num_gpu_blocks, num_cpu_blocks)253 254    @classmethod255    def from_engine_args(256        cls,257        engine_args: EngineArgs,258        usage_context: UsageContext = UsageContext.ENGINE_CONTEXT,259        args: argparse.ArgumentParser = None,260    ) -> "LLMEngine":261        """Creates an LLM engine from the engine arguments."""262        # Create the engine configs.263        engine_config = engine_args.create_engine_config()264 265        # Initialize the cluster and specify the executor class.266        if engine_config.device_config.device_type == "neuron":267            from vllm.executor.neuron_executor import NeuronExecutor268            executor_class = NeuronExecutor269        elif engine_config.device_config.device_type == "cpu":270            from vllm.executor.cpu_executor import CPUExecutor271            executor_class = CPUExecutor272        elif engine_config.parallel_config.worker_use_ray:273            initialize_ray_cluster(engine_config.parallel_config)274            from vllm.executor.ray_gpu_executor import RayGPUExecutor275            executor_class = RayGPUExecutor276        else:277            assert engine_config.parallel_config.world_size == 1, (278                "Ray is required if parallel_config.world_size > 1.")279            # from vllm.executor.gpu_executor import GPUExecutor280            from serve.gpu_executor import GPUExecutor281            executor_class = GPUExecutor282 283        # Create the LLM engine.284        engine = cls(285            **engine_config.to_dict(),286            executor_class=executor_class,287            log_stats=not engine_args.disable_log_stats,288            usage_context=usage_context,289            args=args,290        )291        return engine292 293    def __reduce__(self):294        # This is to ensure that the LLMEngine is not referenced in295        # the closure used to initialize Ray worker actors296        raise RuntimeError("LLMEngine should not be pickled!")297 298    def get_tokenizer(self) -> "PreTrainedTokenizer":299        return self.tokenizer.get_lora_tokenizer(None)300 301    def get_tokenizer_for_seq(self,302                              sequence: Sequence) -> "PreTrainedTokenizer":303        return self.tokenizer.get_lora_tokenizer(sequence.lora_request)304 305    def _init_tokenizer(self, **tokenizer_init_kwargs):306        init_kwargs = dict(307            tokenizer_id=self.model_config.tokenizer,308            enable_lora=bool(self.lora_config),309            max_num_seqs=self.scheduler_config.max_num_seqs,310            max_input_length=None,311            tokenizer_mode=self.model_config.tokenizer_mode,312            trust_remote_code=self.model_config.trust_remote_code,313            revision=self.model_config.tokenizer_revision)314        init_kwargs.update(tokenizer_init_kwargs)315        self.tokenizer = get_tokenizer_group(316            self.parallel_config.tokenizer_pool_config, **init_kwargs)317 318    def _verify_args(self) -> None:319        self.model_config.verify_with_parallel_config(self.parallel_config)320        self.cache_config.verify_with_parallel_config(self.parallel_config)321        if self.lora_config:322            self.lora_config.verify_with_model_config(self.model_config)323            self.lora_config.verify_with_scheduler_config(324                self.scheduler_config)325 326    def encode_request(327        self,328        request_id: str,  # pylint: disable=unused-argument329        prompt: Optional[str],330        prompt_token_ids: Optional[List[int]] = None,331        lora_request: Optional[LoRARequest] = None,332    ):333        if prompt_token_ids is None:334            assert prompt is not None335            prompt_token_ids = self.tokenizer.encode(request_id=request_id,336                                                     prompt=prompt,337                                                     lora_request=lora_request)338        return prompt_token_ids339 340    def add_request(341        self,342        request_id: str,343        prompt: Optional[str],344        sampling_params: SamplingParams,345        prompt_token_ids: Optional[List[int]] = None,346        arrival_time: Optional[float] = None,347        lora_request: Optional[LoRARequest] = None,348        multi_modal_data: Optional[MultiModalData] = None,349    ) -> None:350        """Add a request to the engine's request pool.351 352        The request is added to the request pool and will be processed by the353        scheduler as `engine.step()` is called. The exact scheduling policy is354        determined by the scheduler.355 356        Args:357            request_id: The unique ID of the request.358            prompt: The prompt string. Can be None if prompt_token_ids is359                provided.360            sampling_params: The sampling parameters for text generation.361            prompt_token_ids: The token IDs of the prompt. If None, we362                use the tokenizer to convert the prompts to token IDs.363            arrival_time: The arrival time of the request. If None, we use364                the current monotonic time.365            multi_modal_data: Multi modal data per request.366 367        Details:368            - Set arrival_time to the current time if it is None.369            - Set prompt_token_ids to the encoded prompt if it is None.370            - Create `best_of` number of :class:`~vllm.Sequence` objects.371            - Create a :class:`~vllm.SequenceGroup` object372              from the list of :class:`~vllm.Sequence`.373            - Add the :class:`~vllm.SequenceGroup` object to the scheduler.374 375        Example:376            >>> # initialize engine377            >>> engine = LLMEngine.from_engine_args(engine_args)378            >>> # set request arguments379            >>> example_prompt = "Who is the president of the United States?"380            >>> sampling_params = SamplingParams(temperature=0.0)381            >>> request_id = 0382            >>>383            >>> # add the request to the engine384            >>> engine.add_request(385            >>>    str(request_id),386            >>>    example_prompt,387            >>>    SamplingParams(temperature=0.0))388            >>> # continue the request processing389            >>> ...390        """391        if lora_request is not None and not self.lora_config:392            raise ValueError(f"Got lora_request {lora_request} but LoRA is "393                             "not enabled!")394        max_logprobs = self.get_model_config().max_logprobs395        if (sampling_params.logprobs396                and sampling_params.logprobs > max_logprobs) or (397                    sampling_params.prompt_logprobs398                    and sampling_params.prompt_logprobs > max_logprobs):399            raise ValueError(f"Cannot request more than "400                             f"{max_logprobs} logprobs.")401        if arrival_time is None:402            arrival_time = time.time()403        prompt_token_ids = self.encode_request(404            request_id=request_id,405            prompt=prompt,406            prompt_token_ids=prompt_token_ids,407            lora_request=lora_request)408 409        # Create the sequences.410        block_size = self.cache_config.block_size411        seq_id = next(self.seq_counter)412        eos_token_id = None413        if self.tokenizer:414            eos_token_id = self.tokenizer.get_lora_tokenizer(415                lora_request).eos_token_id416        else:417            logger.warning("Use None for EOS token id because tokenizer is "418                           "not initialized")419        seq = Sequence(seq_id, prompt, prompt_token_ids, block_size,420                       eos_token_id, lora_request)421        422        # Defensive copy of SamplingParams, which are used by the sampler,423        # this doesn't deep-copy LogitsProcessor objects424        sampling_params = sampling_params.clone()425        # Add the eos token id into the sampling_params to support min_tokens426        # processing427        if seq.eos_token_id is not None:428            sampling_params.all_stop_token_ids.add(seq.eos_token_id)429        sampling_params.update_from_generation_config(430            self.generation_config_fields)431 432        # Create the sequence group.433        seq_group = SequenceGroup(request_id, [seq], sampling_params,434                                  arrival_time, lora_request, multi_modal_data)435 436        # Add the sequence group to the scheduler.437        self.scheduler.add_seq_group(seq_group)438 439    def abort_request(self, request_id: Union[str, Iterable[str]]) -> None:440        """Aborts a request(s) with the given ID.441 442        Args:443            request_id: The ID(s) of the request to abort.444 445        Details:446            - Refer to the447              :meth:`~vllm.core.scheduler.Scheduler.abort_seq_group`448              from class :class:`~vllm.core.scheduler.Scheduler`.449 450        Example:451            >>> # initialize engine and add a request with request_id452            >>> request_id = str(0)453            >>> # abort the request454            >>> engine.abort_request(request_id)455        """456        self.scheduler.abort_seq_group(request_id)457 458    def get_model_config(self) -> ModelConfig:459        """Gets the model configuration."""460        return self.model_config461 462    def get_num_unfinished_requests(self) -> int:463        """Gets the number of unfinished requests."""464        return self.scheduler.get_num_unfinished_seq_groups()465 466    def has_unfinished_requests(self) -> bool:467        """Returns True if there are unfinished requests."""468        return self.scheduler.has_unfinished_seqs()469 470    def _process_model_outputs(471            self, output: List[SamplerOutput],472            scheduled_seq_groups: List[SequenceGroup],473            ignored_seq_groups: List[SequenceGroup]) -> List[RequestOutput]:474        """Apply the model output to the sequences in the scheduled seq groups.475 476        Returns RequestOutputs that can be returned to the client.477        """478        now = time.time()479 480        # Organize outputs by [sequence group][step] instead of481        # [step][sequence group].482        output_by_sequence_group = create_output_by_sequence_group(483            sampler_outputs=output, num_seq_groups=len(scheduled_seq_groups))484 485        # Update the scheduled sequence groups with the model outputs.486        for scheduled_seq_group, outputs in zip(scheduled_seq_groups,487                                                output_by_sequence_group):488            seq_group = scheduled_seq_group.seq_group489            seq_group.update_num_computed_tokens(490                scheduled_seq_group.token_chunk_size)491            # If uncomputed tokens > 0, it means prefill is chunked.492            # We don't need to process outputs in that case.493            if seq_group.get_num_uncomputed_tokens() == 0:494                self.output_processor.process_outputs(seq_group, outputs)495 496        # Free the finished sequence groups.497        self.scheduler.free_finished_seq_groups()498 499        # Create the outputs.500        request_outputs: List[RequestOutput] = []501        for scheduled_seq_group in scheduled_seq_groups:502            seq_group = scheduled_seq_group.seq_group503            seq_group.maybe_set_first_token_time(now)504            request_output = RequestOutput.from_seq_group(seq_group)505            request_outputs.append(request_output)506        for seq_group in ignored_seq_groups:507            request_output = RequestOutput.from_seq_group(seq_group)508            request_outputs.append(request_output)509        return request_outputs510 511    def step(self) -> List[RequestOutput]:512        """Performs one decoding iteration and returns newly generated results.513 514        .. figure:: https://i.imgur.com/sv2HssD.png515            :alt: Overview of the step function516            :align: center517 518            Overview of the step function.519 520        Details:521            - Step 1: Schedules the sequences to be executed in the next522              iteration and the token blocks to be swapped in/out/copy.523 524                - Depending on the scheduling policy,525                  sequences may be `preempted/reordered`.526                - A Sequence Group (SG) refer to a group of sequences527                  that are generated from the same prompt.528 529            - Step 2: Calls the distributed executor to execute the model.530            - Step 3: Processes the model output. This mainly includes:531 532                - Decodes the relevant outputs.533                - Updates the scheduled sequence groups with model outputs534                  based on its `sampling parameters` (`use_beam_search` or not).535                - Frees the finished sequence groups.536 537            - Finally, it creates and returns the newly generated results.538 539        Example:540            >>> # Please see the example/ folder for more detailed examples.541            >>>542            >>> # initialize engine and request arguments543            >>> engine = LLMEngine.from_engine_args(engine_args)544            >>> example_inputs = [(0, "What is LLM?",545            >>>    SamplingParams(temperature=0.0))]546            >>>547            >>> # Start the engine with an event loop548            >>> while True:549            >>>     if example_inputs:550            >>>         req_id, prompt, sampling_params = example_inputs.pop(0)551            >>>         engine.add_request(str(req_id), prompt, sampling_params)552            >>>553            >>>     # continue the request processing554            >>>     request_outputs = engine.step()555            >>>     for request_output in request_outputs:556            >>>         if request_output.finished:557            >>>             # return or show the request output558            >>>559            >>>     if not (engine.has_unfinished_requests() or example_inputs):560            >>>         break561        """562        seq_group_metadata_list, scheduler_outputs = self.scheduler.schedule()563        if not scheduler_outputs.is_empty():564            output = self.model_executor.execute_model(565                seq_group_metadata_list=seq_group_metadata_list,566                blocks_to_swap_in=scheduler_outputs.blocks_to_swap_in,567                blocks_to_swap_out=scheduler_outputs.blocks_to_swap_out,568                blocks_to_copy=scheduler_outputs.blocks_to_copy,569                num_lookahead_slots=scheduler_outputs.num_lookahead_slots)570        else:571            output = []572 573        request_outputs = self._process_model_outputs(574            output, scheduler_outputs.scheduled_seq_groups,575            scheduler_outputs.ignored_seq_groups)576 577        # Log stats.578        if self.log_stats:579            self.stat_logger.log(self._get_stats(scheduler_outputs))580 581        return request_outputs582 583    def do_log_stats(self) -> None:584        """Forced log when no requests active."""585        if self.log_stats:586            self.stat_logger.log(self._get_stats(scheduler_outputs=None))587 588    def _get_stats(self,589                   scheduler_outputs: Optional[SchedulerOutputs]) -> Stats:590        """Get Stats to be Logged to Prometheus."""591        now = time.time()592 593        # KV Cache Usage in %.594        num_total_gpu = self.cache_config.num_gpu_blocks595        num_free_gpu = self.scheduler.block_manager.get_num_free_gpu_blocks()596        gpu_cache_usage = 1.0 - (num_free_gpu / num_total_gpu)597 598        num_total_cpu = self.cache_config.num_cpu_blocks599        cpu_cache_usage = 0.600        if num_total_cpu > 0:601            num_free_cpu = self.scheduler.block_manager.get_num_free_cpu_blocks(602            )603            cpu_cache_usage = 1.0 - (num_free_cpu / num_total_cpu)604 605        # Scheduler State606        num_running = len(self.scheduler.running)607        num_swapped = len(self.scheduler.swapped)608        num_waiting = len(self.scheduler.waiting)609 610        # Iteration stats if we have scheduler output.611        num_prompt_tokens = 0612        num_generation_tokens = 0613        time_to_first_tokens = []614        time_per_output_tokens = []615        time_e2e_requests = []616        if scheduler_outputs is not None:617            prompt_run = scheduler_outputs.num_prefill_groups > 0618 619            # Number of Tokens.620            if prompt_run:621                num_prompt_tokens = sum(622                    len(scheduled_seq_group.seq_group.prompt_token_ids)623                    for scheduled_seq_group in624                    scheduler_outputs.scheduled_seq_groups)625                num_generation_tokens = sum(626                    scheduled_seq_group.seq_group.num_seqs()627                    for scheduled_seq_group in628                    scheduler_outputs.scheduled_seq_groups)629            else:630                num_generation_tokens = scheduler_outputs.num_batched_tokens631 632            # Latency Timings.633            time_last_iters = []634            for scheduled_seq_group in scheduler_outputs.scheduled_seq_groups:635                seq_group = scheduled_seq_group.seq_group636                # Time since last token.637                # (n.b. updates seq_group.metrics.last_token_time)638                time_last_iters.append(seq_group.get_last_latency(now))639                # Time since arrival for all finished requests.640                if seq_group.is_finished():641                    time_e2e_requests.append(now -642                                             seq_group.metrics.arrival_time)643 644            time_to_first_tokens = time_last_iters if prompt_run else []645            time_per_output_tokens = [] if prompt_run else time_last_iters646 647        return Stats(648            now=now,649            num_running=num_running,650            num_swapped=num_swapped,651            num_waiting=num_waiting,652            gpu_cache_usage=gpu_cache_usage,653            cpu_cache_usage=cpu_cache_usage,654            num_prompt_tokens=num_prompt_tokens,655            num_generation_tokens=num_generation_tokens,656            time_to_first_tokens=time_to_first_tokens,657            time_per_output_tokens=time_per_output_tokens,658            time_e2e_requests=time_e2e_requests,659        )660 661    def add_lora(self, lora_request: LoRARequest) -> bool:662        return self.model_executor.add_lora(lora_request)663 664    def remove_lora(self, lora_id: int) -> bool:665        return self.model_executor.remove_lora(lora_id)666 667    def list_loras(self) -> List[int]:668        return self.model_executor.list_loras()669 670    def check_health(self) -> None:671        self.model_executor.check_health()