비전-언어 모델¶
목차¶
- 비전-언어 모델이란?
- VLM 파이프라인
- 지원 VLM 패밀리
- Florence-2
- Falcon-OCR 레이아웃 인식 OCR
- GOT-OCR 2.0
- Nemotron-Parse
- 비전 인코더
- 비전-텍스트 커넥터
- CLI에서 VLM 사용
- API에서 VLM 사용
- 비디오 및 오디오 입력
- 고유 비디오 경로가 없는 모델의 비디오
- Vision Feature Cache
비전-언어 모델이란?¶
비전-언어 모델(VLM)은 이미지와 텍스트를 모두 이해할 수 있는 모델입니다. 추론 시에 하나 이상의 이미지가 텍스트 프롬프트와 함께 전달되면, 모델은 두 모달리티를 모두 고려한 텍스트 응답을 생성합니다.
VLM은 다음과 같은 작업에 활용됩니다:
- 이미지 설명 - 사진, 다이어그램, 스크린샷에 대한 캡션이나 상세한 설명 생성.
- 시각적 질의응답 - 이미지 내용에 관한 질문에 답변.
- 문서 이해 - 스캔된 문서, 차트, 표에서 구조화된 정보 추출.
- 코드 및 UI 지원 - 코드 편집기, 웹 페이지, 애플리케이션 인터페이스 스크린샷 분석.
- 다중 이미지 비교 - 두 개 이상의 이미지 간의 차이점 또는 관계 설명.
mlxcel은 VLM을 비전 모듈이 부착된 텍스트 언어 모델로 취급합니다. 텍스트 모델은 표준 언어 모델과 동일하게 로드되고 비전 모듈은 이미지를 텍스트 토큰과 동일한 임베딩 공간으로 처리하는 추가 가중치 집합입니다.
VLM 파이프라인¶
이미지가 프롬프트와 함께 전달되면, mlxcel은 결합된 표현이 텍스트 모델에 도달하기 전에 4단계 파이프라인을 통해 처리합니다.
flowchart LR
IMG[Raw Image] --> PROC[Image Processor\nresize / normalize / tile]
PROC --> ENC[Vision Encoder\nViT / SigLIP / Pixtral]
ENC --> CONN[Connector\nMLP / Linear / Merger]
CONN --> MERGE[Embedding Merge\ninsert at image token positions]
TEXT[Text Tokens] --> EMBED[Text Embedding Table]
EMBED --> MERGE
MERGE --> LLM[Text Language Model\nforward pass]
LLM --> Out[Generated Tokens] 1단계: 이미지 프로세서
이미지 프로세서는 원시 입력 이미지를 처리합니다. 모델 패밀리에 따라 이 단계에서 다음 작업이 수행될 수 있습니다: - 이미지를 고정 해상도로 크기 조정. - 고해상도 지원을 위해 이미지를 고정 크기 패치 그리드로 타일링. - 인코더가 기대하는 범위와 평균/분산으로 픽셀 값을 정규화.
2단계: 비전 인코더
비전 인코더는 처리된 픽셀 데이터를 패치 수준 은닉 상태 벡터 시퀀스로 변환하는 별도의 신경망(일반적으로 Vision Transformer, 즉 ViT)입니다. 각 패치는 이미지의 한 영역에 해당합니다. 출력은 [batch, num_patches, hidden_dim] 형태의 텐서입니다.
3단계: 커넥터(프로젝터)
커넥터는 비전 인코더의 은닉 차원을 텍스트 모델의 은닉 차원으로 매핑하는 소형 프로젝션 네트워크입니다. 두 네트워크의 너비가 다른 경우가 많기 때문에 이 과정이 필요합니다. 가장 단순한 커넥터는 단일 선형 레이어이고 더 복잡한 커넥터는 다층 MLP 또는 공간 풀링을 사용합니다.
4단계: 임베딩 병합
프로젝션된 이미지 특징은 텍스트 토큰 임베딩 시퀀스에서 이미지 플레이스홀더 토큰이 있던 자리에 삽입됩니다. 결합된 시퀀스는 표준 토큰 임베딩 시퀀스인 것처럼 텍스트 언어 모델에 전달됩니다. 텍스트 모델은 수정되지 않습니다.
지원 VLM 패밀리¶
mlxcel은 config.json의 model_type 필드에서 자동으로 식별되는 다음 VLM 패밀리에 대한 로더를 포함합니다:
| 패밀리 | 모델 | 비고 |
|---|---|---|
| Gemma 3 | Gemma3 VL 변형 | Gemma 스타일 4D 어텐션 마스크 병합 |
| Gemma 3n | Gemma3n VL 변형 | Gemma 스타일 병합 |
| Gemma 4 | Gemma 4 VLM 변형 | 이미지, 비디오, 선택적 오디오 피처 경로 |
| Gemma 4 Unified | Gemma 4 Unified | encoder-free 텍스트, 이미지, 오디오, 비디오. 한 프롬프트에 비디오와 오디오를 함께 받는 유일한 계열입니다 |
| DiffusionGemma | diffusiongemma-26B-A4B | Gemma 4 비전 파이프라인(768x768, 이미지당 256 soft token)이 블록 디퓨전 텍스트 디코더에 연결; 블록 디퓨전 참조 |
| LLaVA | LLaVA 1.5, 1.6; Bunny | LLaVA 스타일 토큰 교체 |
| Llama 4 | Llama 4 Scout, Maverick | 네이티브 이미지 타일링을 갖춘 Llama4 비전 인코더 |
| Qwen2-VL | Qwen2-VL | Multimodal RoPE(MRoPE)를 사용하는 Qwen2-VL ViT |
| Qwen2.5-VL | Qwen2.5-VL | MRoPE를 사용하는 Qwen2-VL ViT |
| Qwen3-VL | Qwen3-VL, Qwen3-VL-MoE | Qwen3 VL ViT |
| Qwen3.5-VL | Qwen3.5-VL, Qwen3.5-VL-MoE | Qwen3.5 VL ViT |
| Qwen3.8-Flash-Next | Qwen3.8-Flash-Next (qwen4_exp) | Qwen3.5 계열 하이브리드 텍스트 스택 위에 Qwen4-Exp 비전 타워를 얹은 구조. 텍스트, 이미지, 비디오 입력 |
| LFM2-VL | LFM2-VL | Packed-patch SigLIP2 ViT, pixel-unshuffle 프로젝터, LFM2 하이브리드 텍스트 모델 |
| Mage-VL | Mage-VL (microsoft/Mage-VL, mlx-community/Mage-VL-8bit) | 이미지 입력만 받습니다. Qwen2-VL smart_resize(patch 16, 2x2 merge)를 거쳐 1-D RoPE를 쓰는 Qwen3-4B 디코더로 들어갑니다. codec-native 비디오, 프레임 샘플링 비디오, LoRA, 텐서 병렬화는 지원하지 않습니다 |
| Youtu-VL | Youtu-VL | SigLIP2 windowed-attention 인코더와 DeepSeek-V3 스타일 MLA 텍스트 |
| Cohere Compass | North-Micro-Vision | DeepStack merger를 쓰는 Qwen3-VL 타워와 Command 계열 parallel decoder |
| LLM-jp-VL | LLM-jp-4 VL, Jagle-VL | InternVL 방식 dynamic tiling을 쓰는 SigLIP2-so400m. 텍스트 백본은 Llama 또는 Qwen3 |
| Kimi K3 | Kimi K3 | Kimi K3 텍스트 백본 앞의 MoonViT3D native-resolution 타워. 비디오는 프레임 대체 경로를 거치며, 요청당 media token은 최대 32768개입니다(MLXCEL_KIMI_K3_MAX_MEDIA_TOKENS) |
| Phi 3 Vision | Phi-3-vision | CLIP 기반 인코더 |
| Phi 4 MM | Phi-4-multimodal | 멀티모달 Phi4 |
| Phi 4 SigLIP | SigLIP을 사용하는 Phi-4 | SigLIP 인코더 변형 |
| Pixtral | Pixtral 12B | 2D RoPE ViT |
| Mistral 3 | Mistral-Small-3.1 | 공간 병합 커넥터를 갖춘 Pixtral 인코더 |
| PaliGemma | PaliGemma ½ | SigLIP 인코더; 이미지 토큰 앞에 BOS 없음 |
| Aya Vision | Aya Vision | Aya 커넥터를 사용하는 SigLIP 인코더 |
| MiniCPM-o | MiniCPM-o 2.6/3.0 | 커스텀 이미지 슬롯 토큰화 |
| MiniCPM-V 4.6 | MiniCPM-V 4.6 | Qwen3.5 텍스트 백본을 사용하는 SigLIP + VitMerger + Merger 파이프라인 |
| Moondream 3 | Moondream3 | 전용 프롬프트 준비 로직 |
| Molmo 2 | Molmo2 및 Molmo-Point 변형 | 토큰 확장을 사용하는 Molmo 인코더 |
| Nemotron H Nano Omni | Nemotron H Nano Omni | 선택적 오디오 모달리티가 있는 비전 지원 Nemotron-H 변형 |
| Florence-2 | Florence-2 base-ft 및 ¾/6/8비트 변환본 | 인코더-디코더. 자유 지시가 아니라 태스크 마커로 구동합니다. Florence-2 참조 |
| LocateAnything | LocateAnything 3B | 생성형 그라운딩. 참조 span과 박스가 일반 텍스트로 나옵니다 |
| Falcon-OCR | Falcon-OCR 300M | 비전 타워 없는 early fusion. 레이아웃 인식 2단계는 선택 사항입니다 |
| GOT-OCR 2.0 | GOT-OCR2_0 | 고정 1024x1024 SAM 스타일 타워와 Qwen2-0.5B 디코더. 요청당 이미지 한 장. GOT-OCR 2.0 참조 |
| Nemotron-Parse | Nemotron-Parse 2.0 (nvidia/NVIDIA-Nemotron-Parse-2.0, mlx-community/Nemotron-Parse-2.0-{4bit,8bit}) | 인코더-디코더 문서 OCR·레이아웃 모델. 요청당 이미지 한 장. Nemotron-Parse 참조 |
| Jina VLM | jina-vlm | overlap-and-resize 크로퍼 + Molmo 스타일 2x2 풀링 커넥터 |
| Muse Glimmer | Meta Muse Glimmer 30B | dense BF16과 고정 affine Q4. 단일 프로세스 전용입니다 |
VLM 계열의 텍스트 전용 체크포인트¶
Qwen-VL 계열(Qwen2-VL, Qwen2.5-VL, Qwen3-VL, Qwen3.5-VL)과 Mage-VL은 비전 타워를 뺀 체크포인트도 로드합니다. config.json에 vision_config가 없거나, vision_config가 null 또는 {}이거나, 최상위 language_model_only가 true이거나, safetensors 인덱스에 비전 가중치(vision_tower.*, model.visual.* 등 타워 접두사)가 없으면 비전 타워 없이 로드됩니다. 이때 텍스트 요청은 그대로 실행되고, 이미지나 비디오 요청은 model <path> was loaded without a vision tower (text-only checkpoint) 오류로 실패합니다. language_model_only: true는 비전 가중치가 들어 있어도 우선하며, mlxcel은 텍스트 전용으로 로드한다는 안내를 stderr에 출력합니다. Qwen 3.5에서 language_model_only 값이 불리언이 아니면 로드 단계에서 거부합니다.
Florence-2¶
Florence-2는 (Nemotron-Parse와 같은) 인코더-디코더 VLM이고, 자유 지시가 아니라 태스크 마커로 구동됩니다. DaViT 타워가 768x768 이미지를 특징 격자로 인코딩하고, 투영된 이미지 토큰이 태스크 프롬프트 임베딩 앞에 이어붙으며, BART 디코더가 캐시된 인코더 출력에 크로스 어텐션하며 greedy로 답을 생성합니다.
-p/--prompt 문자열이 열다섯 가지 태스크 모드 중 하나를 고릅니다. 마커는 대소문자를 가리지 않고 꺾쇠괄호는 생략할 수 있습니다.
mlxcel generate -m models/Florence-2-base-ft-bf16 --image page.png -p '<OD>'
mlxcel generate -m models/Florence-2-base-ft-bf16 --image photo.jpg -p '<DETAILED_CAPTION>'
mlxcel generate -m models/Florence-2-base-ft-bf16 --image doc.png -p '<OCR_WITH_REGION>'
일곱 가지 태스크는 마커 뒤에 입력 텍스트를 받습니다. 예를 들어 <CAPTION_TO_PHRASE_GROUNDING> a red car처럼 씁니다. 공간 답변은 체크포인트의 1000구간 <loc_*> 토큰으로 돌아오고, 원본 이미지 크기 기준의 픽셀 좌표 박스, 사각형 박스, 폴리곤으로 파싱됩니다.
HTTP에서는 OpenAI 호환 POST /v1/chat/completions에 태스크 프롬프트를 텍스트로 담은 user 메시지 하나와 image_url 콘텐츠 파트 하나를 보냅니다. message.content에는 CLI가 출력하는 것과 같은 렌더링 텍스트가 들어가고, 파싱된 좌표는 mlxcel 전용 확장 필드 message.florence2_result에 JSON으로 함께 옵니다.
서빙은 설계상 한 번에 한 요청입니다. 인코더 패스는 디코드 루프와 비용 특성이 달라서, 요청은 배치·paged 스케줄러가 아니라 전용 워커 채널(--max-queue-depth로 상한)에 대기했다가 순차적으로 처리됩니다. 디코드가 greedy이므로 샘플링 파라미터와 response_format은 받되 무시하고, stream: true 요청은 파싱 결과가 전체 디코드 이후에만 존재하므로 답변 전체를 델타 청크 하나로 전달합니다.
dense와 양자화 export 모두 로드됩니다. -large-ft 릴리스는 로드는 되지만 공개된 MLX 변환본에서 쓸 만한 텍스트를 생성하지 못하므로 -base-ft를 권합니다.
Falcon-OCR 레이아웃 인식 OCR¶
Falcon-OCR는 기본적으로 페이지 전체를 한 번에 전사합니다. 레이아웃 검출 파일을 넘기면 카테고리 라우팅, 중첩 박스 억제, 영역 크롭, 카테고리별 지시를 적용하는 레이아웃 인식 2단계로 전환됩니다.
검출은 별도 명령으로 남아 있으며 검출 결과는 generate의 입력입니다. PP-DocLayoutV3 체크포인트(박스를 읽기 순서로 내보내는 기준 레이아웃 검출기) 또는 RT-DETRv2 레이아웃 체크포인트로 mlxcel detect --format json을 실행하면 이 파일이 만들어집니다. generate는 받은 파일의 순서를 그대로 유지하고, mlxcel detect는 읽기 순서 head가 없는 RT-DETRv2의 박스를 위에서 아래, 왼쪽에서 오른쪽 순으로 정렬합니다.
GOT-OCR 2.0¶
GOT-OCR 2.0(model_type: GOT)은 0.58B 문서 OCR 모델입니다. 모든 페이지를 고정 1024x1024 캔버스로 늘린 뒤 정확히 특징 행 256개로 만들므로 타일링이나 종횡비 보존이 없습니다. 요청당 이미지 한 장만 받습니다.
체크포인트에 채팅 템플릿이 없습니다. mlxcel은 참조 구현이 만드는 고정 대화를 CLI와 서버 모두에서 제공하며, 클라이언트의 system 메시지는 이 고정 메시지로 바꿉니다. 지시문이 모드를 정합니다.
| 지시문 | 출력 |
|---|---|
OCR: | 일반 텍스트 |
OCR with format: | Markdown / LaTeX |
[x1,y1,x2,y2] OCR with format: | 0..1000으로 정규화한 좌표의 한 영역 |
[red] OCR with format: | 색으로 표시한 영역 |
생성은 <|endoftext|>뿐 아니라 <|im_end|> 턴 구분자에서도 멈춥니다. 다중 크롭 OCR은 구현되어 있지 않습니다.
Nemotron-Parse¶
Nemotron-Parse(model_type: nemotron_parse)는 NVIDIA의 문서 OCR·레이아웃 모델로, 인코더-디코더 VLM입니다. 페이지는 종횡비를 유지한 채 2048x1664 안에 맞게 줄인 뒤 흰색으로 채워 정확히 그 크기로 만듭니다. C-RADIOv2-H 타워가 이를 인코더 상태 3329개로 인코딩하고, 토큰화한 태스크 프롬프트로 시작하는 10층 mBART 디코더가 </s>까지 greedy로 디코딩합니다. 답은 <x_..><y_..> 박스 토큰(페이지 기준 정규화)과 <class_..> 태그가 붙은 markdown이며, 특수 토큰을 그대로 둔 채 출력합니다.
Florence-2처럼 자기 CLI 드라이버와 전용 단일 스트림 서버 워커에서 실행되며, 자기회귀 루프나 배치 스케줄러를 거치지 않습니다. CLI에서는 -p 문자열이 태스크 프롬프트입니다.
mlxcel generate -m models/Nemotron-Parse-2.0-8bit --image page.png \
-p '</s><s><predict_bbox><predict_classes><output_markdown><predict_no_text_in_pic>' -n 4096
학습된 프롬프트는 </s><s> 뒤에 다음 제어 토큰을 조합합니다.
| 토큰 | 효과 |
|---|---|
<predict_bbox> | 박스 토큰을 출력 |
<predict_classes> | 클래스 태그를 출력 |
<output_markdown> | markdown 텍스트를 출력 |
<predict_no_text_in_pic> | 그림 안의 텍스트는 받아 적지 않음(기본값. 이 토큰과 다음 토큰 중 정확히 하나를 씁니다) |
<predict_text_in_pic> | 그림 안의 텍스트도 받아 적음 |
프롬프트가 비어 있으면 모델이 같은 출력을 반복하므로 두 프런트엔드 모두 기본 프롬프트로 바꿔 넣습니다(CLI는 경고를 출력합니다). --repetition-penalty와 요청의 repetition_penalty는 시드를 포함한 디코더 시퀀스 전체에 transformers 방식으로 적용됩니다. 모델 카드의 스크립트는 1.1을 쓰지만 mlxcel 기본값은 1.0입니다.
HTTP로는 태스크 프롬프트를 텍스트 파트로, image_url 파트 하나를 함께 담은 user 메시지 하나로 POST /v1/chat/completions를 보냅니다. 서버는 태스크 프롬프트를 제한하고(1024바이트, 제어 문자 불가, 시드 토큰 최대 256개), 이미지는 정확히 한 장만 받으며, 오디오와 비디오는 거부하고, 스트리밍 요청에는 delta 하나로 응답합니다. 배치 크기 2 이상, 모델 카드의 표 삽입·반복 중단 logits processor는 지원하지 않습니다.
비전 인코더¶
mlxcel은 여러 비전 인코더 아키텍처를 구현합니다. 올바른 인코더는 모델 설정을 기반으로 자동 선택됩니다.
SigLIP
사용 모델: PaliGemma, Aya Vision, Phi 4 SigLIP, 일부 Gemma 3 변형. SigLIP(Sigmoid Loss for Language-Image Pre-training)은 CLIP에서 사용되는 대조 손실 대신 이진 교차 엔트로피 손실로 학습된 ViT입니다. 이 인코더는 고정 해상도에서 패치 임베딩을 생성합니다.
Llama 4 Vision Encoder
사용 모델: Llama 4 모델. Llama 4의 멀티모달 아키텍처를 위해 설계된 네이티브 비전 트랜스포머.
Qwen2-VL ViT with MRoPE
사용 모델: Qwen2-VL, Qwen2.5-VL. 이 인코더는 표준 ViT 위치 인코딩을 Multimodal Rotary Position Embedding(MRoPE)으로 확장하여, 이미지 내 공간적 위치와 다중 대화 내 이미지의 시간적 위치를 모두 인코딩합니다. 덕분에 모델이 이미지 위치를 더 정밀하게 추론할 수 있습니다.
Pixtral 2D RoPE ViT
사용 모델: Pixtral, Mistral 3. 이미지 그리드의 2차원 구조를 보존하는 2D RoPE 위치 인코딩을 사용하는 Vision Transformer로, 모델이 패치 간의 공간적 관계를 추론할 수 있게 합니다.
CLIP 변형
사용 모델: Phi 3 Vision 및 관련 모델. 대조적 사전 학습이 적용된 표준 CLIP ViT 인코더.
Moondream3 Encoder
커스텀 이미지 전처리를 포함하는 Moondream3 아키텍처 전용 인코더.
Molmo2 Encoder
Molmo2 아키텍처 전용 인코더.
MiniCPM-o Encoder
MiniCPM-o 아키텍처 전용 인코더.
비전-텍스트 커넥터¶
비전 인코더가 패치 임베딩을 생성한 후, 커넥터가 이를 텍스트 모델의 은닉 공간으로 매핑합니다. 커넥터는 항상 비전 인코더와 언어 모델에 비해 소규모 네트워크입니다.
| 커넥터 | 사용 모델 | 설명 |
|---|---|---|
| MLP | 대부분의 LLaVA 패밀리 모델, Qwen-VL | GELU 활성화를 사용하는 2층 MLP |
| Linear | 단순 변형 | 단일 선형 프로젝션 |
| Identity | 패스스루 케이스 | 변환 없음; 인코더와 LLM의 은닉 크기가 이미 일치하는 경우 사용 |
| Mistral3 Spatial Merger | Mistral 3 | 이미지 토큰의 시퀀스 길이를 줄이는 공간 풀링 후 MLP |
| Aya Vision | Aya Vision | Aya Vision 패밀리의 커스텀 커넥터 |
CLI에서 VLM 사용¶
mlxcel generate 명령어는 --image를 통해 하나 이상의 이미지 경로를 받습니다. 모델이 비전 입력을 지원해야 하며 텍스트 전용 모델에 이미지를 전달하면 이미지가 무시됩니다.
단일 이미지:
mlxcel generate \
--model ./models/Qwen2.5-VL-7B-Instruct \
--image ./photo.jpg \
--prompt "What is in this image?" \
--max-tokens 200
여러 이미지:
mlxcel generate \
--model ./models/Qwen2.5-VL-7B-Instruct \
--image ./before.png --image ./after.png \
--prompt "What are the differences between these two screenshots?" \
--max-tokens 300
PaliGemma를 이용한 문서 이해 예시:
mlxcel generate \
--model ./models/PaliGemma-3B \
--image ./invoice.png \
--prompt "Extract the total amount from this invoice." \
--max-tokens 50
채팅 템플릿 고려 사항:
대부분의 VLM에서 채팅 템플릿은 프롬프트 텍스트에 상대적인 올바른 위치에 이미지 플레이스홀더 토큰을 자동으로 삽입합니다. 다중 대화에서 이미지 배치를 정밀하게 제어해야 하는 경우, --no-chat-template을 사용하고 모델의 예상 템플릿 형식에 맞게 프롬프트를 직접 형식화하십시오.
API에서 VLM 사용¶
/v1/chat/completions 엔드포인트를 사용할 때, 이미지는 OpenAI 멀티모달 메시지 형식을 사용하여 메시지 콘텐츠의 일부로 전달됩니다. 이미지는 URL, 로컬 경로, 또는 Base64 인코딩된 데이터 URI로 제공할 수 있습니다.
URL에서 이미지 전달:
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen2.5-VL-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": "https://example.com/photo.jpg"}
},
{
"type": "text",
"text": "Describe this image in detail."
}
]
}
],
"max_tokens": 300
}'
Base64 데이터 URI에서 이미지 전달:
IMAGE_B64=$(base64 -i ./photo.jpg)
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{
\"model\": \"Qwen2.5-VL-7B-Instruct\",
\"messages\": [
{
\"role\": \"user\",
\"content\": [
{
\"type\": \"image_url\",
\"image_url\": {\"url\": \"data:image/jpeg;base64,${IMAGE_B64}\"}
},
{
\"type\": \"text\",
\"text\": \"What text appears in this image?\"
}
]
}
],
\"max_tokens\": 200
}"
OpenAI SDK를 이용한 Python 예시:
import base64
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="unused")
with open("./diagram.png", "rb") as f:
image_data = base64.b64encode(f.read()).decode()
response = client.chat.completions.create(
model="Qwen2.5-VL-7B-Instruct",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_data}"},
},
{
"type": "text",
"text": "Explain what this architecture diagram shows.",
},
],
}
],
max_tokens=400,
)
print(response.choices[0].message.content)
비디오 및 오디오 입력¶
mlxcel generate는 같은 프롬프트 표면에서 비디오와 오디오 미디어도 받을 수 있습니다:
mlxcel generate \
--model ./models/gemma-4-vl \
--video ./clip.mp4 \
--fps 2.0 \
--prompt "Describe the clip." \
--max-tokens 200
mlxcel generate \
--model ./models/gemma-4-audio \
--audio ./question.wav \
--prompt "Transcribe and answer the question." \
--max-tokens 200
프레임 추출에는 PATH에 있는 ffmpeg가 필요하며 --fps는 목표 샘플링 속도를 제어하고 기본값은 2.0입니다. 고유 비디오 경로가 있는 계열은 자체 시간 축 처리기로 클립을 소비합니다. Gemma 4 VL, Gemma 4 Unified, Inkling, Kimi-VL / Kimi-VL 2.5, Qwen-VL 계열(Qwen2-VL, Qwen2.5-VL, Qwen3-VL, Qwen3-VL MoE, Qwen3.5-VL과 그 MoE 변형, Qwen3.8-Flash-Next)이 여기에 해당합니다. 비전 타워가 있는 그 밖의 체크포인트는 아래의 프레임 대체 경로를 사용합니다.
서버는 채팅 메시지 안의 비디오와 오디오 콘텐츠 파트를 받습니다:
{
"role": "user",
"content": [
{"type": "video_url", "video_url": {"url": "file:///srv/videos/demo.mp4", "fps": 2.0}},
{"type": "text", "text": "Summarize the visible events."}
]
}
로컬 video_url 경로를 사용하려면 서버가 읽을 수 있는 디렉터리 목록을 MLXCEL_VIDEO_DIR_ALLOWLIST에 쉼표로 구분해 지정해야 합니다. 기본값은 fail-closed입니다. 즉, allowlist에 포함된 디렉터리 아래가 아니면 로컬 비디오 경로를 거부합니다. 고유 비디오 계열과 프레임 대체 경로 모두 서버 시작 시(라우터가 모델을 로드할 때마다) 이 목록을 한 번 해석하고 요청 처리 중에는 다시 읽지 않으므로, 변수를 바꾸거나 목록에 있는 디렉터리를 새로 만든 뒤에는 서버를 재시작하십시오. Data-video URI와 HTTP(S) URL은 ffmpeg가 읽기 전에 권한 0700인 전용 디렉터리 안에 권한 0600인 새 파일로 기록됩니다. 로드한 체크포인트가 비디오를 받으면 서버는 시작 시 ffmpeg와 ffprobe를 한 번 확인하고, 둘 중 하나가 없으면 경고를 남기며 그 결과를 프로세스가 끝날 때까지 유지합니다. 따라서 서버를 시작하기 전에 ffmpeg를 설치하십시오.
오디오 콘텐츠는 OpenAI input_audio 형태를 사용합니다. 현재 연결된 오디오 경로는 WAV 페이로드를 기대합니다:
한 프롬프트의 비디오와 오디오는 Gemma 4 Unified만 받습니다. CLI에서는 --video와 --audio를 함께, 서버에서는 video_url 블록과 input_audio 블록을 함께 보냅니다. 프레임 묶음과 오디오 묶음이 백본에 함께 들어가며, 요청당 오디오 클립 한 개 제한은 그대로입니다. 다른 계열은 이 조합을 거부하며 서버는 Combined video and audio inputs are not supported와 함께 400 오류를 반환합니다. 이미지+비디오, 이미지+오디오 요청은 로드된 모델이 해당 경로를 구현한 경우 지원됩니다.
고유 비디오 경로가 없는 모델의 비디오¶
비전 타워는 있지만 고유 비디오 경로가 없는 체크포인트는 클립을 순서가 있는 정지 이미지로 받습니다. 클립을 요청한 fps(video_url.fps, 없으면 서버의 --video-fps 또는 CLI의 --fps)로 디코딩하고, 첫 프레임과 마지막 프레임을 항상 남기며 최대 --video-max-frames개(기본값 16, 최소 2)로 고르게 줄인 뒤 PNG로 인코딩해 그만큼의 이미지로 시간 순서대로 보냅니다. 프레임 앞에는 Here is a video as a sequence of N frames in chronological order. 문장이 붙어 모델이 이를 한 클립으로 읽습니다. 클립이 여러 개면 각 클립 프레임 앞에 문장이 하나씩 붙습니다. CLI는 --video 클립을 --image 입력 뒤, 질문 앞에 배치하므로 같은 클립을 질문 앞에 나열한 서버 요청과 같은 프롬프트를 만듭니다.
이미지 파이프라인의 나머지는 그대로입니다. 템플릿은 프레임마다 이미지 자리 표시자를 하나씩 만들고, 프롬프트 캐시의 멀티모달 다이제스트는 프레임 바이트를 해시하므로 같은 fps와 프레임 상한으로 같은 클립을 보낸 두 요청은 접두부를 공유합니다. CLI에서는 실행별 비공개 임시 디렉터리에 프레임을 쓰고 실행이 끝나면 지웁니다.
대체 경로의 비용과 한계는 다음과 같습니다.
- 프롬프트는 모델의 이미지당 토큰 비용에 남긴 프레임 수를 곱한 만큼 늘어납니다. 이미지당 256토큰이면 기본값 16프레임이 약 4,096 프롬프트 토큰을 더합니다.
- 프레임은 요청당 이미지 예산(
--max-images, 기본값 16)에 포함됩니다. 예산을 넘는 요청은 디코딩 전에 거부하며, 거부 메시지는 프레임 수, 호출자가 직접 보낸 이미지 수, 한도를 알려 줍니다. - 시간 인코딩이 없습니다. 모델은 순서가 있는 그림 묶음과 그것이 한 비디오라는 문장만 보며, 타임스탬프나 프레임 사이의 움직임은 받지 않습니다.
Muse Glimmer는 두 진입점 모두에서 --video와 video_url을 거부합니다. 비전 타워가 없는 체크포인트도 비디오를 거부합니다. GET /props는 고유 경로 계열과 대체 경로 계열 모두 video: true를 보고하며, 서버 로그의 model <id> has no native video path; sending K of N sampled frames from <file> as ordered images 줄이 대체 경로를 사용했음을 알려 줍니다.
Vision Feature Cache¶
같은 이미지를 다시 보는 멀티턴 VLM 대화는 기본적으로 매 턴마다 vision tower 비용을 다시 지불합니다. 이미지는 전처리, vision encoder 통과, multimodal connector projection을 거친 뒤에야 언어 모델 입력으로 사용됩니다. SigLIP, Qwen의 ViT 같은 인코더에서는 이 과정이 prefill 외 비용 중 가장 큰 비중을 차지하며 이미지가 바뀌지 않았더라도 매 다음 턴마다 동일하게 반복됩니다.
mlxcel-server는 이 비용을 없애기 위해 로드된 VLM별로 projection 이후의 이미지 피처를 LRU로 유지합니다. 캐시 히트 시 vision tower와 multimodal embedder를 모두 건너뛰고 캐시된 항목 자체가 원래 언어 모델이 받을 그 텐서이기 때문에 근사가 아닌 비트 단위로 동일한 재사용이 이뤄집니다.
지원 모델¶
현재 feature cache가 연결된 계열은 다음과 같습니다.
- Gemma 4 VLM (
SingleArrayFeatures기반 단일 배열 피처) - Qwen 2.5-VL (단일 배열 피처)
- Qwen 3-VL (다단계 vision pyramid을 담는
DeepStackFeatures) - Cohere Compass (deep-stack 피처)
- LFM2-VL (이미지별로 캐시하는 단일 배열 피처)
다른 VLM 계열(Qwen 2-VL, Qwen 3.5-VL, Qwen 3-VL-MoE, Molmo·Molmo-Point, Phi 3 Vision, Phi 4 MM, MiniCPM-O, MiniCPM-V 4.6, Moondream 3, Gemma 3 VLM, LLaVA, Llama 4 VLM, Pixtral)은 trait 기본 구현을 통해 자동으로 비캐시 경로로 폴백합니다. 다른 계열을 추가하려면 해당 vision feature shape에 대해 CloneableFeatures를 구현하고 get_input_embeddings에 _with_cache 변형을 연결하면 됩니다. 요청 수준의 변경은 필요하지 않습니다.
캐시 키¶
키는 요청 경계에서 이미지를 구분하는 정보로부터 만들어집니다.
- 파일시스템 경로: 요청이 로컬 파일이나
file://URL을 지정한 경우. 경로는 해시 전에 정규화되어,./img.png와/abs/path/img.png처럼 같은 파일로 resolve되는 두 표현이 중복 엔트리가 되지 않도록 합니다. - 인라인 페이로드의 SHA-256 다이제스트: 이미지가 Base64 data URI나
http(s)://다운로드로 들어온 경우. 해시는 원본 압축 바이트에 대해 계산되며(image_hash_from_bytes), 디코드된 픽셀 텐서를 해시하지 않도록 설계되어 있습니다.
경로 키와 해시 키는 같은 캐시 안에서 서로 분리된 네임스페이스를 가집니다. 따라서 /abs/img.png를 디스크에서 읽은 요청과 동일 바이트를 인라인 업로드한 다음 요청은 서로 다른 엔트리를 갖습니다. 이는 의도된 설계입니다. 두 전달 방식 사이에서 같은 이미지를 식별하려면 경로 기반 요청마다 디스크를 읽어 해시해야 하며 그러면 캐시의 목적 자체가 훼손되기 때문입니다.
--vision-cache-size 조정¶
| 값 | 동작 |
|---|---|
0 | 캐시 비활성화. 매 턴 vision 파이프라인 전체를 실행합니다. 캐시 경로의 정확성을 비캐시 경로와 대조해 검증할 때나 메모리 부담이 지배적인 환경에서 사용합니다. |
1..N | 로드된 VLM당 LRU가 최대 N개 엔트리를 유지합니다. 기본값 20. |
크기 산정 기준은 워크로드 성격에 따라 다릅니다. 세션당 5장 정도의 서로 다른 이미지를 쓰는 채팅 워크로드가 여러 개 동시에 돌아간다면 max_batch_size * session당 평균 이미지 수에 약간의 여유를 더한 값이 적절합니다. 기본값 20은 각기 5장을 가진 채팅 세션 4개가 동시에 돌아가는 정도에 대응됩니다. 같은 invoice나 스크린샷이 여러 턴에 걸쳐 다시 등장하는 문서 이해 워크로드라면 서로 다른 원본 문서 수와 같은 크기면 보통 충분하고 대략 100을 넘는 값은 히트율이 그 전에 이미 plateau에 이르러 이득이 거의 없습니다. 엔트리당 메모리 footprint는 입력 해상도가 아니라 vision encoder의 출력 차원과 이미지 토큰 수에 비례합니다. image_size=448의 Qwen 2.5-VL에서 엔트리 하나는 수백 KB 수준이고 Qwen 3-VL의 deep-stack 피처는 이미지당 수 MB 수준입니다.
캐시 정확성 검증¶
같은 프롬프트를 캐시 사용·미사용으로 두 번 실행했을 때 출력이 정확히 동일해야 합니다.
# 캐시 비활성화
mlxcel-server --model ./models/Qwen2.5-VL-7B-Instruct --vision-cache-size 0
# 캐시 활성화 (기본값 20)
mlxcel-server --model ./models/Qwen2.5-VL-7B-Instruct
두 서버에 같은 이미지를 쓰는 2턴 대화를 동일하게 보내 보면, temperature=0에서 생성 텍스트가 완전히 일치해야 합니다. qwen2.5-vl-3b-4bit가 기준 smoke test 대상이고 불일치가 나타난다면 캐시 사이즈 문제가 아니라 해당 계열의 _with_cache 연결 경로에 회귀가 생긴 신호입니다.
캐시 범위와 수명¶
캐시는 로드된 VLM당 Rc<ModelVisionCaches> 형태로 BatchScheduler가 소유하므로 수명이 로드된 모델에 종속됩니다. 모델을 다시 로드하거나(--model 경로 변경, 서버 재시작) 하면 빈 캐시로 시작하고 프로세스 간 공유나 디스크 영속화는 없습니다. 빌드 아티팩트 저장소가 아니라 실행 중인 서버의 요청 경로 가속기라는 점이 설계 원칙입니다.
레거시 no-batch 워커(--no-batch)는 기본 캐시 크기를 유지하되 --vision-cache-size를 노출하지 않으므로, 캐시 크기를 조정해야 한다면 기본 배치 워커를 계속 사용하십시오.