275 lines
8.4 KiB
Bash
Executable File
275 lines
8.4 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
#====================================================================
|
|
# Qwen3.5 + Open WebUI (vllm-mlx) 원클릭 셋업
|
|
# 환경: Apple Silicon Mac (M1/M2/M3/M4) / Docker Desktop / Python 3.10+
|
|
#====================================================================
|
|
|
|
VENV_DIR="$HOME/mlx-env"
|
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
MODEL="mlx-community/Qwen3.5-35B-A3B-4bit"
|
|
PORT=8090
|
|
WEBUI_PORT=3000
|
|
MAX_TOKENS=8192
|
|
MAX_INPUT_TOKENS=8192
|
|
TEMPERATURE=0.7
|
|
TOP_P=0.9
|
|
|
|
echo "============================================"
|
|
echo " Qwen3.5 + Open WebUI (vllm-mlx) 셋업"
|
|
echo "============================================"
|
|
echo ""
|
|
|
|
#--------------------------------------------------------------------
|
|
# 1. 사전 요구사항 확인
|
|
#--------------------------------------------------------------------
|
|
echo "[1/6] 사전 요구사항 확인..."
|
|
|
|
# Python
|
|
if ! command -v python3 &>/dev/null; then
|
|
echo "❌ python3가 설치되어 있지 않습니다."
|
|
exit 1
|
|
fi
|
|
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
|
PYTHON_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1)
|
|
PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
|
|
if [ "$PYTHON_MAJOR" -lt 3 ] || { [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 10 ]; }; then
|
|
echo "❌ Python 3.10 이상이 필요합니다. (현재: $PYTHON_VERSION)"
|
|
exit 1
|
|
fi
|
|
echo " ✓ Python $PYTHON_VERSION"
|
|
|
|
# Docker
|
|
if ! command -v docker &>/dev/null; then
|
|
echo "❌ Docker가 설치되어 있지 않습니다."
|
|
exit 1
|
|
fi
|
|
if ! docker info &>/dev/null; then
|
|
echo "❌ Docker Desktop이 실행 중이 아닙니다."
|
|
exit 1
|
|
fi
|
|
echo " ✓ Docker"
|
|
|
|
# Apple Silicon
|
|
if ! sysctl -n machdep.cpu.brand_string 2>/dev/null | grep -q "Apple"; then
|
|
echo "❌ Apple Silicon이 아닙니다. MLX는 Apple Silicon에서만 동작합니다."
|
|
exit 1
|
|
fi
|
|
echo " ✓ Apple Silicon"
|
|
|
|
# RAM
|
|
TOTAL_RAM_GB=$(sysctl -n hw.memsize | awk '{printf "%.0f", $1/1024/1024/1024}')
|
|
echo " ✓ RAM: ${TOTAL_RAM_GB}GB"
|
|
if [ "$TOTAL_RAM_GB" -lt 32 ]; then
|
|
echo " ⚠️ RAM이 32GB 미만입니다. 4bit 모델(~20GB)도 빡빡할 수 있습니다."
|
|
fi
|
|
|
|
echo ""
|
|
|
|
#--------------------------------------------------------------------
|
|
# 2. 가상환경 생성 및 패키지 설치
|
|
#--------------------------------------------------------------------
|
|
echo "[2/6] 가상환경 및 패키지 설치..."
|
|
|
|
# conda 감지 경고
|
|
if [ -n "$CONDA_DEFAULT_ENV" ]; then
|
|
echo " ⚠️ conda 환경 감지 ($CONDA_DEFAULT_ENV)."
|
|
echo " MPICH 충돌 방지를 위해 별도 venv를 사용합니다."
|
|
fi
|
|
|
|
if [ ! -d "$VENV_DIR" ]; then
|
|
python3 -m venv "$VENV_DIR"
|
|
echo " ✓ 가상환경 생성: $VENV_DIR"
|
|
else
|
|
echo " ✓ 기존 가상환경 사용: $VENV_DIR"
|
|
fi
|
|
|
|
source "$VENV_DIR/bin/activate"
|
|
|
|
# vllm-mlx
|
|
if ! pip show vllm-mlx &>/dev/null; then
|
|
echo " vllm-mlx 설치 중... (시간이 걸릴 수 있습니다)"
|
|
pip install -q git+https://github.com/waybarrios/vllm-mlx.git
|
|
echo " ✓ vllm-mlx 설치 완료"
|
|
else
|
|
echo " ✓ vllm-mlx 이미 설치됨"
|
|
fi
|
|
|
|
# torch, torchvision
|
|
if ! pip show torch &>/dev/null; then
|
|
echo " torch, torchvision 설치 중..."
|
|
pip install -q torch torchvision
|
|
echo " ✓ torch 설치 완료"
|
|
else
|
|
echo " ✓ torch 이미 설치됨"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
#--------------------------------------------------------------------
|
|
# 3. transformers fast image processor 패치
|
|
# 원인: transformers >= 5.x의 fast image processor가 PyTorch 텐서만
|
|
# 반환하지만, mlx_vlm은 numpy/MLX 배열을 기대하여 충돌 발생.
|
|
# 해결: fast 클래스를 slow 클래스로 리다이렉트.
|
|
#--------------------------------------------------------------------
|
|
echo "[3/6] 이미지 프로세서 호환성 패치..."
|
|
|
|
FAST_FILE=$("$VENV_DIR/bin/python3" -c "
|
|
import transformers, os
|
|
print(os.path.join(os.path.dirname(transformers.__file__),
|
|
'models/qwen2_vl/image_processing_qwen2_vl_fast.py'))
|
|
")
|
|
|
|
if grep -q "mlx_vlm 호환" "$FAST_FILE" 2>/dev/null; then
|
|
echo " ✓ 이미 패치 적용됨"
|
|
else
|
|
cp "$FAST_FILE" "${FAST_FILE}.bak"
|
|
|
|
cat > "$FAST_FILE" << 'PATCH'
|
|
"""
|
|
Fast Image processor class for Qwen2-VL.
|
|
Patched: mlx_vlm 호환을 위해 slow 버전으로 폴백합니다.
|
|
원본: image_processing_qwen2_vl_fast.py.bak
|
|
|
|
배경:
|
|
transformers >= 5.x는 Fast Image Processor를 기본 로드합니다.
|
|
Fast 버전은 PyTorch 텐서만 반환하지만, mlx_vlm은 numpy/MLX 배열을 기대합니다.
|
|
이 패치는 fast 클래스를 slow 클래스로 대체하여 호환성을 확보합니다.
|
|
|
|
복원:
|
|
cp image_processing_qwen2_vl_fast.py.bak image_processing_qwen2_vl_fast.py
|
|
"""
|
|
|
|
from .image_processing_qwen2_vl import Qwen2VLImageProcessor as Qwen2VLImageProcessorFast
|
|
|
|
__all__ = ["Qwen2VLImageProcessorFast"]
|
|
PATCH
|
|
|
|
echo " ✓ 패치 적용 완료 (원본 백업됨)"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
#--------------------------------------------------------------------
|
|
# 4. Docker Compose 설정
|
|
#--------------------------------------------------------------------
|
|
echo "[4/6] Docker Compose 설정..."
|
|
|
|
if [ ! -f "$PROJECT_DIR/docker-compose.mlx.yml" ]; then
|
|
cat > "$PROJECT_DIR/docker-compose.mlx.yml" << EOF
|
|
services:
|
|
open-webui:
|
|
image: ghcr.io/open-webui/open-webui:main
|
|
container_name: open-webui-mlx
|
|
ports:
|
|
- "${WEBUI_PORT}:8080"
|
|
environment:
|
|
- OPENAI_API_BASE_URL=http://host.docker.internal:${PORT}/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
|
|
echo " ✓ docker-compose.mlx.yml 생성"
|
|
else
|
|
echo " ✓ docker-compose.mlx.yml 이미 존재"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
#--------------------------------------------------------------------
|
|
# 5. Open WebUI 실행
|
|
#--------------------------------------------------------------------
|
|
echo "[5/6] Open WebUI 실행..."
|
|
|
|
cd "$PROJECT_DIR"
|
|
docker compose -f docker-compose.mlx.yml up -d 2>&1 | grep -v "^$"
|
|
echo " ✓ Open WebUI 실행 중 (http://localhost:${WEBUI_PORT})"
|
|
|
|
echo ""
|
|
|
|
MLLM_FLAG="--mllm"
|
|
|
|
#--------------------------------------------------------------------
|
|
# 6. vllm-mlx 서버 시작
|
|
#--------------------------------------------------------------------
|
|
LOG_FILE="$PROJECT_DIR/vllm-mlx.log"
|
|
|
|
echo "[6/6] vllm-mlx 서버 시작 (백그라운드)..."
|
|
echo ""
|
|
echo " 모델: $MODEL"
|
|
echo " 포트: $PORT"
|
|
echo " max_tokens (출력): $MAX_TOKENS"
|
|
echo " max_input (입력): $MAX_INPUT_TOKENS"
|
|
echo " temperature: $TEMPERATURE"
|
|
echo " top_p: $TOP_P"
|
|
echo " 모드: 멀티모달 (이미지 + 텍스트)"
|
|
echo ""
|
|
|
|
# 백그라운드 실행, 로그는 파일로
|
|
MAX_CACHE_BLOCKS=$((MAX_INPUT_TOKENS / 64))
|
|
|
|
nohup vllm-mlx serve "$MODEL" \
|
|
--port "$PORT" \
|
|
--max-tokens "$MAX_TOKENS" \
|
|
--default-temperature "$TEMPERATURE" \
|
|
--default-top-p "$TOP_P" \
|
|
--kv-cache-quantization \
|
|
--use-paged-cache \
|
|
--max-cache-blocks "$MAX_CACHE_BLOCKS" \
|
|
--timeout 600 \
|
|
$MLLM_FLAG > "$LOG_FILE" 2>&1 &
|
|
|
|
SERVER_PID=$!
|
|
echo " 서버 PID: $SERVER_PID"
|
|
echo " 로그 파일: $LOG_FILE"
|
|
echo ""
|
|
|
|
# 서버가 뜰 때까지 대기 (최대 120초, 첫 실행 시 모델 다운로드 포함)
|
|
echo " 서버 준비 대기 중... (첫 실행 시 모델 다운로드 ~20GB)"
|
|
for i in $(seq 1 120); do
|
|
if curl -s http://localhost:$PORT/v1/models > /dev/null 2>&1; then
|
|
echo ""
|
|
echo " ✓ 서버 준비 완료!"
|
|
break
|
|
fi
|
|
# 프로세스가 죽었는지 확인
|
|
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
|
echo ""
|
|
echo " ❌ 서버 시작 실패. 로그를 확인하세요:"
|
|
echo " tail -50 $LOG_FILE"
|
|
exit 1
|
|
fi
|
|
printf "."
|
|
sleep 1
|
|
done
|
|
|
|
# 120초 후에도 안 되면 안내
|
|
if ! curl -s http://localhost:$PORT/v1/models > /dev/null 2>&1; then
|
|
echo ""
|
|
echo " ⚠️ 서버가 아직 준비 중입니다. (모델 다운로드 중일 수 있음)"
|
|
echo " 로그 확인: tail -f $LOG_FILE"
|
|
echo " 준비되면 http://localhost:${WEBUI_PORT} 접속"
|
|
else
|
|
echo ""
|
|
fi
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo " 셋업 완료!"
|
|
echo "============================================"
|
|
echo ""
|
|
echo " 브라우저: http://localhost:${WEBUI_PORT}"
|
|
echo " (첫 접속 시 회원가입 → 첫 계정이 admin)"
|
|
echo ""
|
|
echo " 로그 확인: tail -f $LOG_FILE"
|
|
echo " 종료: ./stop-mlx.sh"
|
|
echo "============================================"
|