멀티모달 (Multimodal)

멀티모달 (Multimodal)

이 페이지는 vLLM에서 멀티모달 모델에 멀티모달 입력을 넘기는 방법을 설명해요.

참고 우리는 멀티모달 지원을 계속 다듬고 있어요. 예정된 변경 사항은 이 RFC에서 확인할 수 있고, 피드백이나 기능 요청이 있다면 GitHub에 이슈를 열어 주세요.

멀티모달 모델을 서빙할 때는 --allowed-media-domains를 설정해서 vLLM이 접근할 수 있는 도메인을 제한하는 걸 고려해 보세요. 이렇게 하면 SSRF(Server-Side Request Forgery) 공격에 취약할 수 있는 임의의 엔드포인트에 접근하는 걸 막을 수 있어요. 이 인자에는 도메인 목록을 넘기면 됩니다. 예: --allowed-media-domains upload.wikimedia.org github.com www.bogotobogo.com

VLLM_MEDIA_URL_ALLOW_REDIRECTS=0을 설정해서 HTTP 리다이렉트로 도메인 제한을 우회하는 것도 막아두는 게 좋아요.

이 제한은 특히 vLLM 파드가 내부 네트워크에 제한 없이 접근할 수 있는 컨테이너 환경에서 vLLM을 실행한다면 더 중요해요.

오프라인 추론 (Offline Inference)

멀티모달 데이터를 입력하려면 vllm.inputs.PromptType에 정의된 스키마를 따르면 돼요.

  • prompt: HuggingFace에 문서화된 형식을 따라야 해요.
  • multi_modal_data: vllm.inputs.MultiModalDataDict에 정의된 스키마를 따르는 딕셔너리예요.

이미지 입력 (Image Inputs)

멀티모달 딕셔너리의 'image' 필드에 이미지 하나를 넘길 수 있어요. 아래 예시를 보면 돼요.

from vllm import LLM

llm = LLM(model="llava-hf/llava-1.5-7b-hf")

# 올바른 형식은 HuggingFace 저장소를 참고하세요
prompt = "USER: <image>\nWhat is the content of this image?\nASSISTANT:"

# PIL.Image로 이미지 로드
image = PIL.Image.open(...)

# 단일 프롬프트 추론
outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"image": image},
})

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

# 배치 추론
image_1 = PIL.Image.open(...)
image_2 = PIL.Image.open(...)
outputs = llm.generate(
    [
        {
            "prompt": "USER: <image>\nWhat is the content of this image?\nASSISTANT:",
            "multi_modal_data": {"image": image_1},
        },
        {
            "prompt": "USER: <image>\nWhat's the color of this image?\nASSISTANT:",
            "multi_modal_data": {"image": image_2},
        }
    ]
)

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

전체 예시: examples/generate/multimodal/vision_language_offline.py

같은 텍스트 프롬프트 안에 이미지 여러 개를 넣고 싶다면, 이미지 목록을 넘기면 돼요.

from vllm import LLM

llm = LLM(
    model="microsoft/Phi-3.5-vision-instruct",
    trust_remote_code=True,  # Phi-3.5-vision을 로드하려면 필요
    max_model_len=4096,  # 그렇지 않으면 작은 GPU에 안 들어갈 수 있어요
    limit_mm_per_prompt={"image": 2},  # 허용할 최대 개수
)

# 올바른 형식은 HuggingFace 저장소를 참고하세요
prompt = "<|user|>\n<|image_1|>\n<|image_2|>\nWhat is the content of each image?<|end|>\n<|assistant|>\n"

# PIL.Image로 이미지 로드
image1 = PIL.Image.open(...)
image2 = PIL.Image.open(...)

outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"image": [image1, image2]},
})

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

전체 예시: examples/generate/multimodal/vision_language_multi_image_offline.py

LLM.chat 메서드를 쓴다면, 메시지 콘텐츠에 이미지를 다양한 형식(이미지 URL, PIL Image 객체, 미리 계산된 임베딩)으로 직접 넘길 수 있어요.

from vllm import LLM
from vllm.assets.image import ImageAsset

llm = LLM(model="llava-hf/llava-1.5-7b-hf")
image_url = "https://picsum.photos/id/32/512/512"
image_pil = ImageAsset('cherry_blossom').pil_image
image_embeds = torch.load(...)

conversation = [
    {"role": "system", "content": "You are a helpful assistant"},
    {"role": "user", "content": "Hello"},
    {"role": "assistant", "content": "Hello! How can I assist you today?"},
    {
        "role": "user",
        "content": [
            {
                "type": "image_url",
                "image_url": {"url": image_url},
            },
            {
                "type": "image_pil",
                "image_pil": image_pil,
            },
            {
                "type": "image_embeds",
                "image_embeds": image_embeds,
            },
            {
                "type": "text",
                "text": "What's in these images?",
            },
        ],
    },
]

# 추론 수행 및 출력 로그
outputs = llm.chat(conversation)

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

다중 이미지 입력을 확장해서 비디오 캡셔닝을 할 수도 있어요. 비디오를 지원하는 Qwen2-VL로 살펴볼게요.

from vllm import LLM

# 비디오당 최대 프레임 수를 4로 지정. 이 값은 바꿀 수 있어요.
llm = LLM("Qwen/Qwen2-VL-2B-Instruct", limit_mm_per_prompt={"image": 4})

