본문으로 건너뛰기

[vLLM] OpenAI 호환 API 서버: FastAPI 기반 HTTP 서빙

들어가며

LLM을 프로덕션에 배포할 때 가장 중요한 요소 중 하나는 OpenAI API와의 호환성이다. 기존 클라이언트 코드 변경 없이 자체 호스팅 모델로 전환할 수 있기 때문이다. vLLM은 vllm/entrypoints/openai/api_server.py에서 FastAPI 기반의 OpenAI 호환 HTTP 서버를 제공한다.

공식 문서: https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html

공식 문서

vLLM 공식 문서: OpenAI Compatible Server

핵심 구조/코드 분석

엔진 클라이언트 생성

서버의 시작점은 build_async_engine_client 함수이다. 이 함수는 비동기 엔진 클라이언트를 생성하는 컨텍스트 매니저로, 내부적으로 AsyncLLM을 초기화한다.

@asynccontextmanager
async def build_async_engine_client(
    args: Namespace,
    *,
    usage_context: UsageContext = UsageContext.OPENAI_API_SERVER,
    client_config: dict[str, Any] | None = None,
) -> AsyncIterator[EngineClient]:
    engine_args = AsyncEngineArgs.from_cli_args(args)
    async with build_async_engine_client_from_engine_args(
        engine_args, usage_context=usage_context,
        client_config=client_config,
    ) as engine:
        yield engine

핵심은 AsyncLLM.from_vllm_config()을 통해 v1 엔진을 직접 프로세스 내에서 생성한다는 것이다.

FastAPI 앱 빌드

build_app 함수는 FastAPI 인스턴스를 생성하고, 모델이 지원하는 태스크에 따라 라우터를 동적으로 등록한다.

def build_app(args, supported_tasks, model_config) -> FastAPI:
    app = FastAPI(lifespan=lifespan)

    register_vllm_serve_api_routers(app)
    register_models_api_router(app)

    if "generate" in supported_tasks:
        register_generate_api_routers(app)
        attach_disagg_router(app)
        attach_rlhf_router(app)

    if any(task in POOLING_TASKS for task in supported_tasks):
        register_pooling_api_routers(app, supported_tasks, model_config)

    app.add_middleware(CORSMiddleware, ...)
    return app

태스크 기반 라우터 등록이 핵심 설계이다. generate, transcription, realtime, pooling 등 모델이 지원하는 태스크에 맞춰 필요한 엔드포인트만 활성화한다.

앱 상태 초기화

init_app_state에서 서빙에 필요한 핵심 객체들이 FastAPI의 app.state에 바인딩된다.

async def init_app_state(engine_client, state, args, supported_tasks):
    state.engine_client = engine_client
    state.openai_serving_models = OpenAIServingModels(
        engine_client=engine_client,
        base_model_paths=base_model_paths,
        lora_modules=lora_modules,
    )
    state.openai_serving_render = OpenAIServingRender(...)
    state.openai_serving_tokenization = OpenAIServingTokenization(...)

서버 실행 흐름

전체 서버 실행은 run_server -> setup_server -> build_and_serve 순서로 진행된다.

async def run_server(args, **uvicorn_kwargs) -> None:
    listen_address, sock = setup_server(args)
    await run_server_worker(listen_address, sock, args, **uvicorn_kwargs)

setup_server에서 소켓을 미리 바인딩하는 이유는 Ray와의 레이스 컨디션을 방지하기 위해서이다. 엔진 초기화 전에 포트를 선점해야 다른 프로세스와의 충돌을 피할 수 있다.

미들웨어 체계

vLLM 서버는 다양한 미들웨어를 지원한다.

# API 키 인증
if tokens := [key for key in (args.api_key or [envs.VLLM_API_KEY]) if key]:
    app.add_middleware(AuthenticationMiddleware, tokens=tokens)

# 스케일링 미들웨어
app.add_middleware(ScalingMiddleware)

# 사용자 커스텀 미들웨어
for middleware in args.middleware:
    module_path, object_name = middleware.rsplit(".", 1)
    imported = getattr(importlib.import_module(module_path), object_name)
    if inspect.isclass(imported):
        app.add_middleware(imported)

왜 이 설계인가

  1. 태스크 기반 라우터 분리: 모든 엔드포인트를 한 파일에 넣지 않고, 태스크별로 분리해서 모듈화했다. 임베딩 모델을 서빙할 때 generate 관련 엔드포인트가 불필요하게 노출되지 않는다.

  2. 소켓 선바인딩: Ray 분산 환경에서 엔진 초기화와 포트 바인딩 사이의 레이스 컨디션을 해결하기 위한 실용적 설계이다.

  3. uvloop 사용: uvloop.run(run_server(args))로 이벤트 루프 성능을 극대화한다. uvloop은 libuv 기반으로 표준 asyncio 대비 2~4배 빠른 성능을 제공한다.

  4. 미들웨어 확장성: --middleware 인자로 외부 미들웨어를 동적으로 주입할 수 있어, 프로덕션 환경에서 로깅이나 모니터링 등을 유연하게 추가할 수 있다.

정리

vLLM의 OpenAI 호환 서버는 단순한 HTTP 래퍼가 아니다. 태스크 기반 동적 라우팅, 소켓 선바인딩, 유연한 미들웨어 체계 등 프로덕션 환경을 고려한 설계가 곳곳에 반영되어 있다. vllm serve 명령 한 줄 뒤에 이런 아키텍처가 동작하고 있다는 것을 알면, 서빙 환경을 커스터마이징할 때 큰 도움이 된다.

댓글

관련 포스트

vLLM 의 다른글