디렌더러 API

디렌더러 API (Derenderer APIs)

렌더러(renderer)가 요청을 토큰 ID로 바꾸는 전처리라면, 디렌더러(derenderer)는 그 반대인 후처리를 담당해요. 생성된 토큰 ID를 다시 완전한 OpenAI 호환 응답으로 바꿔주죠. 분리 서빙(disaggregated serving)에서 token-in / token-out 엔진을 구축할 때 이 둘을 함께 써요. 이 페이지에서 디렌더러 API를 살펴볼게요.

출처: vLLM 공식 문서 — serving/online_serving/derenderer

디렌더러 API는 렌더러 API의 후처리 대응물이에요. /render가 요청을 토큰 ID로 바꾸는 전처리라면, /derender는 생성된 토큰 ID를 다시 완전한 OpenAI 호환 응답으로 바꿔요(디토크나이제이션, reasoning 파싱, tool call 파싱). 이 모든 것이 GPU 없이 이루어져요.

이렇게 하면 분리 서빙에서 token-in / token-out 엔진의 루프가 닫혀요.

  • GPU 없는 후처리: 디토크나이제이션, reasoning 파싱, tool call 파싱이 /render를 호스팅하는 것과 같은 GPU 없는 프론트엔드에서 실행돼요.
  • 파서 일관성: 디렌더러가 vLLM의 tool·reasoning 파서를 재사용하므로, 분리 배포가 표준 vllm serve 서버와 같은 content/reasoning/tool_calls 분리를 만들어요.
  • 비스트리밍: 엔드포인트는 모든 토큰 ID가 있는 완전한 GenerateResponse를 기대하고 원샷 파싱을 수행해요. 스트리밍 derender는 별도의 엔드포인트 설계가 필요하며 현재는 지원되지 않지만 파이프라인에 있어요.

두 엔드포인트 모두 vllm launch render로 시작하는 GPU 없는 렌더링 서버가 /render 엔드포인트와 함께 호스팅해요.

파이프라인 (Pipeline)

                render                 generate                derender
  request  ───────────────▶  token_ids  ─────────▶  token_ids  ──────────▶  response
 (chat /            (GPU less)          (token-in /            (GPU less)   (OpenAI
 completion)            │               token-out engine)          ▲        compatible)
                        └─────────────── request + prompt_tokens ──┘

derender 단계는 엔진의 token_ids만으로는 부족해요. render 단계에서 가져온 원본 chat_request/completion_requestprompt_tokens도 소비해요(요청 형식 참고). 그래야 tool·reasoning 파서가 필요한 컨텍스트를 갖거든요.

API 참조 (API Reference)

  • Chat Completions Derender API (/v1/chat/completions/derender)
    • 단일 GenerateResponseChatCompletionResponse로 후처리.
  • Completions Derender API (/v1/completions/derender)
    • GenerateResponse 객체 목록(프롬프트당 하나)을 CompletionResponse로 후처리.

요청 형식 (Request format)

각 요청은 엔진의 GenerateResponse와, GPU 없이 최종 응답을 재구성하는 데 필요한 호출자 메타데이터를 함께 감싸요.

/v1/chat/completions/derender:

    stream: Literal[False] = False

    model: str | None = None
    """Served model name. Defaults to the server's served model name."""

    generate_response: GenerateResponse
    """The complete token-in / token-out engine response to derender."""

    prompt_tokens: int | None = None
    """Prompt token count for usage; defaults to 0 if omitted.

    GenerateResponse carries only output tokens; the caller already has
    len(GenerateRequest.token_ids) from the render step.
    """

    chat_request: ChatCompletionRequest | None = None
    """The original (post-adjust_request) ChatCompletionRequest from /render.

    Required by the parsing so that tool/reasoning parsers can receive the full
    request context they expect (request.tools, request.tool_choice,
    request._grammar_from_parser, etc.).
    """

/v1/completions/derender:

    stream: Literal[False] = False

    model: str | None = None
    """Served model name. Defaults to the server's served model name."""

    generate_responses: list[GenerateResponse]
    """One response per prompt, parallel to the list[GenerateRequest]
    returned by /v1/completions/render."""

    prompt_tokens: list[int] | None = None
    """One prompt token count per response; each defaults to 0 if omitted.

    If provided, len(prompt_tokens) must equal len(generate_responses).
    """

    completion_request: CompletionRequest | None = None
    """The original (post-adjust_request) CompletionRequest from /render.

    Mirrors chat_request on DerenderChatRequest. Required by the parsing
    so parsers receive the full request context.
    """

과도하게 큰 페이로드는 tokenizer.decode()나 파서가 실행되기 전에 400으로 거부돼요.

예제 (Example)

아래 예시는 GPU 없는 렌더 서버(/render, /derender)와 token-in / token-out 엔진(/inference/v1/generate)에 대해 챗 요청의 전체 render → generate → derender 왕복을 구동해요.

먼저 두 서버를 실행하세요.

vllm launch render meta-llama/Llama-3.2-1B-Instruct --port 8100
VLLM_ENABLE_SCALE_OUT_ENDPOINTS=1 vllm serve \
    meta-llama/Llama-3.2-1B-Instruct --tokens-only --port 8200
import httpx

MODEL = "meta-llama/Llama-3.2-1B-Instruct"
RENDER = "http://localhost:8100"  # vllm launch render ...
ENGINE = "http://localhost:8200"  # token-in / token-out engine

chat_request = {
    "model": MODEL,
    "messages": [{"role": "user", "content": "What is 2+2?"}],
    "max_tokens": 32,
}

with httpx.Client(timeout=60.0) as client:
    # 1. Render: request -> token IDs (GPU less)
    generate_request = client.post(
        f"{RENDER}/v1/chat/completions/render", json=chat_request
    ).json()
    prompt_tokens = len(generate_request["token_ids"])

    # 2. Generate: token IDs -> token IDs (token-in / token-out engine)
    generate_response = client.post(
        f"{ENGINE}/inference/v1/generate", json=generate_request
    ).json()

    # 3. Derender: token IDs -> ChatCompletionResponse (GPU less)
    response = client.post(
        f"{RENDER}/v1/chat/completions/derender",
        json={
            "model": MODEL,
            "generate_response": generate_response,
            "prompt_tokens": prompt_tokens,
            "chat_request": chat_request,
        },
    ).json()

print(response["choices"][0]["message"]["content"])

chat_request를 넘기면 디렌더러가 설정된 tool·reasoning 파서를 실행할 수 있어요. 즉 response["choices"][0]["message"]vllm serve 서버가 만들 것과 같은 content/reasoning/tool_calls 분리를 갖게 돼요. chat_request를 생략하면 단순 디토크나이제이션만 수행돼요.

더 알아보기 (Learn more)