# 요청 페이로드 생성.
video_frames = ... # 앞서 지정한 프레임 수만 담긴 비디오를 로드
message = {
    "role": "user",
    "content": [
        {
            "type": "text",
            "text": "Describe this set of frames. Consider the frames to be a part of the same video.",
        },
    ],
}
for i in range(len(video_frames)):
    base64_image = encode_image(video_frames[i]) # base64 인코딩.
    new_image = {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
    message["content"].append(new_image)

# 추론 수행 및 출력 로그
outputs = llm.chat([message])

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

커스텀 RGBA 배경색 (Custom RGBA Background Color)

RGBA 이미지(투명도가 있는 이미지)를 로드할 때 vLLM은 이를 RGB 형식으로 변환해요. 기본적으로 투명 픽셀은 흰색 배경으로 바뀌죠. 이 배경색은 media_io_kwargsrgba_background_color 파라미터로 바꿀 수 있어요.

from vllm import LLM

# 기본 흰색 배경 (설정 불필요)
llm = LLM(model="llava-hf/llava-1.5-7b-hf")

# 다크 테마용 커스텀 검정 배경
llm = LLM(
    model="llava-hf/llava-1.5-7b-hf",
    media_io_kwargs={"image": {"rgba_background_color": [0, 0, 0]}},
)

# 커스텀 브랜드 색상 배경 (예: 파랑)
llm = LLM(
    model="llava-hf/llava-1.5-7b-hf",
    media_io_kwargs={"image": {"rgba_background_color": [0, 0, 255]}},
)

참고

  • rgba_background_color는 RGB 값을 리스트 [R, G, B]나 튜플 (R, G, B)로 받으며, 각 값은 0~255 범위예요.
  • 이 설정은 투명도를 가진 RGBA 이미지에만 영향을 주고, RGB 이미지는 그대로예요.
  • 지정하지 않으면 호환성을 위해 기본 흰색 배경 (255, 255, 255)이 사용돼요.

Moondream3 프롬프트 레시피 (Moondream3 Prompt Recipes)

[Moondream3ForCausalLM](../../api/vllm/model_executor/models/moondream3/#vllm.model_executor.models.moondream3.Moondream3ForCausalLM)는 두 가지 작업별 프롬프트 형식을 지원해요.

  • query: 이미지에 대해 질문하기.
  • caption: 이미지 캡션 생성하기.
from vllm import LLM, SamplingParams
from vllm.assets.image import ImageAsset

llm = LLM(
    model="moondream/moondream3-preview",
    tokenizer="moondream/starmie-v1",
    trust_remote_code=True,
    max_model_len=2048,
    limit_mm_per_prompt={"image": 1},
)

image = ImageAsset("stop_sign").pil_image

def make_query_prompt(question: str) -> str:
    return (
        "<|endoftext|><image><|md_reserved_0|>query<|md_reserved_1|>"
        f"{question}<|md_reserved_2|>"
    )

def make_caption_prompt(length: str = "normal") -> str:
    return (
        "<|endoftext|><image><|md_reserved_0|>"
        f"describe<|md_reserved_1|>{length}<|md_reserved_2|>"
    )

query_out = llm.generate(
    {
        "prompt": make_query_prompt("What is shown in this image?"),
        "multi_modal_data": {"image": image},
    },
    SamplingParams(max_tokens=64, temperature=0),
)[0].outputs[0].text

caption_out = llm.generate(
    {
        "prompt": make_caption_prompt(),
        "multi_modal_data": {"image": image},
    },
    SamplingParams(max_tokens=100, temperature=0),
)[0].outputs[0].text

print("query:", query_out)
print("caption:", caption_out)

참고 네이티브 Moondream3 모델에는 detectpoint 스킬도 있어요. 하지만 이들은 커스텀 좌표 디코딩이 필요해서 이 vLLM 구현에서는 노출하지 않아요.

비디오 입력 (Video Inputs)

멀티 이미지 입력을 쓰는 대신, NumPy 배열의 리스트를 멀티모달 딕셔너리의 'video' 필드에 직접 넘길 수도 있어요.

NumPy 배열 대신 'torch.Tensor' 인스턴스를 넘길 수도 있는데, Qwen2.5-VL을 쓴 아래 예시를 보면 돼요.

from transformers import AutoProcessor
from vllm import LLM, SamplingParams
from qwen_vl_utils import process_vision_info

model_path = "Qwen/Qwen2.5-VL-3B-Instruct"
video_path = "https://content.pexels.com/videos/free-videos.mp4"

llm = LLM(
    model=model_path,
    gpu_memory_utilization=0.8,
    enforce_eager=True,
    limit_mm_per_prompt={"video": 1},
)

sampling_params = SamplingParams(max_tokens=1024)

video_messages = [
    {
        "role": "system",
        "content": "You are a helpful assistant.",
    },
    {
        "role": "user",
        "content": [
            {"type": "text", "text": "describe this video."},
            {
                "type": "video",
                "video": video_path,
                "total_pixels": 20480 * 28 * 28,
                "min_pixels": 16 * 28 * 28,
            },
        ]
    },
]

messages = video_messages
processor = AutoProcessor.from_pretrained(model_path)
prompt = processor.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
)

image_inputs, video_inputs = process_vision_info(messages)
mm_data = {}
if video_inputs is not None:
    mm_data["video"] = video_inputs

llm_inputs = {
    "prompt": prompt,
    "multi_modal_data": mm_data,
}

outputs = llm.generate([llm_inputs], sampling_params=sampling_params)
for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

참고 process_vision_info는 Qwen2.5-VL 및 유사한 모델에만 적용할 수 있어요.

전체 예시: examples/generate/multimodal/vision_language_offline.py

비디오 토큰 프루닝 (Video Token Pruning)

지원되는 모델에서는 vLLM이 비전 인코더 뒤의 비디오 토큰을 프루닝해서 prefill 시간과 KV 캐시 사용량을 줄일 수 있어요. 다만 정확도는 어느 정도 희생되죠. --video-pruning-rate <q>로 각 비디오에서 q 비율만큼의 비디오 토큰을 프루닝하고, --video-pruning-method로 학습이 필요 없는 알고리즘을 고르면 돼요.

  • evs (Efficient Video Sampling, 기본값): 이전 프레임과 시간적 유사도가 가장 낮은 토큰을 버려요. 첫 번째 프레임은 항상 온전히 유지돼요.
  • vidcom2 (Video Compression Commander): 비디오 수준·프레임 수준 피처 중심과의 유사도로 토큰에 점수를 매기고, 두드러진 프레임에 예산을 더 많이 배분해요. 프레임당 최소 하나의 토큰은 유지돼요.
vllm serve Qwen/Qwen3-VL-8B-Instruct \
    --video-pruning-rate 0.75 --video-pruning-method vidcom2

참고 evs는 멀티모달 프루닝을 구현하는 모든 모델이 지원하고, vidcom2는 현재 Qwen3-VL만 지원해요. 지원되지 않는 조합은 시작 시 거부돼요. 비디오 프루닝을 켜면 유지되는 토큰 수가 데이터에 따라 달라지기 때문에 인코더 CUDA 그래프도 비활성화돼요.

오디오 입력 (Audio Inputs)

멀티모달 딕셔너리의 'audio' 필드에 튜플 (array, sampling_rate)을 넘길 수 있어요.

전체 예시: examples/generate/multimodal/audio_language_offline.py

긴 오디오 자막 처리를 위한 청크 분할 (Chunking Long Audio for Transcription)

Whisper 같은 음성-텍스트 모델은 처리할 수 있는 최대 오디오 길이(보통 30초)가 있어요. 더 긴 오디오 파일의 경우, vLLM은 조용한 구간에서 오디오를 지능적으로 잘라서 발화를 잘라내는 일을 최소화하는 유틸리티를 제공해요.

from vllm import LLM, SamplingParams
from vllm.multimodal.audio import split_audio
from vllm.multimodal.media.audio import load_audio

# 긴 오디오 파일 로드
audio, sr = load_audio("long_audio.wav", sr=16000)

# 저에너지(조용한) 구간에서 청크로 분할
chunks = split_audio(
    audio_data=audio,
    sample_rate=sr,
    max_clip_duration_s=30.0,      # 청크 최대 길이(초)
    overlap_duration_s=1.0,         # 조용한 분할 지점을 찾는 탐색 윈도우
    min_energy_window_size=1600,    # 에너지 계산용 윈도우 크기 (16kHz에서 약 100ms)
)

# Whisper 모델 초기화
llm = LLM(model="openai/whisper-large-v3-turbo")
sampling_params = SamplingParams(temperature=0, max_tokens=256)

# 각 청크를 텍스트로 변환
transcriptions = []
for chunk in chunks:
    outputs = llm.generate({
        "prompt": "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
        "multi_modal_data": {"audio": (chunk, sr)},
    }, sampling_params)
    transcriptions.append(outputs[0].outputs[0].text)

# 결과 합치기
full_transcription = " ".join(transcriptions)

split_audio 함수는:

  • 1D 모노 오디오를 기대해요 (load_audio가 기본적으로 다운믹스해요).
  • 발화를 잘라내지 않도록 조용한 지점에서 분할해요.
  • RMS 에너지로 overlap 윈도우 안의 저진폭 영역을 찾아요.
  • 모든 오디오 샘플을 보존해요 (데이터 손실 없음).
  • 어떤 샘플 레이트도 지원해요.

자동 오디오 채널 정규화 (Automatic Audio Channel Normalization)

vLLM은 특정 오디오 형식을 요구하는 모델을 위해 오디오 채널을 자동으로 정규화해요. torchaudio 같은 라이브러리로 오디오를 로드하면 스테레오 파일이 [channels, time] 형태로 반환되지만, 많은 오디오 모델(특히 Whisper 기반 모델)은 [time] 형태의 모노 오디오를 기대하기 때문이에요.

모노 변환을 자동 지원하는 모델:

  • Whisper 및 모든 Whisper 기반 모델
  • Qwen2-Audio
  • Qwen2.5-Omni / Qwen3-Omni (Qwen2.5-Omni에서 상속)
  • Ultravox

이 모델들에서 vLLM은 자동으로:

  1. 피처 추출기를 통해 모델이 모노 오디오를 요구하는지 감지하고
  2. 채널 평균화로 다중 채널 오디오를 모노로 변환하며
  3. (channels, time) 형식(torchaudio)과 (time, channels) 형식(soundfile)을 모두 처리해요.

스테레오 오디오 예시:

import torchaudio
from vllm import LLM

# 스테레오 오디오 파일 로드 - (channels, time) 형태로 반환
audio, sr = torchaudio.load("stereo_audio.wav")
print(f"Original shape: {audio.shape}")  # 예: torch.Size([2, 16000])

# vLLM이 Whisper 기반 모델에 맞춰 자동으로 모노로 변환
llm = LLM(model="openai/whisper-large-v3")

outputs = llm.generate({
    "prompt": "",
    "multi_modal_data": {"audio": (audio.numpy(), sr)},
})

수동 변환은 필요 없어요. vLLM이 모델 요구 사항에 따라 채널 정규화를 자동으로 처리해요.

임베딩 입력 (Embedding Inputs)

특정 데이터 타입(즉, 이미지·비디오·오디오)에 속하는 미리 계산된 임베딩을 언어 모델에 직접 입력하려면, 형태가 (..., hidden_size of LM)인 텐서를 멀티모달 딕셔너리의 해당 필드에 넘기면 돼요. 정확한 형태는 사용하는 모델에 따라 달라져요.

이 기능은 enable_mm_embeds=True로 활성화해야 해요.

경고 잘못된 형태의 임베딩을 넘기면 vLLM 엔진이 크래시할 수 있어요. 이 플래그는 신뢰할 수 있는 사용자에게만 켜 주세요!

이미지 임베딩 (Image Embeddings)

from vllm import LLM

# 입력으로 이미지 임베딩을 사용한 추론
llm = LLM(model="llava-hf/llava-1.5-7b-hf", enable_mm_embeds=True)

# 올바른 형식은 HuggingFace 저장소를 참고하세요
prompt = "USER: <image>\nWhat is the content of this image?\nASSISTANT:"

# 대부분의 모델에서 `image_embeds`의 형태: (num_images, image_feature_size, hidden_size)
image_embeds = torch.load(...)

outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"image": image_embeds},
})

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

