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