AI

다중 GPU ollama 관리 스크립트

Gadget 2026. 4. 30. 17:10

 

gemini 등을 활용하여 관리 스크립트를 만들었습니다.

계속 개선하다보니 꽤 쓸만합니다.

 

# ollama start 0  : gpu0 시작

# ollama start : gpu 전체 시작

# ollama stop 1   : gpu1 중지

# ollama stop : gpu 전체 중지

# ollama status 1 : 상태 1초마다 갱신 (어떤 모델이 떠있는 지 확인)

#!/bin/bash

# ======================================================================
# Ollama Cluster Management
# ======================================================================

# 1. 환경 설정
export OLLAMA_BIN="${OLLAMA_BIN:-./bin/ollama}"
P0="11434"; G0="0"; PID0="/tmp/ollama_gpu0.pid"; LOG0="/tmp/ollama_gpu0.log"
P1="11435"; G1="1"; PID1="/tmp/ollama_gpu1.pid"; LOG1="/tmp/ollama_gpu1.log"

# Ollama 최적화 변수
export OLLAMA_FLASH_ATTENTION=1
export OLLAMA_NUM_PARALLEL=6
export OLLAMA_MAX_LOADED_MODELS=2
export OLLAMA_KEEP_ALIVE=-1
export OLLAMA_MODELS="/app/ollama/models"

# UI 및 제어 코드 (ANSI)
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'
CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m'
GOTO_TOP='\033[H'; ERASE_TO_END='\033[J'
HIDE_CURSOR='\033[?25l'; SHOW_CURSOR='\033[?25h'

# 2. 핵심 관리 함수
check_bin() {
    if [ ! -f "$OLLAMA_BIN" ]; then
        echo -e "${RED}Error: Ollama binary not found at $OLLAMA_BIN${NC}"
        exit 1
    fi
}

start_instances() {
    check_bin
    TARGET=${1:-all}
    for i in 0 1; do
        if [ "$TARGET" == "all" ] || [ "$TARGET" == "$i" ]; then
            PORT_VAR="P$i"; GPU_VAR="G$i"; PID_FILE="PID$i"; LOG_FILE="LOG$i"
            if [ -f "${!PID_FILE}" ] && kill -0 $(cat "${!PID_FILE}") 2>/dev/null; then
                echo -e "${YELLOW}GPU $i is already running (PID: $(cat "${!PID_FILE}"))${NC}"
            else
                echo -e "${GREEN}[+] Launching GPU $i on Port ${!PORT_VAR}...${NC}"
                CUDA_VISIBLE_DEVICES="${!GPU_VAR}" OLLAMA_HOST="0.0.0.0:${!PORT_VAR}" \
                nohup "$OLLAMA_BIN" serve > "${!LOG_FILE}" 2>&1 &
                echo $! > "${!PID_FILE}"
            fi
        fi
    done
}

stop_instances() {
    TARGET=${1:-all}
    for i in 0 1; do
        if [ "$TARGET" == "all" ] || [ "$TARGET" == "$i" ]; then
            PID_FILE="PID$i"
            if [ -f "${!PID_FILE}" ]; then
                PID=$(cat "${!PID_FILE}")
                echo -e "${RED}[!] Stopping GPU $i (PID: $PID)...${NC}"
                kill -15 $PID 2>/dev/null
                rm -f "${!PID_FILE}"
            fi
        fi
    done
}