# 추가 필드가 필요한 모델의 예시
llm = LLM(
    "Qwen/Qwen2-VL-2B-Instruct",
    limit_mm_per_prompt={"image": 4},
    enable_mm_embeds=True,
)
mm_data = {
    "image": {
        # 형태: (total_feature_size, hidden_size)
        # total_feature_size = sum(image_feature_size for image in images)
        "image_embeds": torch.load(...),
        # 형태: (num_images, 3)
        # 위치 인코딩 계산에 image_grid_thw가 필요
        "image_grid_thw": torch.load(...),
    }
}

llm = LLM(
    "openbmb/MiniCPM-V-2_6",
    trust_remote_code=True,
    limit_mm_per_prompt={"image": 4},
    enable_mm_embeds=True,
)
mm_data = {
    "image": {
        # 형태: (num_images, num_slices, hidden_size)
        # num_slices는 이미지마다 다를 수 있음
        "image_embeds": [torch.load(...) for image in images],
        # 형태: (num_images, 2)
        # 슬라이스된 이미지의 세부 정보 계산에 image_sizes가 필요
        "image_sizes": [image.size for image in images],
    }
}

Qwen3-VL의 경우, image_embeds에는 기본 이미지 임베딩과 deepstack 피처가 모두 포함되어야 해요.

