본문으로 건너뛰기

[vLLM] EngineCore: 핵심 실행 루프

들어가며

vLLM v1에서 EngineCore는 별도 프로세스에서 실행되는 핵심 실행 루프이다. 스케줄러가 어떤 요청을 실행할지 결정하고, Executor가 GPU에서 모델을 실행하며, 결과를 다시 스케줄러에 반영하는 세 단계 반복이 step() 함수에 집약되어 있다.

소스 경로: vllm/v1/engine/core.py

핵심 구조/코드 분석

클래스 초기화

class EngineCore:
    """Inner loop of vLLM's Engine."""

    def __init__(self, vllm_config, executor_class, log_stats, ...):
        # Executor 생성 (GPU 워커 관리)
        self.model_executor = executor_class(vllm_config)

        # KV Cache 초기화 및 프로파일링
        kv_cache_config = self._initialize_kv_caches(vllm_config)

        # 스케줄러 생성
        Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
        self.scheduler = Scheduler(
            vllm_config=vllm_config,
            kv_cache_config=kv_cache_config,
            structured_output_manager=self.structured_output_manager,
            ...
        )

초기화 과정에서 주목할 점은 KV 캐시 프로파일링이다. _initialize_kv_caches()는 모델의 피크 메모리 사용량을 프로파일링한 뒤 남은 GPU 메모리로 KV 캐시 블록 수를 결정한다.

step() - 핵심 실행 루프

def step(self) -> tuple[dict[int, EngineCoreOutputs], bool]:
    if not self.scheduler.has_requests():
        return {}, False

    scheduler_output = self.scheduler.schedule()
    future = self.model_executor.execute_model(scheduler_output, non_block=True)
    grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)

    model_output = future.result()
    if model_output is None:
        model_output = self.model_executor.sample_tokens(grammar_output)

    self._process_aborts_queue()
    engine_core_outputs = self.scheduler.update_from_output(
        scheduler_output, model_output
    )
    return engine_core_outputs, scheduler_output.total_num_scheduled_tokens > 0

한 스텝은 정확히 세 단계로 진행된다:

  1. schedule(): continuous batching 스케줄러가 다음 배치를 구성
  2. execute_model(): 비동기로 GPU에서 모델 포워드 패스 실행 (Future 반환)
  3. update_from_output(): 모델 출력을 스케줄러에 반영 (토큰 추가, 완료 처리)

non_block=True로 실행하면서 그 사이에 grammar bitmask를 준비하는 것이 파이프라이닝의 핵심이다.

배치 큐 (Pipeline Parallelism)

self.batch_queue_size = self.model_executor.max_concurrent_batches
if self.batch_queue_size > 1:
    self.batch_queue = deque(maxlen=self.batch_queue_size)

파이프라인 병렬 처리 시 여러 배치를 동시에 실행할 수 있다. step_with_batch_queue()는 큐가 가득 차지 않으면 새 배치를 스케줄링하고, 가득 차면 첫 번째 배치의 완료를 기다린다. 이를 통해 파이프라인 버블을 제거한다.

KV 캐시 초기화

def _initialize_kv_caches(self, vllm_config) -> KVCacheConfig:
    kv_cache_specs = self.model_executor.get_kv_cache_specs()
    available_gpu_memory = self.model_executor.determine_available_memory()

    kv_cache_configs = get_kv_cache_configs(
        vllm_config, kv_cache_specs, available_gpu_memory
    )
    self.model_executor.initialize_from_config(kv_cache_configs)
    return scheduler_kv_cache_config

모델의 각 레이어가 필요한 KV 캐시 스펙을 수집하고, GPU 메모리 프로파일링 결과에 따라 블록 수를 자동 결정한다. max_model_len이 메모리에 맞지 않으면 자동으로 줄이는 auto-fit 기능도 있다.

왜 이 설계인가

  1. 별도 프로세스 실행: EngineCore는 ZMQ 소켓을 통해 AsyncLLM과 통신한다. 이렇게 하면 스케줄링과 GPU 실행이 API 서버의 asyncio 이벤트 루프를 블로킹하지 않는다.

  2. Continuous Batching: 매 step마다 스케줄러가 새로운 요청을 추가하고 완료된 요청을 제거한다. 전통적인 static batching과 달리 GPU 활용률이 극대화된다.

  3. 비동기 스케줄링: async_scheduling 옵션을 켜면 모델 실행과 스케줄링을 오버랩할 수 있다. Speculative decoding의 draft token도 비동기로 업데이트된다.

  4. GC 최적화: 초기화가 끝나면 freeze_gc_heap()으로 힙을 고정하고 enable_envs_cache()로 환경변수 조회를 캐싱한다. 이는 추론 루프에서의 마이크로 레이턴시를 줄이기 위함이다.

참고

댓글

관련 포스트

vLLM 의 다른글