A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 8, 08:45 UTC
[Core] Add sleep level 0 mode with enqueue/wait pattern (#33195)
Signed-off-by: Jaewon Lee <jaewon@meta.com> Co-authored-by: Lu Fang <30275821+houseroad@users.noreply.github.com>
Jaewon committed
Feb 12, 2026 at 16:16 UTC
aa181c923bf83b6f8c4ce5613492a6b410c0c535
5 files changed
+167
-26
vllm/entrypoints/llm.py
+112
-17
@@ -458,6 +458,93 @@ class LLM:
458
459
return self.engine_class.validate_outputs(outputs, RequestOutput)
460
461
+ def enqueue(
462
+ self,
463
+ prompts: PromptType | Sequence[PromptType],
464
+ sampling_params: SamplingParams | Sequence[SamplingParams] | None = None,
465
+ lora_request: list[LoRARequest] | LoRARequest | None = None,
466
+ priority: list[int] | None = None,
467
+ use_tqdm: bool | Callable[..., tqdm] = True,
468
+ tokenization_kwargs: dict[str, Any] | None = None,
469
+ ) -> list[str]:
470
+ """Enqueue prompts for generation without waiting for completion.
471
+
472
+ This method adds requests to the engine queue but does not start
473
+ processing them. Use wait_for_completion() to process the queued
474
+ requests and get results.
475
+
476
+ Args:
477
+ prompts: The prompts to the LLM. See generate() for details.
478
+ sampling_params: The sampling parameters for text generation.
479
+ lora_request: LoRA request to use for generation, if any.
480
+ priority: The priority of the requests, if any.
481
+ use_tqdm: If True, shows a tqdm progress bar while adding requests.
482
+ tokenization_kwargs: Overrides for `tokenizer.encode`.
483
+
484
+ Returns:
485
+ A list of request IDs for the enqueued requests.
486
+ """
487
+ model_config = self.model_config
488
+ runner_type = model_config.runner_type
489
+ if runner_type != "generate":
490
+ raise ValueError("LLM.enqueue() is only supported for generative models.")
491
+
492
+ if sampling_params is None:
493
+ sampling_params = self.get_default_sampling_params()
494
+
495
+ # Use the same preprocessing as _run_completion
496
+ seq_prompts = prompt_to_seq(prompts)
497
+ seq_params = self._params_to_seq(sampling_params, len(seq_prompts))
498
+
499
+ if any(param.truncate_prompt_tokens is not None for param in seq_params):
500
+ engine_prompts: Sequence[DictPrompt | TokPrompt] = [
501
+ engine_prompt
502
+ for prompt, param in zip(seq_prompts, seq_params)
503
+ for engine_prompt in self._preprocess_completion(
504
+ [prompt],
505
+ tokenization_kwargs=merge_kwargs(
506
+ tokenization_kwargs,
507
+ dict(truncate_prompt_tokens=param.truncate_prompt_tokens),
508
+ ),
509
+ )
510
+ ]
511
+ else:
512
+ engine_prompts = self._preprocess_completion(
513
+ seq_prompts,
514
+ tokenization_kwargs=tokenization_kwargs,
515
+ )
516
+
517
+ request_ids = self._validate_and_add_requests(
518
+ prompts=engine_prompts,
519
+ params=seq_params,
520
+ use_tqdm=use_tqdm,
521
+ lora_request=self._get_modality_specific_lora_reqs(
522
+ engine_prompts, lora_request
523
+ ),
524
+ tokenization_kwargs=tokenization_kwargs,
525
+ priority=priority,
526
+ )
527
+
528
+ return request_ids
529
+
530
+ def wait_for_completion(
531
+ self,
532
+ use_tqdm: bool | Callable[..., tqdm] = True,
533
+ ) -> list[RequestOutput]:
534
+ """Wait for all enqueued requests to complete and return results.
535
+
536
+ This method processes all requests currently in the engine queue
537
+ and returns their outputs. Use after enqueue() to get results.
538
+
539
+ Args:
540
+ use_tqdm: If True, shows a tqdm progress bar.
541
+
542
+ Returns:
543
+ A list of RequestOutput objects for all completed requests.
544
+ """
545
+ outputs = self._run_engine(use_tqdm=use_tqdm)
546
+ return self.engine_class.validate_outputs(outputs, RequestOutput)
547
+
548
def _get_modality_specific_lora_reqs(
549
self,
550
prompts: Sequence[DictPrompt | TokPrompt],
@@ -1618,19 +1705,22 @@ class LLM:
1705
during the sleep period, before `wake_up` is called.
1706
1707
Args:
1621
- level: The sleep level. Level 1 sleep will offload the model
1622
- weights and discard the kv cache. The content of kv cache
1623
- is forgotten. Level 1 sleep is good for sleeping and waking
1624
- up the engine to run the same model again. The model weights
1625
- are backed up in CPU memory. Please make sure there's enough
1626
- CPU memory to store the model weights. Level 2 sleep will
1627
- discard both the model weights and the kv cache. The content
1628
- of both the model weights and kv cache is forgotten. Level 2
1629
- sleep is good for sleeping and waking up the engine to run a
1630
- different model or update the model, where previous model
1631
- weights are not needed. It reduces CPU memory pressure.
1708
+ level: The sleep level.
1709
+ - Level 0: Pause scheduling but continue accepting requests.
1710
+ Requests are queued but not processed.
1711
+ - Level 1: Offload model weights to CPU, discard KV cache.
1712
+ The content of kv cache is forgotten. Good for
1713
+ sleeping and waking up the engine to run the same
1714
+ model again. Please make sure there's enough CPU
1715
+ memory to store the model weights.
1716
+ - Level 2: Discard all GPU memory (weights + KV cache).
1717
+ Good for sleeping and waking up the engine to run
1718
+ a different model or update the model, where
1719
+ previous model weights are not needed. It reduces
1720
+ CPU memory pressure.
1721
"""
1633
- self.reset_prefix_cache()
1722
+ if level > 0:
1723
+ self.reset_prefix_cache()
1724
self.llm_engine.sleep(level=level)
1725
1726
def wake_up(self, tags: list[str] | None = None):
@@ -1641,9 +1731,10 @@ class LLM:
1731
Args:
1732
tags: An optional list of tags to reallocate the engine memory
1733
for specific memory allocations. Values must be in
1644
- `("weights", "kv_cache")`. If None, all memory is reallocated.
1645
- wake_up should be called with all tags (or None) before the
1646
- engine is used again.
1734
+ `("weights", "kv_cache", "scheduling")`. If None, all memory
1735
+ is reallocated. wake_up should be called with all tags
1736
+ (or None) before the engine is used again.
1737
+ Use tags=["scheduling"] to resume from level 0 sleep.
1738
"""
1739
self.llm_engine.wake_up(tags)
1740
@@ -1810,7 +1901,7 @@ class LLM:
1901
lora_request: Sequence[LoRARequest | None] | LoRARequest | None,
1902
tokenization_kwargs: dict[str, Any] | None = None,
1903
priority: list[int] | None = None,
1813
- ) -> None:
1904
+ ) -> list[str]:
1905
num_requests = len(prompts)
1906
seq_params = self._params_to_seq(params, num_requests)
1907
seq_lora_requests = self._lora_request_to_seq(lora_request, num_requests)
@@ -1844,6 +1935,8 @@ class LLM:
1935
self.llm_engine.abort_request(added_request_ids, internal=True)
1936
raise e
1937
1938
+ return added_request_ids
1939
+
1940
def _add_request(
1941
self,
1942
prompt: PromptType | DictPrompt | TokPrompt,
@@ -1895,7 +1988,9 @@ class LLM:
1988
return engine_request.request_id
1989
1990
def _run_engine(
1898
- self, *, use_tqdm: bool | Callable[..., tqdm] = True
1991
+ self,
1992
+ *,
1993
+ use_tqdm: bool | Callable[..., tqdm] = True,
1994
) -> list[RequestOutput | PoolingRequestOutput]:
1995
# Initialize tqdm.
1996
if use_tqdm:
vllm/v1/engine/async_llm.py
+2
-1
@@ -938,7 +938,8 @@ class AsyncLLM(EngineClient):
938
await self.engine_core.reset_encoder_cache_async()
939
940
async def sleep(self, level: int = 1) -> None:
941
- await self.reset_prefix_cache()
941
+ if level > 0:
942
+ await self.reset_prefix_cache()
943
await self.engine_core.sleep_async(level)
944
945
if self.logger_manager is not None:
vllm/v1/engine/core.py
+46
-6
@@ -614,13 +614,43 @@ class EngineCore:
614
self.model_executor.reset_encoder_cache()
615
616
def sleep(self, level: int = 1):
617
- self.model_executor.sleep(level)
617
+ """Put the engine to sleep at the specified level.
618
+
619
+ Args:
620
+ level: Sleep level.
621
+ - Level 0: Pause scheduling only. Requests are still accepted
622
+ but not processed. No GPU memory changes.
623
+ - Level 1: Offload model weights to CPU, discard KV cache.
624
+ - Level 2: Discard all GPU memory.
625
+ """
626
+ if level == 0:
627
+ # Level 0: Just pause scheduling, don't touch GPU
628
+ self.pause_scheduler()
629
+ else:
630
+ # Level 1+: Delegate to executor for GPU memory management
631
+ self.model_executor.sleep(level)
632
633
def wake_up(self, tags: list[str] | None = None):
620
- self.model_executor.wake_up(tags)
634
+ """Wake up the engine from sleep.
635
+
636
+ Args:
637
+ tags: Tags to wake up. Use ["scheduling"] for level 0 wake up.
638
+ """
639
+ if tags is not None and "scheduling" in tags:
640
+ # Level 0 wake up: Resume scheduling
641
+ self.resume_scheduler()
642
+ # Remove "scheduling" from tags if there are other tags to process
643
+ remaining_tags = [t for t in tags if t != "scheduling"]
644
+ if remaining_tags:
645
+ self.model_executor.wake_up(remaining_tags)
646
+ else:
647
+ # Full wake up
648
+ self.resume_scheduler()
649
+ self.model_executor.wake_up(tags)
650
651
def is_sleeping(self) -> bool:
623
- return self.model_executor.is_sleeping
652
+ """Check if engine is sleeping at any level."""
653
+ return self._scheduler_paused or self.model_executor.is_sleeping
654
655
def execute_dummy_batch(self):
656
self.model_executor.execute_dummy_batch()
@@ -1023,7 +1053,13 @@ class EngineCoreProc(EngineCore):
1053
# 1) Poll the input queue until there is work to do.
1054
self._process_input_queue()
1055
# 2) Step the engine core and return the outputs.
1026
- self._process_engine_step()
1056
+ # Skip if scheduling is paused (level 0 sleep)
1057
+ if not self._scheduler_paused:
1058
+ self._process_engine_step()
1059
+ else:
1060
+ # When scheduling is paused, still need to check for wake up
1061
+ # by processing any utility requests that might resume scheduling
1062
+ pass
1063
1064
def _process_input_queue(self):
1065
"""Exits when an engine step needs to be performed."""
@@ -1031,7 +1067,7 @@ class EngineCoreProc(EngineCore):
1067
waited = False
1068
while (
1069
not self.engines_running
1034
- and not self.scheduler.has_requests()
1070
+ and (not self.scheduler.has_requests() or self._scheduler_paused)
1071
and not self.batch_queue
1072
and not self._scheduler_paused
1073
):
@@ -1414,11 +1450,15 @@ class DPEngineCoreProc(EngineCoreProc):
1450
# 1) Poll the input queue until there is work to do.
1451
self._process_input_queue()
1452
1453
+ # Skip processing if scheduling is paused (level 0 sleep)
1454
+ if self._scheduler_paused:
1455
+ continue
1456
+
1457
# 2) Step the engine core.
1458
executed = self._process_engine_step()
1459
self._maybe_publish_request_counts()
1420
-
1460
local_unfinished_reqs = self.scheduler.has_unfinished_requests()
1461
+
1462
if not executed:
1463
if not local_unfinished_reqs and not self.engines_running:
1464
# All engines are idle.
vllm/v1/engine/core_client.py
+2
-1
@@ -194,7 +194,7 @@ class EngineCoreClient(ABC):
194
raise NotImplementedError
195
196
def dp_engines_running(self) -> bool:
197
- """Returns True id data parallel engines are collectively in a
197
+ """Returns True if data parallel engines are collectively in a
198
running state."""
199
raise NotImplementedError
200
@@ -724,6 +724,7 @@ class SyncMPClient(MPClient):
724
# it is forwarded to the outputs_queue so we can raise it
725
# from this (run_output_handler) task to shut down the server.
726
outputs = self.outputs_queue.get()
727
+
728
if isinstance(outputs, Exception):
729
raise self._format_exception(outputs) from None
730
if outputs.wave_complete is not None:
vllm/v1/engine/llm_engine.py
+5
-1
@@ -312,7 +312,11 @@ class LLMEngine:
312
313
# 4) Record stats
314
with record_function_or_nullcontext("llm_engine step: record_stats"):
315
- if self.logger_manager is not None and outputs.scheduler_stats is not None:
315
+ if (
316
+ self.logger_manager is not None
317
+ and outputs.scheduler_stats is not None
318
+ and len(outputs.outputs) > 0
319
+ ):
320
self.logger_manager.record(
321
scheduler_stats=outputs.scheduler_stats,
322
iteration_stats=iteration_stats,