오디오 임베딩 입력 (Audio Embedding Inputs)

이미지 임베딩처럼 미리 계산된 오디오 임베딩도 넘길 수 있어요.

from vllm import LLM
import torch

# 오디오 임베딩 지원 활성화
llm = LLM(model="fixie-ai/ultravox-v0_5-llama-3_2-1b", enable_mm_embeds=True)

# 올바른 형식은 HuggingFace 저장소를 참고하세요
prompt = "USER: <audio>\nWhat is in this audio?\nASSISTANT:"

# 미리 계산된 오디오 임베딩 로드, 보통 형태:
# (num_audios, audio_feature_size, hidden_size of LM)
audio_embeds = torch.load(...)

outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"audio": audio_embeds},
})

for o in outputs:
    generated_text = o.outputs[0].text
    print(generated_text)

캐시된 입력 (Cached Inputs)

멀티모달 입력을 쓸 때 vLLM은 기본적으로 각 미디어 항목을 콘텐츠로 해싱해서 요청 간 캐싱을 가능하게 해요. 선택적으로 multi_modal_uuids를 넘겨 각 항목에 고유한 안정적인 ID를 제공하면, 원본 콘텐츠를 다시 해싱하지 않고도 요청 간 작업을 재사용할 수 있어요.

from vllm import LLM
from PIL import Image

# 두 이미지를 쓰는 Qwen2.5-VL 예시
llm = LLM(model="Qwen/Qwen2.5-VL-3B-Instruct")

prompt = "USER: <image><image>\nDescribe the differences.\nASSISTANT:"
img_a = Image.open("/path/to/a.jpg")
img_b = Image.open("/path/to/b.jpg")

outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"image": [img_a, img_b]},
    # 캐싱용 안정적인 ID 제공.
    # 요구 사항 (이 예시가 만족함):
    #  - multi_modal_data에 있는 모든 모달리티를 포함할 것.
    #  - 리스트의 경우 동일한 개수의 항목을 제공할 것.
    #  - 해당 항목은 콘텐츠 해싱으로 폴백하려면 None을 사용.
    "multi_modal_uuids": {"image": ["sku-1234-a", None]},
})

for o in outputs:
    print(o.outputs[0].text)

UUID를 쓰면 해당 항목의 캐시 히트를 기대할 수 있을 때 미디어 데이터를 아예 보내지 않을 수도 있어요. 단, 건너뛴 미디어에 해당하는 UUID가 없거나 UUID가 캐시에 히트하지 않으면 요청이 실패한다는 점을 유의하세요.

from vllm import LLM
from PIL import Image

# 두 이미지를 쓰는 Qwen2.5-VL 예시
llm = LLM(model="Qwen/Qwen2.5-VL-3B-Instruct")

prompt = "USER: <image><image>\nDescribe the differences.\nASSISTANT:"
img_b = Image.open("/path/to/b.jpg")

outputs = llm.generate({
    "prompt": prompt,
    "multi_modal_data": {"image": [None, img_b]},
    # img_a는 캐시될 것으로 예상되므로 실제 이미지를 아예 보내지 않아도 됨.
    "multi_modal_uuids": {"image": ["sku-1234-a", None]},
})

for o in outputs:
    print(o.outputs[0].text)

경고 멀티모달 프로세서 캐싱과 프리픽스 캐싱이 모두 비활성화되어 있으면, 사용자가 제공한 multi_modal_uuids는 무시돼요.

온라인 서빙 (Online Serving)

OpenAI 호환 서버는 Chat Completions API를 통해 멀티모달 데이터를 받아요. 미디어 입력은 각 미디어를 고유하게 식별하는 데 사용할 수 있는 선택적 UUID도 지원하는데, 이는 요청 간 미디어 결과를 캐시하는 데 쓰여요.

중요 Chat Completions API를 쓰려면 채팅 템플릿이 필수예요. HF 형식 모델의 경우 기본 채팅 템플릿이 chat_template.json 또는 tokenizer_config.json 안에 정의되어 있어요.

기본 채팅 템플릿이 없으면 먼저 vllm/transformers_utils/chat_templates/registry.py에서 내장 폴백을 찾아요. 폴백도 없다면 에러가 나고, --chat-template 인자로 채팅 템플릿을 직접 제공해야 해요.

특정 모델의 경우 examples 안에 대체 채팅 템플릿을 제공해요. 예를 들어 VLM2Vec는 Phi-3-Vision의 기본 템플릿과 다른 examples/pooling/embed/template/vlm2vec_phi3v.jinja를 사용해요.

이미지 입력 (Image Inputs)

이미지 입력은 OpenAI Vision API에 따라 지원돼요. Phi-3.5-Vision을 쓰는 간단한 예시를 보여드릴게요.

먼저 OpenAI 호환 서버를 실행하세요.

