콘텐츠로 이동

서버 모드

목차


서버 모드란?

서버 모드에서는 mlxcel이 상시 실행 HTTP 서비스로 동작합니다. 각 요청마다 모델을 로드하고 종료하는 mlxcel generate와 달리, 서버는 모델을 한 번만 로드하여 메모리에 상주시키고 서비스 수명 동안 무제한의 추론 요청을 처리합니다.

이 설계는 다음과 같은 경우에 적합합니다:

  • 여러 클라이언트가 동시에 동일한 모델에 접근해야 할 때.
  • 요청 지연 시간이 중요하고 대형 모델의 모델 로드 시간(수 초가 걸릴 수 있음)이 요청별로 허용되지 않을 때.
  • OpenAI API 또는 llama.cpp 서버의 드롭인 대체가 필요할 때.

서버는 OpenAI 호환 생성, Responses, Audio, Embeddings, Anthropic Messages, Jina/Cohere/TEI 호환 재순위화, llama-server 고유 완성·임베딩·토큰화·관측성 경로를 제공합니다. 이러한 계약을 사용하는 클라이언트는 보통 기본 URL만 바꾸면 되지만 고유 임베딩·완성 경로와 OpenAI 경로는 의도적으로 응답 모양이 다릅니다.


서버 시작

서버를 시작할 수 있는 두 가지 바이너리가 있습니다:

바이너리 진입점 사용 사례
mlxcel serve mlxcel CLI 하위 명령어 일반적인 사용
mlxcel-server 독립 실행형 바이너리 서버 전용 실행 파일이 필요한 배포 환경

두 바이너리 모두 src/server/startup.rs의 동일한 시작 로직을 공유합니다. 대부분의 런타임 플래그는 공유되지만 llama.cpp 호환성을 위해 일부 문법은 의도적으로 다릅니다. mlxcel serve는 --n-parallel과 --draft-model을 primary로 표시하고 standalone mlxcel-server는 --parallel과 --model-draft를 primary로 표시합니다. 대응하는 다른 spelling도 alias로 허용됩니다. 자세한 내용은 투기적 디코딩과 CLI 명령어 - mlxcel serve를 참조하십시오.

최소 실행:

# bare 이름은 mlx-community/<name>으로 해석되고 첫 사용 시 자동 다운로드됩니다.
mlxcel serve --model Qwen2.5-7B-Instruct-4bit

독립 실행형 서버 바이너리:

mlxcel-server --model Qwen2.5-7B-Instruct-4bit --port 8080

모델 인자는 기존 로컬 경로, HuggingFace owner/name repo-id, 또는 ${MLXCEL_DEFAULT_ORG:-mlx-community}를 통해 해석되는 bare 이름을 받습니다. repo-id가 캐시에 없으면 mlxcel 전역 store에 다운로드됩니다. serve와 mlxcel-server에서 해당 store를 재배치하려면 --models-dir <PATH> 또는 MLXCEL_MODELS_DIR를 사용하십시오.

서버 기본값은 병렬 요청 슬롯 4개(--parallel 4 / --n-parallel 4)와 automatic decode storage입니다. Batch를 지원하지 않는 모델군은 슬롯 1개로 clamp됩니다. Prompt-prefix cache와 APC는 기본 활성화됩니다. 통제된 비교를 위해 끄려면 --no-prompt-cache 또는 --apc-enabled=false를 사용하십시오.

DFlash speculative decoding 서버:

mlxcel-server \
  --model ./models/qwen3.5-27b-4bit \
  --model-draft ./models/Qwen3.5-27B-DFlash \
  --draft-kind dflash \
  --draft-block-size 16 \
  --port 8080

mlxcel serve에서는 같은 drafter 경로의 primary spelling이 --draft-model이며 --model-draft도 허용됩니다.

mlxcel serve \
  --model ./models/qwen3.5-27b-4bit \
  --draft-model ./models/Qwen3.5-27B-DFlash \
  --draft-kind dflash \
  --draft-block-size 16 \
  --port 8080

Full-model drafter(target과 토크나이저가 같은 소형 체크포인트):

mlxcel serve \
  --model ./models/Qwen3-8B-4bit \
  --draft-model ./models/Qwen3-0.6B-4bit \
  --draft-kind full-model \
  --port 8080

워커는 시작할 때 target 옆에 drafter를 로드합니다. 대상 요청은 각자의 draft 캐시에서 초안을 만들고, 활성 배치가 --prompt-lookup-max-batch(기본 2)행 이하인 동안 디코드 틱마다 검증 라운드를 한 번 실행합니다.

프롬프트 조회 투기적 디코딩(초안 체크포인트 없음):

mlxcel serve \
  --model ./models/Qwen3-1.7B-4bit \
  --draft-kind prompt-lookup \
  --port 8080