# 3. 모니터링 출력 함수 (더블 버퍼링 적용)
print_status() {
    local BUFFER=""
    
    # 1. 커서를 맨 위로 보내고, 이전 출력물의 잔상을 아래로 밀면서 지우기 시작
    BUFFER+="${GOTO_TOP}${ERASE_TO_END}"
    
    BUFFER+="${BLUE}================================================================${NC}\n"
    BUFFER+="    Ollama Cluster Status | $(date '+%Y-%m-%d %H:%M:%S')      \n"
    BUFFER+="${BLUE}================================================================${NC}\n"

    # GPU 섹션
    BUFFER+="${YELLOW}[1. GPU Hardware Metrics]${NC}\n"
    GPU_INFO=$(nvidia-smi --query-gpu=index,name,utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits | \
    awk -F', ' -v g="$GREEN" -v y="$YELLOW" -v r="$RED" -v nc="$NC" '
    {
        gu = $3 + 0; vu = $4 + 0; vt = $5 + 0; vp = (vu/vt)*100;
        gc = (gu >= 70 ? r : (gu >= 30 ? y : g));
        vc = (vp >= 85 ? r : (vp >= 60 ? y : g));
        gb=""; gf=int(gu/10); for(i=1;i<=10;i++) gb=(i<=gf ? gb "#" : gb " ");
        vb=""; vf=int(vp/10); for(i=1;i<=10;i++) vb=(i<=vf ? vb "#" : vb " ");
        # 행 끝에 충분한 공백을 주어 이전 잔상을 덮어씀
        printf "GPU %s [%-10s] | GPU: %s[%s] %3d%%%s | VRAM: %s[%s] %3d%%%s (%d/%dMB)          \n", 
               $1, $2, gc, gb, gu, nc, vc, vb, vp, nc, vu, vt
    }')
    BUFFER+="${GPU_INFO}\n"

    # 인프라 섹션
    BUFFER+="\n${YELLOW}[2. Infrastructure (Serve) Details]${NC}\n"
    BUFFER+=$(printf "%-10s %-8s %-20s %-10s %-15s" "TARGET" "PID" "START" "ELAPSED" "RESPONSE")
    BUFFER+="\n----------------------------------------------------------------\n"
    
    BIN_NAME=$(basename "$OLLAMA_BIN")
    for i in 0 1; do
        PID_FILE="PID$i"; PORT_VAR="P$i"
        if [ -f "${!PID_FILE}" ] && kill -0 $(cat "${!PID_FILE}") 2>/dev/null; then
            PID=$(cat "${!PID_FILE}")
            INFO=$(ps -p $PID -o lstart,etime --no-headers)
            START_TIME=$(echo $INFO | awk '{print $2" "$3" "$4" "$5}')
            ELAPSED=$(echo $INFO | awk '{print $NF}')
            RT=$(curl -s -o /dev/null -w "%{time_total}" --connect-timeout 1 http://127.0.0.1:${!PORT_VAR}/api/tags)
            R_COLOR="${GREEN}"; RESP="${RT}s"
            [[ "$RT" == "0.000" ]] && R_COLOR="${RED}" && RESP="OFF/TIMEOUT"
            BUFFER+=$(printf "${GREEN}%-10s${NC} %-8s %-20s %-10s ${R_COLOR}%-15s${NC}" "GPU $i" "$PID" "$START_TIME" "$ELAPSED" "$RESP")
            BUFFER+="\n"
        else
            # OFF 상태일 때도 줄 길이를 맞춰서 잔상을 지움
            BUFFER+=$(printf "${RED}%-10s${NC} %-8s %-20s %-10s %-15s" "GPU $i" "OFF" "-" "-" "-")
            BUFFER+="\n"
        fi
    done

    # 활성 프로세스 섹션 (수정된 부분)
    BUFFER+="\n${YELLOW}[3. Active User Processes (Client)]${NC}\n"
    UPROCS=$(ps -ef | grep "$BIN_NAME" | grep -v "serve" | grep -v "grep" | grep -v "$(basename "$0")")
    if [ -z "$UPROCS" ]; then
        BUFFER+="No active user-initiated processes found.                        \n"
    else
        BUFFER+="${CYAN}  PID      USER     START    MODEL                          PORT${NC}\n"
        
        # 다중 프로세스일 경우를 대비해 while read로 한 줄씩 파싱
        while read -r line; do
            PID=$(echo "$line" | awk '{print $2}')
            USER_NAME=$(echo "$line" | awk '{print $1}')
            START_TIME=$(echo "$line" | awk '{print $5}')
            
            # 1. 포트 추출 (--port 45501)
            PORT=$(echo "$line" | sed -n 's/.*--port \([0-9]*\).*/\1/p')
            [[ -z "$PORT" ]] && PORT="-"
            
            # 2. sha256 해시값 추출 (64자리)
            HASH=$(echo "$line" | sed -n 's/.*sha256-\([a-f0-9]\{64\}\).*/\1/p')
            
            MODEL_NAME="unknown"
            if [ -n "$HASH" ]; then
                # manifests 하위에서 해시값을 포함하는 파일 찾기 (-l 옵션으로 파일명만 추출하여 부하 최소화)
                MANIFEST_FILE=$(grep -rl "$HASH" "$OLLAMA_MODELS/manifests/registry.ollama.ai" 2>/dev/null | head -n 1)
                
                if [ -n "$MANIFEST_FILE" ]; then
                    # 경로의 마지막 두 요소(모델/태그)만 추출 (예: qwen3.6/35b)
                    MODEL_NAME=$(echo "$MANIFEST_FILE" | awk -F'/' '{print $(NF-1)"/"$NF}')
                else
                    # 모델을 못 찾으면 해시 앞 8자리 출력
                    MODEL_NAME="blob:${HASH:0:8}"
                fi
            fi
            
            # 출력 템플릿에 맞춰 잔상 제거용 공백 포함 출력
            BUFFER+=$(printf "  %-8s %-8s %-8s %-30s %-8s              " "$PID" "$USER_NAME" "$START_TIME" "$MODEL_NAME" "$PORT")
            BUFFER+="\n"
        done <<< "$UPROCS"
    fi

    # 네트워크 섹션
    BUFFER+="\n${YELLOW}[4. Network Listen Status]${NC}\n"
    for i in 0 1; do
        PV="P$i"
        L_STAT=$(ss -lntp "sport = :${!PV}" | grep -q "$BIN_NAME" && echo -e "${GREEN}LISTEN${NC}" || echo -e "${RED}CLOSED${NC}")
        BUFFER+="Port ${!PV}: ${L_STAT}                                \n"
    done
    BUFFER+="${BLUE}================================================================${NC}\n"
    
    # 2. 마지막으로 한 번 더 아래쪽 잔상을 확실히 제거
    BUFFER+="${ERASE_TO_END}"
    
    # 버퍼를 %b로 출력하여 이스케이프 문자 해석
    printf "%b" "$BUFFER"
}

status_instances() {
    INTERVAL=$1
    if [ -z "$INTERVAL" ]; then
        print_status
    else
        clear
        printf "${HIDE_CURSOR}"
        trap "printf '${SHOW_CURSOR}'; exit" INT TERM
        while true; do
            print_status
            echo -e "${CYAN}Refresh: ${INTERVAL}s. Press [CTRL+C] to exit.${NC}"
            sleep "$INTERVAL"
        done
    fi
}

model_manage() {
    check_bin
    TARGET_GPU=$1; shift
    if [[ "$TARGET_GPU" != "0" && "$TARGET_GPU" != "1" ]]; then
        echo -e "${RED}Error: Use GPU 0 or 1.${NC}"; exit 1
    fi
    PORT_VAR="P$TARGET_GPU"
    OLLAMA_HOST="127.0.0.1:${!PORT_VAR}" "$OLLAMA_BIN" "$@"
}

# 4. 엔트리 포인트 (Case문)
case "$1" in
    start)  start_instances "$2" ;;
    stop)   stop_instances "$2" ;;
    status) status_instances "$2" ;;
    model)  shift; model_manage "$@" ;;
    *)      echo -e "${YELLOW}Usage: $0 {start|stop|status [sec]|model [0|1] cmd}${NC}"; exit 1 ;;
esac