vllm serve microsoft/Phi-3.5-vision-instruct --runner generate \
  --trust-remote-code --max-model-len 4096 --limit-mm-per-prompt.image 2

그런 다음 OpenAI 클라이언트를 이렇게 쓰면 돼요.

import os
from openai import OpenAI

openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

# 단일 이미지 입력 추론

# 원격 이미지 처리를 테스트하기 위한 공개 이미지 URL
image_url = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/vision_model_images/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"

# 원격 이미지로 채팅 완성 생성
chat_response = client.chat.completions.create(
    model="microsoft/Phi-3.5-vision-instruct",
    messages=[
        {
            "role": "user",
            "content": [
                # NOTE: 이미지 토큰 `<image>`로 프롬프트를 포맷할 필요는 없어요.
                # API 서버가 프롬프트를 자동으로 처리하기 때문이에요.
                {
                    "type": "text",
                    "text": "What's in this image?",
                },
                {
                    "type": "image_url",
                    "image_url": {"url": image_url},
                    "uuid": image_url,  # 선택 사항
                },
            ],
        }
    ],
)
print("Chat completion output:", chat_response.choices[0].message.content)

# 로컬 이미지 파일 경로 (실제 이미지 파일을 가리키도록 수정)
image_file = "/path/to/image.jpg"

# 로컬 이미지 파일로 채팅 완성 생성
# --allowed-local-media-path 인자로 API 서버/엔진을 실행해야 함.
if os.path.exists(image_file):
    chat_completion_from_local_image_url = client.chat.completions.create(
        model="microsoft/Phi-3.5-vision-instruct",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": "What's in this image?",
                    },
                    {
                        "type": "image_url",
                        "image_url": {"url": f"file://{image_file}"},
                    },
                ],
            }
        ],
    )
    result = chat_completion_from_local_image_url.choices[0].message.content
    print("Chat completion output from local image file:\n", result)
else:
    print(f"Local image file not found at {image_file}, skipping local file test.")

# 다중 이미지 입력 추론
image_url_duck = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/duck.jpg"
image_url_lion = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/lion.jpg"

chat_response = client.chat.completions.create(
    model="microsoft/Phi-3.5-vision-instruct",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What are the animals in these images?",
                },
                {
                    "type": "image_url",
                    "image_url": {"url": image_url_duck},
                    "uuid": image_url_duck,  # 선택 사항
                },
                {
                    "type": "image_url",
                    "image_url": {"url": image_url_lion},
                    "uuid": image_url_lion,  # 선택 사항
                },
            ],
        }
    ],
)
print("Chat completion output:", chat_response.choices[0].message.content)

전체 예시: examples/generate/multimodal/openai_chat_completion_client_for_multimodal.py

로컬 파일 경로에서 로드하는 것도 vLLM에서 지원돼요. API 서버/엔진을 실행할 때 --allowed-local-media-path로 허용된 로컬 미디어 경로를 지정하고, API 요청의 url로 파일 경로를 넘기면 됩니다.

API 요청의 텍스트 콘텐츠에 이미지 플레이스홀더를 넣을 필요는 없어요. 이미 이미지 콘텐츠로 표현되니까요. 실제로는 텍스트와 이미지 콘텐츠를 섞어서 이미지 플레이스홀더를 텍스트 중간에 둘 수도 있어요.

참고 HTTP URL로 이미지를 가져오는 기본 타임아웃은 5초예요. 환경 변수로 바꿀 수 있어요:

export VLLM_IMAGE_FETCH_TIMEOUT=<timeout>

비디오 입력 (Video Inputs)

image_url 대신 video_url로 비디오 파일을 넘길 수 있어요. LLaVA-OneVision을 쓰는 간단한 예시를 보여드릴게요.

먼저 OpenAI 호환 서버를 실행하세요.

vllm serve llava-hf/llava-onevision-qwen2-0.5b-ov-hf --runner generate --max-model-len 8192

그런 다음 OpenAI 클라이언트를 이렇게 쓰면 돼요.

from openai import OpenAI

openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

video_url = "https://huggingface.co/datasets/raushan-testing-hf/videos-test/resolve/main/sample_demo_1.mp4"

## 페이로드에 비디오 URL 사용
chat_completion_from_url = client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this video?",
                },
                {
                    "type": "video_url",
                    "video_url": {"url": video_url},
                    "uuid": video_url,  # 선택 사항
                },
            ],
        }
    ],
    model=model,
    max_completion_tokens=64,
)

result = chat_completion_from_url.choices[0].message.content
print("Chat completion output from image url:", result)

전체 예시: examples/generate/multimodal/openai_chat_completion_client_for_multimodal.py

참고 HTTP URL로 비디오를 가져오는 기본 타임아웃은 30초예요. 환경 변수로 바꿀 수 있어요:

export VLLM_VIDEO_FETCH_TIMEOUT=<timeout>

비디오 디코딩 백엔드 (Video Decoding Backend)

vLLM은 선택 가능한 디코딩 백엔드로 비디오 바이트를 프레임으로 디코딩해요. 네 가지 백엔드를 지원해요.

백엔드 디바이스 설명
opencv (기본값) CPU OpenCV 기반 디코더
torchcodec CPU TorchCodec (PyTorch 네이티브) 디코더
pynvvideocodec GPU NVIDIA PyNvVideoCodec 디코더
deepstream GPU NVIDIA DeepStream 디코더

두 CPU 백엔드는 궁극적으로 FFmpeg를 기반으로 해요. torchcodec는 어떤 FFmpeg 버전을 쓸지 골라주고, opencv는 링크된 FFmpeg 빌드를 그대로 사용해요.

--media-io-kwargsbackend 파라미터를 넘겨 백엔드를 선택해요.

vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "torchcodec"}}'

TorchCodec 전용 파라미터:

