# Qwen3.5 + Open WebUI — vllm-mlx (Mac 최적화) > 환경: MacBook Pro M4 Pro 48GB / Docker Desktop / Python 3.10+ --- # 목차 1. [환경 준비](#1-환경-준비) 2. [서버 시작](#2-서버-시작) 3. [Open WebUI 연결](#3-open-webui-연결) 4. [종료 / 재시작](#4-종료--재시작) 5. [파라미터 레퍼런스](#5-파라미터-레퍼런스) 6. [트러블슈팅](#6-트러블슈팅) --- # 1. 환경 준비 > ⚠️ conda 환경에서 실행하면 MPICH와 MLX가 충돌합니다. 반드시 별도 venv에서 실행하세요. ```bash conda deactivate python3 -m venv ~/mlx-env source ~/mlx-env/bin/activate pip install git+https://github.com/waybarrios/vllm-mlx.git ``` --- # 2. 서버 시작 모델은 첫 시작 시 HuggingFace에서 자동 다운로드됩니다. ## 텍스트 전용 ```bash source ~/mlx-env/bin/activate vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 \ --max-tokens 8192 \ --default-temperature 0.7 \ --default-top-p 0.9 ``` ## 텍스트 + 이미지 (멀티모달) ```bash source ~/mlx-env/bin/activate vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 \ --max-tokens 8192 \ --default-temperature 0.7 \ --default-top-p 0.9 \ --mllm ``` > `--mllm` 플래그 하나로 이미지 입력이 활성화됩니다. > > 포그라운드로 실행됩니다. **새 터미널 탭**을 열어서 다음 단계를 진행하세요. ## 용도별 추천 설정 ```bash # 문서 생성 (보수적) vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 --max-tokens 8192 \ --default-temperature 0.2 --default-top-p 0.95 --mllm # 대화 / 창의적 응답 vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 --max-tokens 8192 \ --default-temperature 1.0 --default-top-p 0.8 --mllm # 동시 요청 최적화 (여러 사용자) # ⚠️ --continuous-batching은 단일 사용자일 때 오히려 느려질 수 있음 vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 --max-tokens 8192 \ --default-temperature 0.7 --mllm --continuous-batching ``` --- # 3. Open WebUI 연결 ### 3-1. 서버 동작 확인 ```bash curl http://localhost:8090/v1/models curl http://localhost:8090/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "mlx-community/Qwen3.5-35B-A3B-4bit", "max_tokens": 8192, "messages": [{"role": "user", "content": "안녕하세요"}] }' ``` ### 3-2. Open WebUI 실행 ```bash cd ~/PyCharmMiscProject/openwebui docker compose -f docker-compose.mlx.yml up -d ``` > `docker-compose.mlx.yml`이 없으면 [부록](#부록-docker-composemlxyml-생성)을 참고하세요. ### 3-3. 브라우저 접속 ``` http://localhost:3000 ``` 1. 첫 접속 시 회원가입 (로컬 전용, 첫 계정 = admin) 2. **설정 → Connections** → OpenAI API 연결 확인 - URL: `http://host.docker.internal:8090/v1` - API Key: `none` 3. 상단 모델 선택 후 채팅 시작 --- # 4. 종료 / 재시작 ### 종료 ```bash # vllm-mlx 서버: Ctrl+C # Open WebUI cd ~/PyCharmMiscProject/openwebui docker compose -f docker-compose.mlx.yml down ``` ### 재시작 ```bash # 터미널 1: 서버 source ~/mlx-env/bin/activate vllm-mlx serve mlx-community/Qwen3.5-35B-A3B-4bit \ --port 8090 --max-tokens 8192 \ --default-temperature 0.7 --default-top-p 0.9 --mllm # 터미널 2: Open WebUI cd ~/PyCharmMiscProject/openwebui docker compose -f docker-compose.mlx.yml up -d ``` --- # 5. 파라미터 레퍼런스 ## 서버 시작 플래그 ### 토큰 제한 | 구분 | 의미 | 설정 방법 | |------|------|-----------| | **max_tokens (출력)** | 모델이 생성하는 최대 토큰 수 | `--max-tokens 8192` | | **context length (입력)** | 모델이 받을 수 있는 최대 입력 토큰 수 | 모델 자체 설정 (Qwen3.5: 최대 262K) | > 입력 길이를 제한하려면 `--use-paged-cache`와 `--max-cache-blocks`를 조합합니다. > `max-cache-blocks × 64(블록 사이즈) = 최대 입력 토큰 수` > > 예: 입력 8192 토큰 제한 → `--use-paged-cache --max-cache-blocks 128` ### 생성 파라미터 | 플래그 | 기본값 | 설명 | |--------|--------|------| | `--max-tokens` | 32768 | 최대 **출력** 토큰 수 (thinking + 응답 합산) | | `--default-temperature` | 모델 기본값 | Temperature (0.0~2.0) | | `--default-top-p` | 모델 기본값 | Nucleus Sampling | | `--reasoning-parser qwen3` | off | thinking 내용을 별도 필드로 추출 | ### 모달리티 | 플래그 | 기본값 | 설명 | |--------|--------|------| | `--mllm` | off | 멀티모달 (이미지 입력) 활성화 | ### 성능 | 플래그 | 기본값 | 설명 | |--------|--------|------| | `--continuous-batching` | off | 동시 요청 배칭 (단일 사용자 시 오히려 느림) | | `--prefill-step-size` | 2048 | 프리필 처리 단위 | | `--stream-interval` | 1 | 스트리밍 배치 토큰 수 (1=부드러움, 클수록 처리량↑) | ### 메모리 | 플래그 | 기본값 | 설명 | |--------|--------|------| | `--kv-cache-quantization` | off | KV 캐시 양자화 활성화 (메모리 절약) | | `--kv-cache-quantization-bits` | 8 | KV 캐시 양자화 비트 (4 또는 8) | | `--cache-memory-percent` | 0.20 | RAM의 몇 %를 캐시에 할당 | ### 서버 | 플래그 | 기본값 | 설명 | |--------|--------|------| | `--port` | - | 서버 포트 | | `--host` | - | 서버 호스트 | | `--api-key` | - | API 키 (미설정 시 인증 없음) | | `--rate-limit` | 0 | 분당 요청 제한 (0 = 무제한) | | `--timeout` | 300 | 요청 타임아웃 (초) | ## 각 파라미터가 하는 일 ### `--default-temperature` (Temperature) 모델의 출력 랜덤성을 조절합니다. ``` 0.0 ←──────────────────────────→ 2.0 결정적 랜덤 (같은 질문 = 같은 답) (창의적, 횡설수설 가능) ``` - `0.0` → 항상 가장 확률 높은 토큰 선택 - `0.7` → 일반 대화에 적합 - `1.0+` → 창의적 글쓰기 ### `--default-top-p` (Nucleus Sampling) 누적 확률 상위 p%에 드는 토큰만 후보로 남깁니다. ``` 예: top-p = 0.9 [토큰A: 50%] [토큰B: 30%] [토큰C: 10%] | [토큰D: 5%] [토큰E: 3%] ... ─────────── 상위 90% (후보) ────────── ──── 제외 ──── ``` - `0.9` → 희귀한 토큰 배제, 안정적 - `1.0` → 필터 없음 ### `--max-tokens` (출력 토큰 제한) 한 요청에서 모델이 **생성(출력)**할 수 있는 최대 토큰 수입니다. - Qwen3.5는 **thinking 토큰 + 응답 토큰이 합산**됩니다 - 너무 크면 thinking이 끝없이 돌 수 있음 - **8192 권장** > 입력 토큰 제한(context length)은 별도 플래그가 없으며, 모델의 context window(Qwen3.5: 최대 262K)를 따릅니다. 메모리가 부족하면 `--kv-cache-quantization`이나 `--cache-memory-mb`로 간접 제한하세요. ### `--mllm` 멀티모달 모드를 활성화합니다. 이 플래그가 없으면 텍스트만 처리합니다. ### `--continuous-batching` 여러 요청을 동시에 처리합니다. **단일 사용자일 때는 오히려 느려지므로**, 동시 사용자가 있을 때만 사용하세요. ### `--reasoning-parser qwen3` Qwen3.5의 `...` thinking 내용을 별도 `reasoning_content` 필드로 추출합니다. Open WebUI에서 thinking 과정을 보고 싶을 때 유용합니다. --- # 6. 트러블슈팅 ## 빠른 참조 | 증상 | 원인 | 해결 | |------|------|------| | thinking만 하고 응답 없음 | `--max-tokens`가 너무 큼 | 8192로 설정 후 서버 재시작 | | conda에서 abort / MPI 에러 | MPICH 충돌 | `conda deactivate` 후 venv에서 실행 | | 이미지 입력 안 됨 | `--mllm` 미설정 | 서버 시작 시 `--mllm` 추가 | | 모델이 안 보임 | 서버 미실행 | `curl http://localhost:8090/v1/models` 확인 | | 컨테이너 안 뜸 | Docker 문제 | `docker logs open-webui-mlx` 확인 | | 포트 충돌 | 이미 사용 중 | `lsof -i :3000` / `lsof -i :8090` | | 요청 타임아웃 | 기본 300초 초과 | `--timeout 600` 으로 늘리기 | | 이미지 입력 시 PyTorch 텐서 에러 | transformers fast image processor 호환성 문제 | 아래 **[이미지 프로세서 호환성 문제](#이미지-프로세서-호환성-문제-transformers--mlx_vlm)** 참고 | | 서버가 Ctrl+C로 안 꺼짐 | 프로세스가 응답 중 | `lsof -ti :8090 \| xargs kill -9` | --- ## 이미지 프로세서 호환성 문제 (transformers + mlx_vlm) ### 증상 `--mllm` 모드에서 이미지를 입력하면 아래 에러가 발생합니다: ``` ValueError: Failed to process inputs with error: Only returning PyTorch tensors is currently supported. ``` 또는 Open WebUI에서: ``` Response payload is not completed: ``` ### 원인 `vllm-mlx`는 내부적으로 `mlx_vlm`을 사용하여 이미지를 처리합니다. `mlx_vlm`은 `transformers` 라이브러리의 이미지 프로세서를 호출하는데, `transformers >= 5.x`에서는 **Fast Image Processor**가 기본으로 로드됩니다. ``` 요청 흐름: Open WebUI → vllm-mlx → mlx_vlm → transformers (이미지 프로세서) → 에러 ``` 문제의 핵심: | | Fast 버전 | Slow 버전 | |---|---|---| | 파일 | `image_processing_qwen2_vl_fast.py` | `image_processing_qwen2_vl.py` | | 반환 형식 | **PyTorch 텐서만** 지원 | numpy 등 다양한 형식 지원 | | mlx_vlm 호환 | ❌ | ✅ | `transformers`는 자동으로 fast 버전을 우선 로드하지만, `mlx_vlm`은 PyTorch 텐서가 아닌 numpy/MLX 배열을 기대하므로 충돌이 발생합니다. ### 해결 방법 fast 파일을 slow 클래스로 리다이렉트합니다. **1. fast 파일 위치 확인:** ```bash # venv 경로에 따라 다를 수 있음 FAST_FILE="$(python -c "import transformers; import os; print(os.path.join(os.path.dirname(transformers.__file__), 'models/qwen2_vl/image_processing_qwen2_vl_fast.py'))")" echo "$FAST_FILE" ``` **2. 원본 백업:** ```bash cp "$FAST_FILE" "${FAST_FILE}.bak" ``` **3. fast 파일을 slow 버전으로 리다이렉트:** ```bash cat > "$FAST_FILE" << 'EOF' """ Fast Image processor class for Qwen2-VL. Patched: mlx_vlm 호환을 위해 slow 버전으로 폴백합니다. 원본: image_processing_qwen2_vl_fast.py.bak """ from .image_processing_qwen2_vl import Qwen2VLImageProcessor as Qwen2VLImageProcessorFast __all__ = ["Qwen2VLImageProcessorFast"] EOF ``` **4. 서버 재시작 후 이미지 테스트** ### 복원 방법 패치를 되돌리려면: ```bash FAST_FILE="$(python -c "import transformers; import os; print(os.path.join(os.path.dirname(transformers.__file__), 'models/qwen2_vl/image_processing_qwen2_vl_fast.py'))")" cp "${FAST_FILE}.bak" "$FAST_FILE" ``` ### 언제 이 패치가 필요 없어지나 - `mlx_vlm`이 fast image processor를 지원하게 업데이트되면 - `transformers`가 PyTorch 외의 텐서 반환을 지원하면 - `vllm-mlx`가 자체 이미지 프로세싱을 구현하면 `pip install --upgrade mlx-vlm` 후 이미지 테스트가 정상이면 패치를 복원해도 됩니다. --- # 부록: docker-compose.mlx.yml 생성 > `~/PyCharmMiscProject/openwebui/docker-compose.mlx.yml`이 없을 때만 실행하세요. ```bash mkdir -p ~/PyCharmMiscProject/openwebui cd ~/PyCharmMiscProject/openwebui cat > docker-compose.mlx.yml << 'EOF' services: open-webui: image: ghcr.io/open-webui/open-webui:main container_name: open-webui-mlx ports: - "3000:8080" environment: - OPENAI_API_BASE_URL=http://host.docker.internal:8090/v1 - OPENAI_API_KEY=none - OLLAMA_BASE_URL= volumes: - open-webui-mlx-data:/app/backend/data extra_hosts: - "host.docker.internal:host-gateway" restart: unless-stopped volumes: open-webui-mlx-data: EOF ```