프롬프트 조회는 컨텍스트 앞부분에서 베낀 토큰을 제안하고 배치 스케줄러 안에서 검증합니다. 명시적으로 켜야 동작하고 수정형 요청에서 효과가 있으며, 활성 배치의 행 수가 --prompt-lookup-max-batch(기본 2) 이하일 때만 검증 라운드를 실행합니다. 적용 조건과 측정 결과는 투기적 디코딩을 참조하십시오. temperature 0에서 프롬프트 조회와 full-model 라운드는 plain decode 토큰 스트림에서 벗어날 수 있어, 투기적 디코딩을 쓰지 않은 실행과 출력이 같다고 보장하지 않습니다(#2778).

모든 인터페이스 바인딩(Docker 또는 원격 접근용):

mlxcel serve \
  --model ./models/Llama-3.1-8B-Instruct \
  --host 0.0.0.0 \
  --port 8080

Unix 소켓 모드:

--host 값이 .sock으로 끝나면 그 경로의 Unix 도메인 소켓으로 서빙합니다. 이때 --port는 무시합니다:

mlxcel serve \
  --model ./models/Gemma-3-4B-Instruct \
  --host /var/run/mlxcel.sock

Unix 소켓 모드는 서버와 클라이언트가 동일한 호스트에서 실행되며 TCP 오버헤드와 개방된 네트워크 포트를 피하고자 할 때 유용합니다.

--port 0은 TCP 설정입니다. 커널이 고르는 임시 포트에 바인딩합니다.

번들 WebUI:

--webui는 같은 리스너의 /webui/에서 관리용 브라우저 UI를 제공합니다. 모델 인자가 없으면 서버는 모델 없이 시작하고 UI에서 모델을 로드합니다.

mlxcel-server --webui --models-dir ./models/mlx --port 8080

--api-key 없이 루프백 리스너로 시작하면 서버가 일회용 세션 키를 터미널에 출력합니다. 루프백이 아닌 리스너에는 명시적 API 키와 TLS가 필요하고, Unix 소켓 리스너는 거부합니다. WebUI를 참조하십시오.


인증

기본적으로 서버는 인증 없이 요청을 수락합니다. 이는 로컬 개발에 적합하지만 API 키를 설정하지 않고 신뢰할 수 없는 네트워크에 노출해서는 안 됩니다.

API 키가 설정되면 /health와 /를 제외한 모든 엔드포인트에서 Authorization 헤더에 Bearer 토큰으로 키를 전달해야 합니다.

인증은 media sandbox가 아님

이미지, 오디오, 비디오 요청은 서버 자신의 fetch를 조종하므로 API 키는 호출자를 인증할 뿐 그 fetch가 어디로 가는지는 제한하지 않습니다. 두 가지 정책이 범위를 좁히며, 둘 다 hostname allowlist도 HTTPS 전용 스위치도 아닙니다. 원격 URL은 공인 주소가 아닌 곳으로 resolve되면 거부하고, 로컬 파일은 --media-path 루트 아래에서만 읽습니다. 미디어 입력을 참조하십시오. 신뢰 client와 함께 reverse proxy, egress firewall, container/OS filesystem control은 그대로 유지하십시오. Local video path는 별도로 MLXCEL_VIDEO_DIR_ALLOWLIST가 제한합니다.

미디어 입력

--media-path <dir> 없이 시작한 서버는 file:// 미디어 URL을 그대로 거부합니다. 루트를 설정하면 요청의 경로를 먼저 그 루트 기준 상대 경로로 resolve하고, 절대 경로는 루트 안의 파일을 가리킬 때만 받습니다. --media-path /srv/media에서 file://cat.png, file:///srv/media/cat.png, 맨 이름 cat.png는 모두 /srv/media/cat.png를 읽고, file:///etc/passwd는 이탈로 거부합니다. resolve한 경로는 canonicalize한 뒤 canonical 루트 안에 남아야 하므로, 루트 밖을 가리키는 symlink는 거부하고 안에 머무는 symlink는 그대로 동작합니다. 열 때 O_NOFOLLOW를 쓰고 일반 파일만 받으므로, 마지막 경로 요소를 symlink로 바꿔치기하는 것도 루트 아래 FIFO로 요청 태스크를 붙잡아 두는 것도 통하지 않습니다. .., 제어 문자, : * ? " < > |, 앞뒤 공백, 끝의 ., 255바이트 초과, 유니코드 구분자 유사 문자, .·/·\·NUL의 퍼센트 이스케이프를 담은 경로는 그 앞에서 먼저 거부합니다.

http(s) 미디어 URL은 공인 주소가 아닌지를 세 지점에서 검사합니다. 요청 전(스킴, URL에 자격 증명이 없는지, 호스트명이 resolve되는 모든 주소), 최대 5홉인 리다이렉트 체인의 매 홉, 그리고 본문 한 바이트를 읽기 전에 실제로 닿은 peer 주소를 다시 한 번 봅니다. 마지막 검사가 DNS rebinding 창을 닫습니다. loopback, unspecified 주소, 멀티캐스트와 브로드캐스트, IPv4 사설 및 link-local 대역(클라우드 메타데이터 서비스 169.254.169.254가 여기서 막힙니다), carrier-grade NAT, 프로토콜 할당·벤치마킹·문서화 프리픽스, 240.0.0.0/4, IPv6 unique-local·link-local·문서화 대역, 그리고 거부 대상 IPv4를 IPv4-mapped나 IPv4-compatible IPv6로 적은 모든 표기를 거부합니다. 내부 오브젝트 스토어에서 미디어를 서비스하는 배포는 MLXCEL_ALLOW_PRIVATE_MEDIA_URLS=1을 설정해 주소 검사를 전부 통과로 바꿉니다.

두 정책 어느 쪽도 페이로드 한도를 바꾸지 않습니다. --max-image-payload-size, --max-images, 디코더의 폭·높이·할당 상한, 오디오의 클립당 및 요청당 상한, 10초 fetch 타임아웃이 모두 그대로 적용됩니다.

비전 타워는 있지만 고유 비디오 경로가 없는 체크포인트에 보낸 video_url 블록은 디코딩되어 순서가 있는 정지 이미지로 모델에 전달됩니다. --video-max-frames(기본값 16, MLXCEL_VIDEO_MAX_FRAMES)는 남길 프레임 수를, --video-fps(기본값 2.0, MLXCEL_VIDEO_FPS)는 블록에 fps가 없을 때의 샘플링 속도를 정합니다. 프레임은 --max-images에 포함됩니다. 고유 비디오 경로가 없는 모델의 비디오를 참조하십시오.

인라인으로 키 설정:

mlxcel serve \
  --model ./models/Qwen2.5-7B-Instruct \
  --api-key "sk-my-secret-key"

파일에서 키 읽기:

echo "sk-my-secret-key" > /etc/mlxcel/api-key
mlxcel serve \
  --model ./models/Qwen2.5-7B-Instruct \
  --api-key-file /etc/mlxcel/api-key

인증된 요청 전송:

curl http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer sk-my-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Hello"}]}'

키가 없거나 올바르지 않으면 서버는 HTTP 401과 JSON 오류 본문을 반환합니다:

{"error": {"message": "Invalid API key", "type": "invalid_api_key"}}

LLAMA_API_KEY 환경 변수는 --api-key 플래그의 대안으로, 컨테이너화된 배포 환경에서 유용합니다.


요청 본문 오류

/v1/chat/completions, /v1/responses, /v1/messages, /v1/messages/count_tokens와 /v1 없는 별칭은 파싱할 수 없는 본문에 /v1/completions와 같은 방식으로 답합니다. 파서가 낸 메시지를 1 KiB로 자른 JSON invalid_request_error를 돌려주며, 이 처리는 가용성 확인과 디스패치, 스트리밍 준비보다 먼저 일어나고 인증은 여전히 그 앞에 있습니다. JSON이 깨졌거나 비어 있거나 model처럼 필드가 없거나 타입이 틀리면 HTTP 400을, Content-Type이 없거나 JSON이 아니면 HTTP 415를 반환합니다. Anthropic 라우트는 자체 봉투인 {"type": "error", "error": {...}}를 쓰고, /v1/messages/count_tokens는 /v1/messages와 똑같이 답합니다. 분산 HTTP 라우터의 /v1/chat/completions도 같으며 본문이 크기 한도를 넘으면 HTTP 413을 포함합니다. 단일 노드 서버에서는 서버 전역 레이어 하나가 모든 HTTP 413을 OpenAI 봉투로 답하므로 Anthropic 라우트에서도 그렇습니다.

Anthropic Messages

Claude Code는 system 프롬프트를 x-anthropic-billing-header 스탬프로 시작하는데, 이 스탬프의 cch 값은 요청마다, cc_version의 네 번째 부분은 세션마다 바뀝니다. Anthropic Messages 라우트는 렌더링 전에 이 두 값을 같은 길이의 f 문자로 바꾸므로, 턴이 스탬프 위치에서 끊기지 않고 직전 프롬프트 전체를 재사용합니다. 정확히 그 헤더로 시작하는 최상위 system 텍스트만 대상입니다. MLXCEL_ANTHROPIC_PIN_BILLING_STAMP=0을 지정하면 클라이언트 텍스트를 그대로 넘깁니다. 이 변수는 기동 시 OnceLock으로 한 번만 읽으므로 이후에 바꿔도 반영되지 않습니다.


엔드포인트 개요

서버는 다음 HTTP 엔드포인트를 제공합니다:

메서드 경로 설명 인증 필요
POST /v1/chat/completions OpenAI 채팅 완성 예(키 설정 시)
POST /v1/completions OpenAI 텍스트 완성 예(키 설정 시)
POST /v1/embeddings OpenAI 임베딩 예(키 설정 시)
POST /v1/rerank, /v1/reranking Jina/Cohere 또는 TEI 재순위화 예(키 설정 시)
POST /v1/messages Anthropic Messages API 생성 예(키 설정 시)
POST /v1/messages/count_tokens Anthropic Messages API 토큰 수 계산 예(키 설정 시)
GET /v1/models 로드된 모델 목록 예(키 설정 시)
POST /v1/responses OpenAI Responses API 응답 생성 예(키 설정 시)
GET /v1/responses/:id 저장된 Responses API 응답 조회 예(키 설정 시)
DELETE /v1/responses/:id 저장된 Responses API 응답 삭제 예(키 설정 시)
POST /v1/responses/:id/cancel Responses API 응답 취소 또는 cancelled 상태 표시 예(키 설정 시)
POST /v1/responses/compact, /responses/compact 오래된 Responses 턴을 compaction 출력 항목 하나로 요약. REST API 참조 예(키 설정 시)
POST /v1/audio/speech Kokoro 음성 합성, WAV 응답 예(키 설정 시)
POST /v1/audio/transcriptions Whisper 전사 예(키 설정 시)
POST /v1/audio/translations Whisper 영어 번역 예(키 설정 시)
POST /v1/images/generations 이미지 생성. --image-model이 체크포인트를 로드할 때만 마운트됩니다. 이미지 생성 참조 예(키 설정 시)
POST /v1/images/edits 참조 이미지를 사용한 이미지 편집. 같은 워커와 마운트 규칙 적용 예(키 설정 시)
GET(WebSocket 업그레이드) /v1/realtime Nemotron VoiceChat 전이중 음성 세션. VoiceChat 체크포인트를 서빙할 때만 마운트됩니다. Nemotron VoiceChat Realtime API 참조 예(키 설정 시)
GET /v1/cache/stats 프롬프트 캐시 통계 예(키 설정 시)
POST /v1/cache/reset 프롬프트 캐시 저장소 초기화 예(키 설정 시)
POST /chat/completions /v1/chat/completions 별칭 예(키 설정 시)
POST /completions, /completion llama-server 고유 완성. /v1/completions의 별칭이 아님 예(키 설정 시)
POST /embedding, /embeddings llama-server 고유 임베딩. /v1/embeddings의 별칭이 아님 예(키 설정 시)
POST /rerank, /reranking /v1 경로와 같은 재순위화 처리기 예(키 설정 시)
POST /messages /v1/messages 별칭 예(키 설정 시)
POST /messages/count_tokens /v1/messages/count_tokens 별칭 예(키 설정 시)
GET /models /v1/models 별칭 예(키 설정 시)
POST /responses /v1/responses 별칭 예(키 설정 시)
GET /responses/:id /v1/responses/:id 별칭 예(키 설정 시)
DELETE /responses/:id /v1/responses/:id 별칭 예(키 설정 시)
POST /responses/:id/cancel /v1/responses/:id/cancel 별칭 예(키 설정 시)
POST /audio/speech, /audio/transcriptions, /audio/translations /v1 없는 오디오 별칭 예(키 설정 시)
POST /tokenize 텍스트 문자열 토큰화 예(키 설정 시)
POST /detokenize 토큰 목록 역토큰화 예(키 설정 시)
POST /apply-template 채팅 템플릿을 렌더링해 /v1/chat/completions가 실제로 사용할 프롬프트를 그대로 반환합니다 예(키 설정 시)
POST /infill llama-server fill-in-the-middle 보완 예(키 설정 시)
POST /v1/chat/completions/input_tokens, /chat/completions/input_tokens 생성 없이 채팅 요청의 입력 토큰 수만 반환 예(키 설정 시)
POST /v1/responses/input_tokens, /responses/input_tokens 생성 없이 Responses 요청의 입력 토큰 수만 반환 예(키 설정 시)
POST /v1/chat/completions/control 진행 중인 completion의 실시간 제어(llama-server b10621 호환) 예(키 설정 시)
GET /v1/stream conversation id와 오프셋으로 재개 가능한 스트림을 재생 예(키 설정 시)
DELETE /v1/stream 보관 중인 스트림 삭제 예(키 설정 시)
POST /v1/streams/lookup conversation id 목록에 대해 보관된 스트림 조회 예(키 설정 시)
GET /lora-adapters 로드된 LoRA 어댑터와 스케일 조회 예(키 설정 시)
POST /lora-adapters 서버 재시작 없이 LoRA 어댑터 스케일 설정 예(키 설정 시)
GET /v1/settings, /settings 타입이 정의된 설정 스키마, 현재 값, 안정적인 mutable fingerprint 예(키 설정 시)
PATCH /v1/settings, /settings 설정 merge 또는 replace를 검증 후 게시. 거부된 이름이 유효한 이름을 되돌리지 않습니다 예(키 설정 시)
GET /health, /v1/health 헬스 체크 아니오
GET / 헬스 체크(별칭) 아니오
GET /slots 슬롯별 상태(기본 활성, --no-slots로 비활성화) 예(키 설정 시)
GET /props 서버 속성(--props 필요) 예(키 설정 시)
GET /metrics Prometheus 메트릭(--metrics 필요) 예(키 설정 시)
POST /slots/:id_slot 슬롯별 동작: ?action=save, restore, erase 예(키 설정 시)
GET /v1/internal/mtp-policy B=1 MTP 판정의 버전된 읽기 인터페이스(settled, profiling, forced, unavailable, exactness_declined) 예(키 설정 시)
GET, POST /tools, /cors-proxy llama-server 호환을 위해 인식하지만 비활성 상태이며 feature_disabled 본문과 함께 403을 반환합니다 예(키 설정 시)
GET /webui/ 번들 WebUI 셸과 정적 자산(--webui 필요) 아니요
GET, POST /ui-api/v1/* WebUI 제어 평면: bootstrap, catalog, 모델 동작, 다운로드, 삭제, 작업, 이벤트, 런타임 관측(--webui 필요) 예(항상)

/slots 엔드포인트는 기본적으로 활성화되어 있으며 --no-slots로 비활성화할 수 있습니다. /props와 /metrics 엔드포인트는 기본적으로 비활성화되어 있고 각각 --props, --metrics로 활성화합니다. 프롬프트 캐시 엔드포인트는 항상 마운트되며 캐시가 꺼져 있으면 안정적인 disabled payload를 반환합니다.

전체 요청 및 응답 스키마는 API 참조와 임베딩 및 재순위화 API를 참고하십시오.


오디오 모델 서빙

Whisper와 Kokoro 체크포인트는 audio-only server slot을 채웁니다. Whisper process는 전사와 번역을, Kokoro process는 음성 합성을 제공합니다. 어느 process도 채팅 생성과 동시에 동작하지 않습니다.

# STT
mlxcel-server -m ./models/whisper-base \
  --audio-queue-depth 8 --audio-request-timeout-secs 120

# TTS
mlxcel-server -m ./models/kokoro-82m \
  --audio-queue-depth 8 --audio-request-timeout-secs 120

체크포인트 layout, 요청 schema, 지원 WAV 형식, voice pack, 제한, 오류 응답은 OpenAI 오디오 API를 참조하십시오.


Nemotron VoiceChat 서빙 (전이중 음성)

Nemotron VoiceChat 체크포인트(model_type: "nemotron_voicechat", 예: mlx-community/NemotronLabs-VoiceChat-11B-4bit)는 채팅 라우트가 아니라 /v1/realtime WebSocket으로 서빙합니다.

mlxcel-server -m models/NemotronLabs-VoiceChat-11B-4bit --port 8080

서버는 전용 realtime 엔진 스레드에 체크포인트를 로드하고 채팅 워커, 배치 스케줄러, 텍스트 warmup은 시작하지 않습니다. /v1/chat/completions, /v1/completions, /v1/responses, /v1/messages는 /v1/realtime을 안내하는 메시지와 함께 501 Not Implemented로 응답합니다.

  • --api-key는 업그레이드 요청에 적용되므로 Authorization: Bearer <key>를 보내야 하고, --api-prefix는 다른 라우트와 마찬가지로 경로 앞에 붙습니다.
  • --cors-origins와 --allowed-origins는 업그레이드 요청의 Origin 헤더를 검사합니다. 정책이 허용하지 않는 브라우저 origin은 403 Forbidden을 받고, Origin을 보내지 않는 클라이언트는 받아들입니다.
  • 엔진은 세션을 한 번에 하나만 받습니다. 세션이 열려 있는 동안 두 번째로 연결하면 server_busy 오류와 close code 1013을 받습니다.
  • 제한: WebSocket 메시지나 프레임 하나당 1 MiB, 세션당 오디오 600초, 연결 후 session.update를 보내기까지 30초, 클라이언트 메시지 없이 120초가 지나면 유휴 세션을 닫습니다.

같은 모델의 CLI 형태는 mlxcel generate --audio <wav> --output-audio <wav>입니다(CLI 레퍼런스 참조). 메시지 프로토콜은 Nemotron VoiceChat Realtime API를 참조하십시오.


임베딩 및 재순위화 모델 서빙

서버는 대화 모델 옆에 임베딩 모델과 재순위화 모델을 함께 실행할 수 있습니다. 각 전용 제공자는 별도 작업자 스레드에서 모델, 토크나이저, MLX 배열을 소유하므로 대화 작업자의 스트림을 공유하지 않습니다.

mlxcel-server \
  -m mlx-community/Qwen3-4B-4bit \
  --embedding-model Qwen/Qwen3-Embedding-0.6B \
  --reranker-model mlx-community/Qwen3-Reranker-0.6B-4bit \
  --port 8080

임베딩 체크포인트를 -m에 직접 지정하면 임베딩 전용 프로세스를 시작하고 레이블 하나짜리 ForSequenceClassification 체크포인트는 재순위화 전용 프로세스를 시작합니다. 생성형 재순위화 모델은 설정이 생성 모델과 비슷하므로 명시적으로 선택해야 합니다.

# 생성형 재순위화 전용. 같은 경로의 반복은 종류를 선택하며 모델을 두 번 불러오지 않습니다.
mlxcel-server \
  -m mlx-community/Qwen3-Reranker-0.6B-4bit \
  --reranker-model mlx-community/Qwen3-Reranker-0.6B-4bit

한 프로세스는 임베딩 제공자 하나와 재순위화 제공자 하나를 지원합니다. --embedding-batch-size의 기본값은 16입니다. --rerank-batch-size 0은 종류별 기본값인 텍스트 쌍 8개 또는 멀티모달 쌍 2개를 선택합니다. 각 작업자 큐는 --embedding-queue-depth로 제한하며 기본값은 8입니다. 두 요청 종류는 기본값 120초인 --embedding-request-timeout-secs를 사용합니다. 큐가 가득 차면 503, 응답 제한 시간이 지나면 504를 반환합니다.

--pooling과 --embd-normalize는 체크포인트의 밀집 벡터 계약을 덮어씁니다. 모델 문서나 호환 요구 사항이 있을 때만 사용하십시오. --embedding/--embeddings 및 --rerank/--reranking은 llama-server 호환 제한 모드를 선택하며 일치하는 제공자가 있어야 합니다.

정확한 전송 형식은 임베딩 및 재순위화 API, 모델 계열, 접두문 규칙, 최대 유사도 합산, 인덱스 설계, 평가는 임베딩과 재순위화를 참고하십시오.


이미지 모델 서빙

--image-model <checkpoint>(또는 MLXCEL_IMAGE_MODEL)는 채팅 모델 옆에 이미지 생성 모델을 로드합니다. --embedding-model이 임베딩 제공자를 로드하는 것과 같은 방식으로, 크기가 제한된 큐를 둔 전용 워커 스레드에서 실행합니다. 디퓨전 실행은 몇 초 동안 GPU를 점유하므로 요청은 한 번에 하나씩 처리합니다. 큐가 가득 차면 503, 요청별 타임아웃이면 504로 응답합니다. 로드 실패는 시작 시 치명적이므로, 이미지 경로를 요청한 서버가 그 경로에 501로 응답하는 일은 없습니다.

mlxcel-server -m mlx-community/Qwen3-4B-4bit --image-model ./models/flux2-klein

-m을 빼면 이미지 전용 서빙이 됩니다. 이미지 체크포인트가 서빙 모델이 되고, 모델 id는 --alias 첫 항목이나 체크포인트 디렉터리 이름입니다.

mlxcel-server --image-model ./models/flux2-klein --port 8080
mlxcel serve --image-model ./models/flux2-klein

/health는 체크포인트 로드가 끝나면 준비 상태를 보고하고, /v1/models는 그 id를 image 기능만 붙여 나열하며 completion은 붙이지 않습니다. /v1/images/* 경로는 평소대로 동작합니다. 채팅, 완성, 프롬프트 검사와 /tokenize, /detokenize, /apply-template은 임베딩 전용 모드와 같은 구조의 501을 반환하며 --image-model과 no -m을 함께 안내합니다. 이미지 전용 모드는 --models-dir, --models-preset, --webui, --embedding-model, --reranker-model, --embeddings, --reranking과 함께 쓸 수 없고, 함께 지정하면 두 플래그를 모두 지목하며 시작에 실패합니다.

경로, 체크포인트 패밀리, 요청 스키마는 이미지 생성을 참조하십시오.


서버 라이프사이클

서버는 시작부터 종료까지 정해진 단계를 거칩니다.

sequenceDiagram
    participant CLI
    participant Startup
    participant ModelLoader
    participant BatchScheduler
    participant HTTP

    CLI->>Startup: mlxcel serve --model ...
    Startup->>Startup: Initialize logging
    Startup->>Startup: Resolve API key
    Startup->>Startup: Resolve chat template
    Startup->>ModelLoader: Load model weights
    ModelLoader-->>Startup: Model ready
    Startup->>BatchScheduler: Start batch scheduler
    Note over Startup: Warmup pass (1 token)
    Startup->>HTTP: Bind TCP / Unix socket
    HTTP-->>CLI: Server listening on :8080

    loop For each request
        HTTP->>BatchScheduler: Enqueue request
        BatchScheduler->>ModelLoader: Prefill + decode
        ModelLoader-->>HTTP: Token stream
        HTTP-->>CLI: SSE / JSON response
    end

    CLI->>HTTP: SIGINT / SIGTERM
    HTTP-->>CLI: Graceful shutdown

단계 설명:

  1. 로깅 초기화 - --verbose와 --log-file 플래그에 따라 tracing subscriber를 구성합니다.
  2. API 키 확인 - --api-key, --api-key-file, 또는 LLAMA_API_KEY에서 키를 읽습니다.
  3. 채팅 템플릿 확인 - --chat-template, --chat-template-file, 또는 모델의 tokenizer_config.json에서 Jinja2 템플릿을 로드합니다.
  4. 모델 로딩 - Safetensor 가중치 파일을 읽고 텐서를 업로드한 뒤에 서버가 Model loaded를 보고하므로, 첫 요청에서 가중치 I/O가 발생하지 않습니다(MLXCEL_WEIGHT_MATERIALIZATION=lazy는 이 작업을 첫 사용 시점으로 미룹니다). 스케줄러는 모델별 디코드 경로를 로그에 남깁니다(예: decode path: native batched forward). --surgery가 설정된 경우 로딩 중에 가중치 surgery pipeline이 적용됩니다. --adapter가 설정된 경우 LoRA 가중치는 어댑터 경로를 통해 병합됩니다.
  5. 배치 스케줄러 시작 - 설정된 큐 깊이와 배치 크기 한도로 연속 배칭 스케줄러가 초기화됩니다.
  6. 워밍업 패스 - Metal 셰이더 컴파일을 트리거하기 위해 단일 1토큰 생성 패스가 실행됩니다. 첫 번째 실제 요청에서 컴파일 지연이 발생하지 않게 됩니다. --no-warmup으로 비활성화할 수 있습니다.
  7. 소켓 바인딩 - 서버가 TCP 주소 또는 Unix 소켓에 바인딩하고 연결 수락을 시작합니다.

서버에는 표준 신호 전파 이외의 명시적인 정상 종료 핸들러가 없습니다. 프로세스가 SIGINT 또는 SIGTERM을 수신할 때 처리 중인 활성 요청은 중단됩니다. 종료할 때 서버는 채팅 모델 워커와 모든 사이드 워커(오디오, 임베딩, 이미지, 리랭크, 리얼타임)를 main이 반환되기 전에 조인합니다. C 런타임의 정적 소멸자가 실행된 뒤 MLX 배열을 해제하는 스레드가 있으면 CPU 빌드가 pure virtual method called로 중단되기 때문입니다. 조인에는 120초 WORKER_EXIT_TIMEOUT이 걸리고 라우터의 풀 드레인에는 150초가 적용됩니다. 데드라인 만료 시점에 아직 로딩 중인 워커는 계속 실행되며, 서버는 정상 종료로 보고하지 않고 해당 워커가 아직 실행 중임을 로그에 남깁니다.


모니터링

헬스 체크

/health 엔드포인트는 서버 상태를 설명하는 JSON 본문을 반환합니다. 인증이 필요하지 않으므로 로드 밸런서 헬스 프로브에 적합합니다. 모델이 로드되어 있고 사용 가능한 용량이 있으면 HTTP 200을 반환하며 모델 로딩 중이거나 사용 가능한 슬롯이 없으면 HTTP 503을 반환합니다.

curl -s http://localhost:8080/health | jq .
{
  "status": "ok",
  "model": "Qwen2.5-7B-Instruct",
  "batch": {
    "active_sequences": 2,
    "queue_depth": 0,
    "max_batch_size": 4
  },
  "observability": {
    "sequences_started": 145,
    "sequences_completed": 143
  },
  "context_size": 0,
  "tool_call_parser": "mlxcel"
}

context_size는 슬롯별 실제 컨텍스트 윈도우입니다. 명시적 --ctx-size와 여러 활성 슬롯을 함께 사용하면 원래 전체 예산이 아니라 슬롯별로 나눈 값을 보고합니다.

Prometheus 메트릭

--metrics로 시작하면 서버가 Prometheus 텍스트 형식으로 /metrics 엔드포인트를 제공합니다:

mlxcel serve --model ./models/Qwen2.5-7B-Instruct --metrics
curl -s http://localhost:8080/metrics | grep mlxcel

메트릭에는 배치 점유율, 요청 큐 깊이, 생성된 토큰 수, 요청 완료 횟수가 포함됩니다.

mlxcel_requests_total과 mlxcel_completion_tokens_total은 채팅 완성 요청을 스트리밍 여부와 관계없이 같은 방식으로 셉니다. 스트리밍 채팅 완성은 생성이 끝날 때 usage 청크의 토큰 수로 한 번 기록되며, 요청이 그 청크를 요구했는지는 상관없습니다. 클라이언트가 취소한 스트림은 세지 않습니다. X-Conversation-Id 없이 보낸 스트림의 연결 끊김, DELETE /v1/stream, 같은 X-Conversation-Id의 새 요청에 의한 세션 교체가 여기에 해당합니다. 클라이언트 연결만 끊긴 재개 가능 스트림은 계속 생성하며 끝날 때 기록됩니다. 스트리밍 /v1/completions와 네이티브 /completion 요청은 이 카운터에 기록되지 않습니다.

슬롯 상태

기본 설정에서는 /slots 엔드포인트가 각 동시성 슬롯의 현재 상태와 /health와 같은 슬롯별 실제 context_size를 표시합니다. 필요하면 --no-slots로 비활성화할 수 있습니다:

curl -s http://localhost:8080/slots | jq .

연동 예시

curl

스트리밍 채팅 완성:

curl -s http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen2.5-7B-Instruct",
    "messages": [{"role": "user", "content": "Count from 1 to 5."}],
    "stream": true
  }'

비스트리밍 텍스트 완성:

curl -s http://localhost:8080/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen2.5-7B-Instruct",
    "prompt": "The capital of France is",
    "max_tokens": 5
  }'

Python requests

import requests

response = requests.post(
    "http://localhost:8080/v1/chat/completions",
    json={
        "model": "Qwen2.5-7B-Instruct",
        "messages": [{"role": "user", "content": "What is Rust?"}],
        "max_tokens": 200,
    },
)
print(response.json()["choices"][0]["message"]["content"])

OpenAI Python SDK

mlxcel의 /v1/chat/completions 엔드포인트는 OpenAI API와 스키마 호환됩니다. base_url을 mlxcel 서버로 지정하면 됩니다:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="sk-any-value",   # required by the client library; ignored if server has no key set
)

response = client.chat.completions.create(
    model="Qwen2.5-7B-Instruct",
    messages=[{"role": "user", "content": "Explain monads in simple terms."}],
    max_tokens=300,
)
print(response.choices[0].message.content)

OpenAI SDK를 이용한 스트리밍:

stream = client.chat.completions.create(
    model="Qwen2.5-7B-Instruct",
    messages=[{"role": "user", "content": "Write a haiku about Rust."}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

요청 우선순위

서버는 프리필 큐에 대해 high, normal(기본값), low 세 가지 우선순위 수준을 지원합니다. 큐 순서를 제어하려면 X-Priority 헤더를 설정하십시오:

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "X-Priority: high" \
  -d '{"model":"Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Urgent query"}]}'

높은 우선순위 요청은 프리필 큐에서 일반 및 낮은 우선순위 요청보다 앞에 위치합니다. 이는 일부 클라이언트(예: 인터랙티브 UI 요청)가 백그라운드 배치 작업보다 낮은 지연 시간이 필요한 경우에 유용합니다.

재현 가능한 탐욕적 출력 (cache_prompt)

프롬프트 캐시 히트는 나뉜 프리필입니다. 캐시된 prefix는 앞선 forward에서 계산했고 지금은 나머지만 프리필하므로 부동소수점 합산 순서가 달라지고, 그 결과 첫 생성 토큰의 logit이 콜드 프리필과 가깝지만 비트 단위로 항상 같지는 않습니다. logit이 거의 같은 지점에서는 탐욕적 응답이 갈라질 수 있는데, 캐시 손상이 아니며 어떤 분할 크기를 써도 사라지지 않습니다.

회귀 테스트, 평가 실행, 두 빌드 비교처럼 캐시 상태와 무관한 출력이 필요하면 cache_prompt: false를 보내십시오. 그 요청은 콜드로 프리필되고 캐시 항목을 읽지도 기증하지도 않습니다. 이 필드는 요청 루트, 평탄화된 OpenAI SDK extra_body, extra_body 안쪽 어디에 있어도 인식하며, 채팅 라우트와 네이티브 /completion에서 동작합니다. 서버 전체에 적용하려면 --no-cache-prompt를 쓰십시오.

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"m","temperature":0,"cache_prompt":false,"messages":[{"role":"user","content":"Hello"}]}'

캐시 히트가 콜드 실행을 정확히 재현하는 조건은 프롬프트 캐시를 참조하십시오.

긴 프리필과 프록시 타임아웃

프롬프트가 큰 경우(32k 토큰 이상), 프리필 순전파 연산이 첫 번째 토큰을 생성하기까지 수십 초가 걸릴 수 있습니다. 이 구간에 SSE 스트림은 열려 있지만 아무 데이터도 전송되지 않습니다.

리버스 프록시와 로드 밸런서는 유휴 타임아웃을 적용하는데 이 타임아웃이 첫 번째 토큰 도착 전에 만료될 수 있습니다.

프록시 관련 설정 기본값
nginx proxy_read_timeout 60초
HAProxy timeout tunnel 60초
AWS ALB 유휴 타임아웃 60초
Cloudflare no-response 타임아웃 100초

mlxcel-server는 15초마다 SSE keepalive 코멘트를 전송하여 연결이 끊기지 않도록 합니다. 이 코멘트는 SSE 토큰 이벤트를 파싱하는 클라이언트에는 보이지 않으며 프록시의 유휴 타이머만 초기화합니다.

긴 컨텍스트 요청에서 클라이언트가 504 오류 또는 연결 리셋 오류를 받는다면, 프록시 유휴 타임아웃이 프리필 시간보다 짧은지 확인하는 것이 첫 번째 점검 사항입니다. proxy_read_timeout을 300초 이상으로 늘리거나 터널 구간 타임아웃을 비활성화하는 것이 가장 안정적인 해결 방법입니다. --decode-timeout 플래그는 서버 측 디코드 행 감지를 제어하며(기본값: 600초), 프리필 구간에는 영향을 주지 않습니다. --timeout은 HTTP 소켓 read/write 타임아웃입니다(기본값: 3600초).

KV 캐시 크기 제한 (--max-kv-size)

기본값에서는 KV 캐시가 시퀀스 수명 동안 제한 없이 커집니다. 긴 컨텍스트 대화나 모델의 컨텍스트 윈도우를 초과하는 생성 작업에서는 처리된 토큰 수에 비례해 메모리 사용량이 늘어납니다. --max-kv-size N은 라이브 KV 윈도우를 N 토큰으로 제한하는데 시퀀스 캐시가 N 토큰을 초과하면 스케줄러가 매 포워드 패스 전에 가장 오래된 항목을 제거해 최근 N 토큰만 어텐션에 참여하도록 합니다.

RoPE 위치 충실성: 캐시의 단조 증가 쓰기 위치(offset)는 절대 감소하지 않습니다. 라이브 윈도우를 트리밍할 때는 offset 대신 live_start가 전진하므로, 캐시된 각 키의 현재 쿼리에 대한 상대 위치가 RoPE 기준으로 항상 정확하게 유지됩니다. 이는 mlx-lm의 위치 시맨틱과 같은 방식이며 단순히 offset을 감소시키는 방식에서 발생하는 위치 드리프트를 방지합니다.

CLI 플래그 (서버 전체 모든 시퀀스에 적용):

mlxcel-server --model ./models/Llama-3.1-8B-Instruct --max-kv-size 4096

환경 변수 (충돌 시 CLI 우선):

LLAMA_ARG_MAX_KV_SIZE=4096 mlxcel-server --model ./models/Llama-3.1-8B-Instruct

허용 범위: 0(비활성화, 기본값) 또는 64 이상 2147483647(i32::MAX) 이하의 정수. 64 미만의 값은 프리필 스텝 단위보다 작아 매 디코드 스텝마다 캐시를 트리밍하게 되므로 시작 시 거부됩니다. i32::MAX를 초과하는 값도 MLX FFI가 캐시 형상 및 슬라이스 인덱스에 i32를 사용하기 때문에 거부됩니다.

슬라이딩 윈도우 모델은 영향받지 않습니다: 자체 회전 윈도우(RotatingKVCache)를 관리하는 모델(Gemma ¾, Exaone 4, RecurrentGemma, Step 3.5, gpt-oss)은 모델 레벨 캡을 그대로 적용하며 --max-kv-size를 무시합니다.

Turbo KV 양자화 제한: --max-kv-size는 Turbo KV 양자화(--kv-cache-mode turbo4* / --kv-quant-scheme turboquant)와 함께 사용할 수 없습니다. 두 옵션을 함께 설정하면 Turbo 양자화된 레이어에 대해 라이브 윈도우 캡이 조용히 무시되고 시작 시 경고가 기록됩니다. 페이지드 메모리 백엔드와의 연동은 이번 릴리스 범위 밖이며 별도로 추적됩니다.


Nemotron-H 요청 메모리 admission

Metal에서 텍스트 전용 Nemotron-H를 서빙하면 요청을 시작하기 전에 보수적으로 잡은 메모리 추정치를 확인합니다. 스케줄러는 이 계열을 활성 슬롯 하나로 제한합니다. 추정치가 예산을 넘는 요청은 GPU forward를 실행하지 않고 오류 유형 request_memory_budget_exceeded와 함께 HTTP 503을 받으며, 스트리밍 요청은 SSE 헤더를 보내기 전에 503을 받습니다. /health는 계속 응답하고 더 작은 요청은 이후에 실행됩니다. 기존 컨텍스트 길이 검사가 먼저 적용되며 그 결과는 여전히 HTTP 400입니다.

예산은 장치의 권장 working set 크기, 설정된 wired 메모리 상한, 현재 MLX allocator 상한 가운데 가장 작은 값입니다. MLXCEL_REQUEST_MEMORY_BUDGET(예: 24G)로 낮출 수는 있지만 높일 수는 없고, 0, 빈 값, none, 음수, 유한하지 않은 값, overflow 값을 명시하면 서버가 시작하지 않습니다. 추정치에는 상주 가중치와 보관된 프롬프트 스냅샷, 프롬프트와 계획된 출력 전체에 대한 콜드 KV, recurrent·convolution 상태, 요청 프리필 계획에서 가장 큰 forward가 들어갑니다.

이 검사를 받는 요청은 콜드로 프리필하며, 프롬프트 스냅샷을 복원하거나 기증하지 않고 백그라운드 프롬프트 캐시 warmup도 건너뜁니다. 배치 스케줄러와 --no-batch 서빙 모두 이 검사를 적용합니다. Metal Nemotron-H의 런타임(비융합) LoRA는 activation 작업 공간을 추정에 넣지 않으므로 모델 생성 단계에서 실패하며, 융합된 어댑터 체크포인트를 써야 합니다. CUDA, ROCm, CPU 백엔드, 다른 모델 계열, 멀티모달 경로, 투기적 디코딩에는 적용되지 않고, 다른 프로세스가 쓰는 메모리까지 막아 주는 장치 단위 예약도 아닙니다.


루프 감지 (N-gram 반복 가드)

일부 체크포인트는 tool 또는 grammar 제약 디코딩에서 토큰 하나나 짧은 블록이 토큰 예산을 소진할 때까지 반복되는 퇴화 루프에 빠질 수 있습니다. mlxcel은 생성 스트림 끝에서 짧은 패턴이 반복되는 것을 감지해 요청을 조기 종료할 수 있는데, vLLM이 SamplingParams에서 제공하는 것과 같은 방식의 대응책입니다.

Gemma 4 계열은 요청이 tool을 쓰는지 json_schema response format을 쓰는지와 무관하게 이 기능을 무조건 기본으로 켭니다(threshold min=1, max=20, count=4). 다른 계열은 요청이 명시적으로 opt-in하지 않는 한 꺼져 있습니다.

세 단계로 제어할 수 있으며 뒤로 갈수록 앞 단계를 override합니다.

  1. 계열별 기본값: Gemma 4는 켜져 있고 그 외는 꺼져 있습니다.
  2. 운영자 override(MLXCEL_LOOP_DETECTION): 이 서버의 모든 요청에 정책을 강제합니다. MLXCEL 환경 변수를 참조하십시오.
  3. 요청 단위 필드(요청 본문의 max_pattern_size, min_pattern_size, min_count): Gemma 4 서버에서 강제로 끄는 max_pattern_size: 0을 포함해 항상 우선합니다. REST API 레퍼런스를 참조하십시오.

content만 돌려보내는 클라이언트를 위한 reasoning 재주입

어떤 채팅 템플릿은 뒤따르는 assistant 메시지에 reasoning이 있는지에 따라 앞선 턴을 다르게 렌더링합니다. 예를 들어 AI21 Jamba-Reasoning은 그 경우에만 앞선 user 턴에 thinking 지시를 남기므로, content만 돌려보내는 클라이언트(OpenAI SDK 기본 동작)는 그 턴을 생성 당시보다 짧게 다시 렌더링하게 되고 후속 요청이 프롬프트 캐시를 놓칩니다.

프롬프트 캐시가 켜져 있으면 채팅 라우트는 일반(도구 호출이 아닌) 응답마다 돌려준 reasoning을 그대로 기억해 두었다가, 나중에 content만 있는 assistant 턴에 클라이언트가 직접 돌려보낸 것처럼 다시 넣습니다.

라우트 다시 넣는 reasoning
/v1/chat/completions reasoning_content
/v1/responses reasoning 출력 항목
/v1/messages thinking 블록. 요청이 extended thinking을 켰을 때만 기록합니다

저장된 reasoning은 모델, 채팅 템플릿 서명(템플릿, kwargs, 도구), 세션(prompt_cache_key, 없으면 user), 앞선 메시지, assistant content가 모두 같은 요청만 찾을 수 있습니다. 하나라도 바뀌면 찾지 못하고, 찾지 못해도 아무것도 바뀌지 않으며, 클라이언트가 직접 돌려보낸 reasoning이 항상 우선합니다. 돌려보낸 reasoning은 reasoning_content와 reasoning 두 필드로 채팅 템플릿에 전달되므로 둘 중 어느 쪽을 읽는 템플릿이든 한 번만 렌더링합니다. 저장소는 32바이트 키와 reasoning 텍스트만 보관하고, 16 MiB와 4096개 항목 안에서 가장 오래 쓰지 않은 항목부터 축출하며(MLXCEL_REASONING_ECHO_MAX_BYTES, 0이면 비활성화), 재시작하면 비워집니다. cache_prompt: false, --reasoning-format none 또는 deepseek-legacy, --skip-chat-parsing에서는 참조하지 않습니다.

생성 프롬프트가 <think>\n으로 끝나는 템플릿(primed-think 템플릿)은 세 라우트 모두에서 첫 토큰부터 모델의 reasoning을 reasoning으로 스트리밍합니다.


추론 토큰 예산 (Qwen3 계열)

Qwen3, Qwen3.5, Qwen3.6 모델은 최종 답변 전에 <think>...</think> 추론 블록을 생성합니다. mlxcel-server는 해당 블록 내에서 모델이 소비할 수 있는 토큰 수를 제한할 수 있으며 llama.cpp의 --reasoning-budget 플래그와 vLLM의 thinking_token_budget 샘플링 파라미터와 동일한 의미를 따릅니다.

값 동작
-1 제한 없음(기본값).
0 첫 번째 추론 토큰에서 즉시 </think> 삽입.
N > 0 추론 블록을 N 토큰으로 제한한 뒤 </think> 강제 삽입.

여는 <think> 토큰은 제한 횟수에 포함되지 않습니다(vLLM 동작 방식과 동일). <think> / </think> 토큰이 없는 모델에서는 파라미터를 무시하고 모델 로드 시 한 번만 경고 로그를 남깁니다.

CLI 플래그 (요청별 값으로 재정의하지 않으면 모든 요청에 적용):

mlxcel-server --model ./models/Qwen3-8B-Instruct --reasoning-budget 256

환경 변수 (CLI 플래그가 충돌 시 우선):

LLAMA_ARG_REASONING_BUDGET=256 mlxcel-server --model ./models/Qwen3-8B-Instruct

/v1/chat/completions 요청별 재정의. 기본 이름은 thinking_budget_tokens이며 thinking_token_budget(vLLM)과 thinking_budget(Qwen)도 별칭으로 허용됩니다. 여러 필드가 함께 있으면 기본 이름이 우선합니다:

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
        "model": "qwen3",
        "messages": [{"role":"user","content":"경사 하강법을 설명해 주세요."}],
        "max_tokens": 512,
        "thinking_budget_tokens": 256
      }'

/completion 요청별 재정의 (llama.cpp 스타일). 동일한 세 가지 별칭 이름이 허용됩니다:

curl http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
        "prompt": "<|im_start|>user\n경사 하강법을 설명해 주세요.<|im_end|>\n<|im_start|>assistant\n",
        "n_predict": 512,
        "thinking_budget_tokens": 256
      }'

우선순위 규칙 (높은 순서부터):

  1. 요청 본문 필드 (thinking_budget_tokens → thinking_token_budget → thinking_budget).
  2. --reasoning-budget <N> CLI 플래그.
  3. LLAMA_ARG_REASONING_BUDGET 환경 변수.
  4. 기본값: 제한 없음(-1).

요청별 값으로 -1을 지정하면 서버 기본값이 설정되어 있더라도 해당 요청에 한해 제한이 해제됩니다. thinking_budget_tokens > 0인 경우에는 max_tokens(/completion에서는 n_predict) 이하여야 하며 그렇지 않으면 400 오류를 반환합니다. -2 같은 다른 음수 값도 400 오류를 반환합니다.

예산이 소진되면 강제 삽입되는 </think>는 스트리밍 모드에서 일반 SSE 델타 청크로 전송되며 finish_reason 처리 시 집계됩니다(사용자 지정 중단 시퀀스를 트리거하지 않습니다).

Chat Template Kwargs

일부 HuggingFace 채팅 템플릿은 프롬프트에 직접 렌더링되는 기능 스위치를 kwarg로 받습니다. mlxcel-server는 시작 시(기본값) 그리고 요청별(재정의)로 JSON 객체 형태의 템플릿 kwarg를 받아들이며 llama.cpp의 --chat-template-kwargs 플래그 및 vLLM의 extra_body.chat_template_kwargs와 동일한 형태를 따릅니다. 현재 대표적인 소비자는 Qwen3.6의 preserve_thinking(다중 턴 추론 보존)이지만 구조 자체는 일반적이어서 향후 추가될 kwarg도 동일한 배관을 그대로 활용합니다.

CLI 플래그:

mlxcel-server --model ./models/Qwen3.6-35B-A3B \
  --chat-template-kwargs '{"preserve_thinking": true}'

환경 변수 (충돌 시 CLI 우선):

LLAMA_ARG_CHAT_TEMPLATE_KWARGS='{"preserve_thinking": true}' \
  mlxcel-server --model ./models/Qwen3.6-35B-A3B

요청별 재정의 (/v1/chat/completions). 세 가지 형태를 허용하며 먼저 일치하는 것이 적용됩니다.

  1. 최상위 chat_template_kwargs (llama.cpp 형태):
{"chat_template_kwargs": {"preserve_thinking": true}}
  1. extra_body 내부 중첩 (vLLM / OpenAI Python SDK 형태):
{"extra_body": {"chat_template_kwargs": {"preserve_thinking": true}}}
client.chat.completions.create(
    model="qwen3.6",
    messages=[...],
    extra_body={"chat_template_kwargs": {"preserve_thinking": True}},
)
  1. DashScope 평면 형태 (보조; preserve_thinking 키만 인식):
{"extra_body": {"preserve_thinking": true}}

병합 규칙: 요청별 키가 서버 기본값을 키 단위로 재정의하며 요청에 없는 서버 기본 키는 그대로 유지됩니다. 서버를 {"preserve_thinking": true, "enable_thinking": false}로 시작한 뒤 요청이 {"preserve_thinking": false}를 보내면, 실제 적용되는 kwarg는 {"preserve_thinking": false, "enable_thinking": false}가 됩니다.

우선순위 규칙 (높은 순서부터):

  1. 요청의 최상위 chat_template_kwargs.
  2. 요청의 extra_body.chat_template_kwargs.
  3. 요청의 extra_body.preserve_thinking (DashScope; preserve_thinking 키에 한함).
  4. --chat-template-kwargs CLI 플래그.
  5. LLAMA_ARG_CHAT_TEMPLATE_KWARGS 환경 변수.
  6. 기본값: 비어 있음 (적용 kwarg 없음).

preserve_thinking 동작:

  • true: 이전 모든 assistant 턴의 <think>...</think> 블록을 그대로 모델에 전달합니다.
  • false (기본값): 최근 비-tool user 턴 이전의 assistant 메시지에서 <think> 블록을 서버 측에서 제거해, Qwen3/Qwen3.5의 rolling-checkpoint 규칙과 일치시킵니다.

추론 기능이 없는 모델(Qwen2, Llama)은 플래그를 조용히 무시합니다. Qwen3/Qwen3.5도 플래그를 받아들이기는 하지만 다중 턴 품질 이점은 Qwen3.6에서만 검증되었는데 이전 Qwen3 계열은 보존된 추론 흔적으로 학습되지 않았기 때문입니다.

--chat-template-kwargs나 LLAMA_ARG_CHAT_TEMPLATE_KWARGS의 JSON이 올바르지 않으면 서버는 명확한 오류와 함께 기동을 거부합니다.

thinking 모델에서 enable_thinking 자동 기본값 설정:

서버는 토크나이저 로드 후 infer_thinking_markers()를 호출해 모델의 think-start / think-end 토큰 시퀀스를 확인합니다. 단일 토큰 <think> / </think>(Qwen3, GLM4, Nemotron-H, ExaOne4), 단일 토큰 <longcat_think> / </longcat_think> 계열, 또는 멀티 토큰 <|channel>thought / <channel|>(Gemma 4)이 확인되면, 서버는 enable_thinking=true를 서버 수준 기본 kwarg로 자동 설정합니다. 이 기본값은 다른 서버 기본 kwarg와 동일하게 build_template_context를 거쳐 적용되므로, 요청별 chat_template_kwargs나 extra_body.chat_template_kwargs로 언제든 재정의할 수 있습니다.

thinking 기능이 없는 모델에서는 플래그를 주입하지 않아 채팅 템플릿에 영향을 미치지 않습니다. 서버는 시작 시 확인된 마커 쌍과 각 시퀀스의 토큰 수를 INFO 로그로 남기는데 Gemma 4의 멀티 토큰 마커가 올바르게 인식되고 있는지 확인하는 데 유용합니다.

INFO Tokenizer recognizes a think marker pair; defaulting chat_template kwarg `enable_thinking=true` think_start=Some("<|channel>thought") think_end=Some("<channel|>") think_start_tokens_len=2 think_end_tokens_len=1

특정 요청에서 thinking 모드를 비활성화하려면 앞서 설명한 세 가지 요청별 형태 중 하나로 enable_thinking=false를 전달하면 됩니다.

reasoning_effort 매핑: chat completions의 reasoning_effort 필드는 세 가지 가드 아래 reasoning_effort chat template kwarg로 매핑됩니다. 명시적인 chat_template_kwargs.reasoning_effort가 우선하고, 로드된 템플릿이 실제로 그 이름을 언급해야 하며, 값은 번역하지 않고 그대로 전달합니다. /v1/responses의 reasoning.effort에는 이 매핑이 적용되지 않으므로 그쪽에서는 chat_template_kwargs를 직접 설정하십시오.

템플릿이 거부한 요청은 실패합니다. chat template 렌더링이 실패하면, 모델이 학습한 적 없는 단순 연결로 폴백하지 않고 생성 시작 전에 HTTP 400을 반환합니다. 타입 경로와 raw/멀티모달 경로에 동일하게 적용되므로 tool call, 이전 턴의 reasoning 필드, 타입 미디어 파트를 실은 요청도 같은 답을 받습니다. 스트리밍 요청은 스트림 중간에 실패하는 대신 SSE 스트림 자리에 400을 반환합니다.

Florence-2 서빙

Florence-2는 인코더-디코더 모델이라 서버가 배치·paged 스케줄러가 아니라 전용 단일 스트림 seq2seq 워커로 라우팅합니다. 요청은 --max-queue-depth로 상한이 걸린 워커 채널에 대기했다가 설계상 한 번에 하나씩 처리됩니다. 인코더 패스는 디코드 루프와 비용 특성이 다르고, 이에 대한 배치 admission 정책은 아직 측정된 바 없습니다.

OpenAI 호환 POST /v1/chat/completions에 Florence-2 태스크 프롬프트를 텍스트로 담은 user 메시지 하나와 image_url 콘텐츠 파트 하나를 보냅니다. message.content에 렌더링된 답변이, mlxcel 전용 message.florence2_result에 파싱된 좌표가 JSON으로 들어갑니다.

디코드가 greedy이므로 샘플링 파라미터와 response_format은 받되 무시하고, stream: true는 파싱 결과가 전체 디코드 이후에만 존재하므로 답변 전체를 델타 청크 하나로 전달합니다. 입력 텍스트를 받는 일곱 태스크 모드의 텍스트는 요청 경계에서 검증하고, 선언된 이미지는 모두 해석돼야 하며, 이미지 페이로드는 다른 계열과 같은 압축 폭탄 방어 한계를 거칩니다. usage.prompt_tokens는 투영된 이미지 토큰을 포함한 융합 인코더 길이를 보고합니다.

태스크 마커와 CLI 사용법은 비전-언어 모델을 참조하십시오.

블록 디퓨전 모델 서빙

로드된 체크포인트가 블록 디퓨전 모델(DiffusionGemma)이면 서버는 요청을 배치 스케줄러 대신 전용 직렬 워커로 보냅니다. 요청은 도착 순서대로 큐에 쌓여 한 번에 하나씩 처리되며 이는 의도된 설계입니다. /v1/chat/completions와 /v1/completions 모두 스트리밍·비스트리밍 응답으로 동작하고 이미지 입력은 표준 OpenAI image_url content part 형식을 따릅니다.

디퓨전 엔진을 조정하는 serve 플래그는 --diffusion-sampler, --diffusion-threshold, --max-denoising-steps 세 가지입니다. 이 체크포인트는 thinking 채널 모델이므로 요청에 충분한 max_tokens를 주어야 합니다(예산 64는 reasoning만 생성하고 content가 빌 수 있으며 256이면 대부분의 짧은 프롬프트를 처리합니다). 생성 메커니즘, 플래그, 운영 시 유의점은 블록 디퓨전을 참조하십시오.