다음 파라미터는 torchcodec 백엔드에만 적용돼요.

  • num_ffmpeg_threads: FFmpeg 디코딩 스레드 수. 0(기본값)은 FFmpeg 기본값인 min(cpu_count + 1, 16)을 사용해요. 이를 통해 스레드 초과 할당을 제어할 수 있어요.
  • seek_mode: 디코더의 탐색 모드. "exact"(기본값)은 디코더 생성 시 파일을 스캔해서 프레임 정확한 샘플링을 보장해요. "approximate"는 그 스캔을 건너뛰어 디코더 생성을 더 빠르게 하지만, 파일 메타데이터에 의존해서(탐색 정확도가 덜할 수 있음) 그 대가를 치러요.
# 예시: 근사 탐색 모드와 FFmpeg 스레드 4개를 쓰는 TorchCodec
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "torchcodec", "seek_mode": "approximate", "num_ffmpeg_threads": 4}}'

PyNvVideoCodec 전용 파라미터:

  • hw_decoders: 각 API 서버 프로세스가 보유하는 최대 동시 하드웨어 디코더 슬롯 수. 양의 정수여야 하며 기본값은 2로, 동시 비디오 워크로드의 권장 시작점이에요. vLLM이 시작 시 이 슬롯들을 위해 GPU 메모리를 예약하기 때문에 이 값은 요청별로 오버라이드할 수 없어요. 슬롯이 추가될 때마다 GPU 메모리 예약이 늘어나므로 늘리기 전에 벤치마크를 먼저 하세요.
# 예시: 권장되는 하드웨어 디코더 2개를 명시적으로 사용
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "pynvvideocodec", "hw_decoders": 2}}'

비디오 프레임 복구 (Video Frame Recovery)

손상되었거나 잘린 비디오 파일을 처리할 때 견고성을 높이기 위해, vLLM은 동적 윈도우 전방 스캔 방식을 쓴 선택적 프레임 복구를 지원해요. 활성화하면 순차 읽기 중 대상 프레임 로드가 실패할 때, (다음 대상 프레임 전에) 성공적으로 잡힌 다음 프레임이 그 자리를 대신해요.

비디오 프레임 복구를 켜려면 --media-io-kwargsframe_recovery 파라미터를 넘기면 돼요.

# 예시: 프레임 복구 활성화
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"frame_recovery": true}}'

파라미터:

  • frame_recovery: 전방 스캔 복구를 활성화하는 불리언 플래그. true면 실패한 프레임을 동적 윈도우(다음 대상 프레임까지) 안의 다음 사용 가능한 프레임으로 복구해요. 기본값은 false예요.

작동 방식:

  1. 시스템이 프레임을 순차적으로 읽어요.
  2. 대상 프레임을 잡는 데 실패하면 "failed"로 표시돼요.
  3. (다음 대상에 도달하기 전에) 성공적으로 잡힌 다음 프레임으로 실패한 프레임을 복구해요.
  4. 이 방식은 비디오 중간 손상과 비디오 끝 잘림을 모두 처리해요.

OpenCV 백엔드를 쓸 때 MP4 같은 일반적인 비디오 형식과 함께 동작해요.

PyNvVideoCodec(NVDEC)로 GPU 비디오 디코딩

pynvvideocodec 백엔드는 NVIDIA NVDEC를 써서 샘플링된 비디오 프레임을 GPU에서 디코딩한 뒤, 멀티모달 전처리를 위해 호스트 메모리로 복사해요. 비디오 태깅처럼 비디오가 크고 추론이 비교적 가벼운 워크로드에서는, CPU 기반 비디오 디코더의 병목을 완화할 수 있어요.

경고 이 백엔드를 쓸 때는 CUDA MPS(Multi-Process Service)가 필요해요. 비디오 디코딩은 API 서버 프로세스에서, 모델 서빙은 엔진 프로세스에서 실행되므로 여러 CUDA 프로세스가 같은 GPU를 공유해요. vLLM을 시작하기 전에 MPS를 구성하고 시작하세요.

또한 비디오 디코딩용 VRAM을 예약하려면 양수 --mm-ipc-gpu-memory-gb 값을 설정해야 해요. vLLM은 이 예산을 KV 캐시에 사용 가능한 메모리에서 잘라내고, 이를 통해 동시 프론트엔드 디코드 할당을 제한해요. 예산이 소진되면 디코드 작업은 엔진의 VRAM 헤드룸을 소모해 요청 서빙 중 OOM(메모리 부족)을 유발하는 대신 대기해요.

환경 변수로 백엔드를 선택하고 워크로드에 맞는 VRAM 예산을 지정하세요. 예를 들어 1 GiB를 예약하려면:

export VLLM_VIDEO_LOADER_BACKEND=pynvvideocodec
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --mm-ipc-gpu-memory-gb 1

또는 --media-io-kwargs로 선택할 수도 있어요.

vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "pynvvideocodec"}}' \
  --mm-ipc-gpu-memory-gb 1

단일 API 서버 프로세스가 디코딩해야 하는 가장 큰 샘플링 비디오를 수용할 만큼 충분한 예산을 고르세요. 여러 API 서버 프로세스를 쓸 때 vLLM은 설정된 예산을 프로세스 간에 균등하게 나눠요.

스트리밍 비디오 소스에는 DeepStream 백엔드를 쓰세요.

DeepStream(NVDEC)으로 GPU 비디오 디코딩

기본적으로 vLLM은 CPU에서 비디오를 디코딩해요. NVIDIA GPU에서는 DeepStream 백엔드로 하드웨어 비디오 엔진(NVDEC)에서 직접 디코딩할 수 있는데, 디코딩을 CPU에서 떼어내 비디오 처리량을 크게 높일 수 있어요. 스트리밍 비디오 소스에 권장되는 GPU 백엔드예요.

백엔드를 설치하세요 (Linux x86-64만 지원):

pip install vllm[deepstream]

pip 휠에는 DeepStream 라이브러리가 번들로 들어있지만, pip로 설치할 수 없는 일부 시스템 패키지에는 여전히 의존해요. Ubuntu에서는:

apt-get install -y \
  gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
  gstreamer1.0-plugins-bad gstreamer1.0-libav \
  python3-gi python3-gst-1.0 libv4l-0 cuda-libraries-13-0

환경 변수로 백엔드를 선택하거나:

export VLLM_VIDEO_LOADER_BACKEND=deepstream
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct

--media-io-kwargs로 요청별로 선택할 수도 있어요.

vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "deepstream"}}'

파라미터:

  • pool_size: 프로세스 전체 디코드 풀의 GPU 디코드 워커 수 ([1, 16]로 제한). 설정하지 않으면 VLLM_MEDIA_LOADING_THREAD_COUNT(기본 8)을 기본값으로 사용해요. 풀은 싱글턴이라 첫 번째 요청의 값이 이겨요.
# 예시: 디코드 워커 12개
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
  --media-io-kwargs '{"video": {"backend": "deepstream", "pool_size": 12}}'

media_io_kwargs로 미리 추출된 프레임 시퀀스

클라이언트 쪽에서 비디오 프레임을 추출해 video/jpeg(base64로 연결된 JPEG 프레임)로 보낼 때, 요청에 media_io_kwargs를 사용하면 원본 비디오 메타데이터를 보존할 수 있어요. 이렇게 하면 클라이언트 쪽 프레임 추출 과정에서 잃어버리기 쉬운 시간 정보를 보존해서 더 정확한 비디오 이해가 가능해져요.

지원되는 파라미터:

파라미터 타입 설명
fps float 원본 비디오의 프레임 레이트
frames_indices list[int] 실제로 샘플링된 프레임의 인덱스
total_num_frames int 원본 비디오의 총 프레임 수
duration float 원본 비디오 길이(초)
do_sample_frames bool 프레임 샘플링 수행 여부
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")

# 클라이언트 쪽 프레임 추출
frames = extract_frames(video_path, num_frames=32)
frames_b64 = ",".join([encode_image(f) for f in frames])
video_url = f"data:video/jpeg;base64,{frames_b64}"

# media_io_kwargs로 비디오 메타데이터 전달
response = client.chat.completions.create(
    model="your-multimodal-model",
    messages=[{
        "role": "user",
        "content": [
            {"type": "video_url", "video_url": {"url": video_url}},
            {"type": "text", "text": "Describe what happens in this video."}
        ]
    }],
    extra_body={
        "media_io_kwargs": {
            "video": {
                "fps": 30.0,
                "frames_indices": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90,
                                   100, 110, 120, 130, 140, 150, 160, 170,
                                   180, 190, 200, 210, 220, 230, 240, 250,
                                   260, 270, 280, 290, 300, 310],
                "total_num_frames": 900,
                "duration": 30.0,
            }
        }
    },
)

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

media_io_kwargs를 쓸까?

클라이언트 쪽에서 프레임을 추출하면 서버가 원본 비디오에 대한 중요한 컨텍스트를 잃어요.

  • 시간 정보: 어떤 프레임이 샘플링됐고, 원본 타임라인에서 어디에 위치했는지
  • 비디오 길이: 원본 비디오가 얼마나 길었는지
  • 프레임 레이트: 원본 재생 속도

이 메타데이터를 넘기면 모델이 샘플링된 프레임의 시간 분포와, 중요한 순간이 건너뛰어졌을지 더 잘 이해할 수 있어요.

커스텀 RGBA 배경색 (Custom RGBA Background Color)

RGBA 이미지에 커스텀 배경색을 쓰려면 --media-io-kwargsrgba_background_color 파라미터를 넘기면 돼요.

# 예시: 다크 테마용 검정 배경
vllm serve llava-hf/llava-1.5-7b-hf \
  --media-io-kwargs '{"image": {"rgba_background_color": [0, 0, 0]}}'

# 예시: 커스텀 회색 배경
vllm serve llava-hf/llava-1.5-7b-hf \
  --media-io-kwargs '{"image": {"rgba_background_color": [128, 128, 128]}}'

오디오 입력 (Audio Inputs)

오디오 입력은 OpenAI Audio API에 따라 지원돼요. Ultravox-v0.5-1B를 쓰는 간단한 예시를 보여드릴게요.

먼저 OpenAI 호환 서버를 실행하세요.

vllm serve fixie-ai/ultravox-v0_5-llama-3_2-1b

그런 다음 OpenAI 클라이언트를 이렇게 쓰면 돼요.

import base64
import requests
from openai import OpenAI
from vllm.assets.audio import AudioAsset

def encode_base64_content_from_url(content_url: str) -> str:
    """원격 url에서 가져온 콘텐츠를 base64 형식으로 인코딩."""

    with requests.get(content_url) as response:
        response.raise_for_status()
        result = base64.b64encode(response.content).decode('utf-8')

    return result

openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

# soundfile/PyAV가 지원하는 어떤 형식이든 지원
audio_url = AudioAsset("winning_call").url
audio_base64 = encode_base64_content_from_url(audio_url)

chat_completion_from_base64 = client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this audio?",
                },
                {
                    "type": "input_audio",
                    "input_audio": {
                        "data": audio_base64,
                        "format": "wav",
                    },
                    "uuid": audio_url,  # 선택 사항
                },
            ],
        }
    ],
    model=model,
    max_completion_tokens=64,
)

result = chat_completion_from_base64.choices[0].message.content
print("Chat completion output from input audio:", result)

또는 audio_url을 넘길 수도 있어요. 이미지 입력의 image_url에 해당하는 오디오 버전이에요.

chat_completion_from_url = client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this audio?",
                },
                {
                    "type": "audio_url",
                    "audio_url": {"url": audio_url},
                    "uuid": audio_url,  # 선택 사항
                },
            ],
        }
    ],
    model=model,
    max_completion_tokens=64,
)

result = chat_completion_from_url.choices[0].message.content
print("Chat completion output from audio url:", result)

전체 예시: examples/generate/multimodal/openai_chat_completion_client_for_multimodal.py

참고 HTTP URL로 오디오를 가져오는 기본 타임아웃은 10초예요. 환경 변수로 바꿀 수 있어요:

export VLLM_AUDIO_FETCH_TIMEOUT=<timeout>

오디오 디코딩 백엔드 (Audio Decoding Backend)

vLLM은 선택 가능한 디코딩 백엔드로 오디오 바이트를 파형으로 디코딩해요.

백엔드 설명
auto (기본값) soundfile, 없으면 torchcodec, 그다음 PyAV 순으로 폴백
soundfile libsndfile만, 폴백 없음
pyav PyAV(FFmpeg)만, 폴백 없음
torchcodec TorchCodec(PyTorch 네이티브)만, 폴백 없음

--media-io-kwargs로 서버별 백엔드를 선택해요.

vllm serve mistralai/Voxtral-Mini-3B-2507 \
  --media-io-kwargs '{"audio": {"audio_backend": "soundfile"}}'

pyav는 프레임별 Python 제너레이터로 FFmpeg를 구동해서, 동시성 상황에서 Python/C 전환이 GIL에서 경합돼요. torchcodec는 각 스트림을 GIL을 전체 기간 동안 해제하는 단일 호출로 디코딩해요. 동시 디코딩 워크로드에는 명시적으로 선택하세요. auto는 지원되는 형식에 soundfile을 우선해 기존 디코딩 동작(인코더 패딩 포함)을 보존해요. 비디오 컨테이너처럼 soundfile이 읽지 못하는 형식에서 추출한 오디오는 폴백 체인을 통해 torchcodec을 쓸 수 있어요.

참고 torchcodec는 CUDA, CPU, XPU 빌드에서 요구 사항으로 함께 딸려와요. 다른 플랫폼(예: ROCm, TPU)에서는 그 백엔드를 활성화하려면 수동으로 설치해야 해요. torchcodec은 또한 시스템 FFmpeg 설치와 링크돼요. 패키지나 FFmpeg를 쓸 수 없으면, auto는 soundfile → PyAV 체인을 사용해요.

임베딩 입력 (Embedding Inputs)

특정 데이터 타입(즉, 이미지·비디오·오디오)에 속하는 미리 계산된 임베딩을 언어 모델에 직접 입력하려면, 각 항목에 형태가 (..., hidden_size of LM)인 텐서를 멀티모달 딕셔너리의 해당 필드에 넘기면 돼요.

중요 오프라인 추론과 달리, 채팅 템플릿이 플레이스홀더 토큰을 올바르게 적용하려면 각 항목의 임베딩을 따로 넘겨야 해요.

이 기능은 vllm serve--enable-mm-embeds 플래그로 활성화해야 해요.

경고 잘못된 형태의 임베딩을 넘기면 vLLM 엔진이 크래시할 수 있어요. 이 플래그는 신뢰할 수 있는 사용자에게만 켜 주세요!

이미지 임베딩 입력 (Image Embedding Inputs)

이미지 임베딩의 경우, base64로 인코딩된 텐서를 image_embeds 필드에 넘길 수 있어요. 다음 예시는 OpenAI 서버에 이미지 임베딩을 넘기는 방법을 보여줘요.

from vllm.utils.serial_utils import tensor2base64

client = OpenAI(
    # 기본값은 os.environ.get("OPENAI_API_KEY")
    api_key=openai_api_key,
    base_url=openai_api_base,
)

# 기본 사용법 - 오프라인 추론의 LLaVA 예시와 동일
model = "llava-hf/llava-1.5-7b-hf"
embeds = {
    "type": "image_embeds",
    "image_embeds": tensor2base64(torch.load(...)),  # 형태: (image_feature_size, hidden_size)
    "uuid": image_url,  # 선택 사항
}

# 추가 필드가 필요한 모델의 추가 예시
model = "Qwen/Qwen2-VL-2B-Instruct"
embeds = {
    "type": "image_embeds",
    "image_embeds": {
        "image_embeds": tensor2base64(torch.load(...)),  # 형태: (image_feature_size, hidden_size)
        "image_grid_thw": tensor2base64(torch.load(...)),  # 형태: (3,)
    },
    "uuid": image_url,  # 선택 사항
}

model = "openbmb/MiniCPM-V-2_6"
embeds = {
    "type": "image_embeds",
    "image_embeds": {
        "image_embeds": tensor2base64(torch.load(...)),  # 형태: (num_slices, hidden_size)
        "image_sizes": tensor2base64(torch.load(...)),  # 형태: (2,)
    },
    "uuid": image_url,  # 선택 사항
}

# 단일 이미지 입력
chat_completion = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant.",
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this image?",
                },
                embeds,
            ],
        },
    ],
    model=model,
)

# 다중 이미지 입력
chat_completion = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant.",
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this image?",
                },
                embeds,
                embeds,
            ],
        },
    ],
    model=model,
)

# 다중 이미지 입력 (교차 배치)
chat_completion = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant.",
        },
        {
            "role": "user",
            "content": [
                embeds,
                {
                    "type": "text",
                    "text": "What's in this image?",
                },
                embeds,
            ],
        },
    ],
    model=model,
)

캐시된 입력 (Cached Inputs)

오프라인 추론과 마찬가지로, 제공한 UUID로 캐시 히트가 예상되면 미디어를 보내지 않고 건너뛸 수 있어요. 미디어를 이렇게 보내면 됩니다.

    # 이미지/비디오/오디오 URL:
    {
        "type": "image_url",
        "image_url": None,
        "uuid": image_uuid,
    },

    # image_embeds
    {
        "type": "image_embeds",
        "image_embeds": None,
        "uuid": image_uuid,
    },

    # input_audio:
    {
        "type": "input_audio",
        "input_audio": None,
        "uuid": audio_uuid,
    },

    # PIL Image:
    {
        "type": "image_pil",
        "image_pil": None,
        "uuid": image_uuid,
    },

    # video_url:
    {
        "type": "video_url",
        "video_url": {},
        "uuid": video_uuid,
    },