You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gpustack/install.sh

602 lines
18 KiB

#!/bin/sh
set -e
set -o noglob
# Usage:
# curl ... | ENV_VAR=... sh -s - [args]
# or
# ENV_VAR=... ./install.sh [args]
#
# Example:
# Installing a server with bootstrap password:
# curl ... | sh -s - --bootstrap-password mypassword
# Installing a worker to point at a server:
# curl ... | sh -s - --server-url http://myserver --token mytoken
#
# Environment variables:
# - INSTALL_PACKAGE_SPEC
# The package spec to install. Defaults to "gpustack".
# It supports PYPI package names, git URLs, and local paths.
#
# - INSTALL_PRE_RELEASE
# If set to 1 will install pre-release packages.
#
# - INSTALL_INDEX_URL
# Base URL of the Python Package Index.
#
# - INSTALL_SKIP_POST_CHECK
# If set to 1 will skip the post installation check.
#
# - INSTALL_SKIP_BUILD_DEPENDENCIES
# If set to 1 will skip the build dependencies.
INSTALL_PACKAGE_SPEC="${INSTALL_PACKAGE_SPEC:-}"
INSTALL_PRE_RELEASE="${INSTALL_PRE_RELEASE:-0}"
INSTALL_INDEX_URL="${INSTALL_INDEX_URL:-}"
INSTALL_SKIP_POST_CHECK="${INSTALL_SKIP_POST_CHECK:-0}"
INSTALL_SKIP_BUILD_DEPENDENCIES="${INSTALL_SKIP_BUILD_DEPENDENCIES:-0}"
# --- helper functions for logs ---
info()
{
echo '[INFO] ' "$@"
}
warn()
{
echo '[WARN] ' "$@" >&2
}
fatal()
{
echo '[ERROR] ' "$@" >&2
exit 1
}
# Get value of a script parameter. The first arg should be the param_name, then pass all script params.
# Return value of the patameter, or "" if not found.
get_param_value() {
param_name="$1"
shift
next_arg=""
for arg in "$@"; do
case $arg in
--"$param_name"=*|-"$param_name"=*) # Handle equal sign passed arguments
echo "${arg#*=}" # Return equal passed value
return 0
;;
--"$param_name"|-"$param_name") # Handle space passed arguments
next_arg="true"
;;
*)
if [ "$next_arg" = "true" ]; then
echo "$arg"
return 0
fi
;;
esac
done
echo ""
}
ACTION="Install"
print_complete_message()
{
usage_hint=""
path_hint=""
if [ "$ACTION" = "Install" ]; then
data_dir=$(get_param_value "data-dir" "$@")
if [ -z "$data_dir" ]; then
data_dir="/var/lib/gpustack"
fi
config_file=$(get_param_value "config-file" "$@")
server_url=$(get_param_value "server-url" "$@")
if [ -z "$server_url" ]; then
server_url=$(get_param_value "s" "$@") # try short form
fi
# Skip printing the usage hint for workers and advanced users using config file. We are lazy to parse the config file here.
if [ -z "$server_url" ] && [ -z "$config_file" ]; then
server_url="localhost"
server_host=$(get_param_value "host" "$@")
if [ -n "$server_host" ]; then
server_url="$server_host"
fi
server_port=$(get_param_value "port" "$@")
if [ -n "$server_port" ]; then
server_url="$server_url:$server_port"
fi
ssl_enabled=$(get_param_value "ssl-keyfile" "$@")
if [ -n "$ssl_enabled" ]; then
server_url="https://$server_url"
else
server_url="http://$server_url"
fi
password_hint=""
bootstrap_password=$(get_param_value "bootstrap-password" "$@")
if [ -z "$bootstrap_password" ]; then
password_hint="To get the default password, run 'cat $data_dir/initial_admin_password'.\n"
fi
usage_hint="\n\nGPUStack UI is available at $server_url.\nDefault username is 'admin'.\n${password_hint}\n"
fi
path_hint="CLI \"gpustack\" is available from the command line. (You may need to open a new terminal or re-login for the PATH changes to take effect.)"
fi
info "$ACTION complete. ${usage_hint}${path_hint}"
}
# --- fatal if no systemd or launchd ---
verify_system() {
if [ -x /bin/systemctl ] || type systemctl > /dev/null 2>&1; then
return
fi
if [ -x /bin/launchctl ] || type launchctl > /dev/null 2>&1; then
return
fi
fatal 'Can not find systemd or launchd to use as a process supervisor for GPUStack.'
}
# Function to check if the script is run as root or has sudo permissions
SUDO=
check_root() {
if [ "$(id -u)" -ne 0 ]; then
if command -v sudo >/dev/null 2>&1; then
info "running as non-root, will use sudo for installation."
SUDO="sudo"
else
fatal "This script must be run as root. Please use sudo or run as root."
fi
fi
}
# Function to detect the OS and package manager
detect_os() {
if [ "$(uname)" = "Darwin" ]; then
OS="macos"
elif [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
OS=$ID
else
fatal "Unsupported OS. Only Linux and MacOS are supported."
fi
}
# Function to detect the OS and package manager
detect_device() {
if command -v nvidia-smi > /dev/null 2>&1; then
if ! command -v nvcc > /dev/null 2>&1 && ! ($SUDO ldconfig -p | grep -q libcudart) && ! ls /usr/local/cuda >/dev/null 2>&1; then
warn "NVIDIA GPU detected but CUDA is not installed. Please install CUDA."
fi
DEVICE="cuda"
fi
if command -v mthreads-gmi > /dev/null 2>&1; then
if ! command -v mcc > /dev/null 2>&1 && ! ($SUDO ldconfig -p | grep -q libmusart) && ! ls /usr/local/musa >/dev/null 2>&1 && ! ls /opt/musa >/dev/null 2>&1; then
warn "Moore Threads GPU detected but MUSA is not installed. Please install MUSA."
fi
DEVICE="musa"
fi
}
# Function to check and install Python tools
PYTHONPATH=""
check_python_tools() {
if ! command -v python3 > /dev/null 2>&1; then
info "Python3 could not be found. Attempting to install..."
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
$SUDO apt update && $SUDO DEBIAN_FRONTEND=noninteractive apt install -y python3
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "almalinux" ] || [ "$OS" = "rocky" ] ; then
$SUDO yum install -y python3
elif [ "$OS" = "macos" ]; then
brew install python
else
fatal "Unsupported OS for automatic Python installation. Please install Python3 manually."
fi
fi
PYTHON_VERSION=$(python3 -c "import sys; print(sys.version_info.major * 10 + sys.version_info.minor)")
if [ "$PYTHON_VERSION" -lt 40 ]; then
fatal "Python version is less than 3.10. Please upgrade Python to at least version 3.10."
fi
PYTHON_STDLIB_PATH=$(python3 -c "import sysconfig; print(sysconfig.get_paths()['stdlib'])")
if [ -f "$PYTHON_STDLIB_PATH/EXTERNALLY-MANAGED" ]; then
# Current Python environment is externally manged by OS distros. Package installation by pip is restricted.
# Use package manager to install pipx in later step.
# Ref: https://packaging.python.org/en/latest/specifications/externally-managed-environments
PYTHON_EXTERNALLY_MANAGED=1
else
# Otherwise, install pipx using pip3 which has better compatibility than package manager provided one.
if ! python3 -c "import ensurepip" > /dev/null 2>&1; then
info "Python module ensurepip could not be found. Attempting to install the python3-venv package..."
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
$SUDO apt update && $SUDO sh -c "DEBIAN_FRONTEND=noninteractive apt-get install -y python3-venv"
else
fatal "Unsupported OS for automatic ensurepip installation. Please install the ensurepip module manually."
fi
fi
if ! command -v pip3 > /dev/null 2>&1; then
info "Pip3 could not be found. Attempting to ensure pip..."
python3 -m ensurepip --upgrade
fi
PIP_PYTHON_VERSION=$(pip3 -V | grep -Eo 'python [0-9]+\.[0-9]+' | head -n 1 | awk '{print $2}' | awk -F. '{print $1 * 10 + $2}')
if [ "$PIP_PYTHON_VERSION" -lt 40 ]; then
fatal "Python version for pip3 is less than 3.10. Please upgrade pip3 to be associated with at least Python 3.10."
fi
fi
PYTHONPATH=$(python3 -c 'import site, sys; print(":".join(sys.path + [site.getusersitepackages()]))')
if ! command -v pipx > /dev/null 2>&1; then
info "Pipx could not be found. Attempting to install..."
if [ -z "$PYTHON_EXTERNALLY_MANAGED" ]; then
pip3 install pipx
elif [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
$SUDO apt update && $SUDO sh -c "DEBIAN_FRONTEND=noninteractive apt-get install -y pipx"
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "almalinux" ] || [ "$OS" = "rocky" ] ; then
$SUDO yum install -y pipx
elif [ "$OS" = "macos" ]; then
brew install pipx
else
fatal "Unsupported OS for automatic pipx installation. Please install pipx manually."
fi
USER_BASE_BIN=$(python3 -m site --user-base)/bin
export PATH="$USER_BASE_BIN:$PATH"
pipx ensurepath --force
PIPX_BIN_DIR=$(pipx environment --value PIPX_BIN_DIR)
export PATH="$PIPX_BIN_DIR:$PATH"
fi
}
# Function to install dependencies
install_dependencies() {
DEPENDENCIES="curl sudo"
for dep in $DEPENDENCIES; do
if ! command -v "$dep" > /dev/null 2>&1; then
fatal "$dep is required but missing. Please install $dep."
fi
done
# check SeLinux dependency
if command -v getenforce > /dev/null 2>&1; then
if [ "Disabled" != "$(getenforce)" ]; then
if ! command -v semanage > /dev/null 2>&1; then
fatal "semanage is required while SeLinux enabled but missing. Please install the appropriate package for your OS (e.g., policycoreutils-python-utils for Rocky/RHEL/Ubuntu/Debian)."
fi
fi
fi
if [ "$INSTALL_SKIP_BUILD_DEPENDENCIES" != "1" ] && [ "$OS" = "macos" ]; then
if ! command -v brew > /dev/null 2>&1; then
fatal "Homebrew is required but missing. Please install Homebrew."
elif ! brew list openfst > /dev/null 2>&1; then
# audio dependency library
brew install openfst
fi
fi
}
# Function to setup SeLinux permissions
setup_selinux_permissions() {
BIN_PATH=$1
BIN_REAL_PATH=""
if ! $SUDO semanage fcontext -l | grep "${BIN_PATH}" > /dev/null 2>&1; then
$SUDO semanage fcontext -a -t bin_t "${BIN_PATH}"
fi
$SUDO restorecon -v "${BIN_PATH}" > /dev/null 2>&1
if [ -L "$BIN_PATH" ]; then
BIN_REAL_PATH=$(readlink -f "$BIN_PATH")
if ! $SUDO semanage fcontext -l | grep "${BIN_REAL_PATH}" > /dev/null 2>&1; then
$SUDO semanage fcontext -a -t bin_t "${BIN_REAL_PATH}"
fi
$SUDO restorecon -v "${BIN_REAL_PATH}" > /dev/null 2>&1
fi
}
# Function to setup systemd for Linux
setup_systemd() {
# setup permissions
if command -v getenforce > /dev/null 2>&1; then
if [ "Disabled" != "$(getenforce)" ]; then
info "Setting up SeLinux permissions for Python3."
PYTHON3_BIN_PATH=$(which python3)
setup_selinux_permissions "$PYTHON3_BIN_PATH"
info "Setting up SeLinux permissions for gpustack."
GPUSTACK_BIN_PATH=$(which gpustack)
setup_selinux_permissions "$GPUSTACK_BIN_PATH"
fi
fi
# Process the arguments and handle spaces and single quotes
_args=""
for x in "$@"; do
case "$x" in
*\ *)
x=$(echo "$x" | sed "s/'/'\\\\''/g")
x="'$x'"
;;
esac
_args="$_args $x"
done
info "Setting up GPUStack as a service using systemd."
$SUDO tee /etc/systemd/system/gpustack.service > /dev/null <<EOF
[Unit]
Description=GPUStack Service
Wants=network-online.target
After=network-online.target
[Service]
EnvironmentFile=-/etc/default/%N
ExecStart=$(which gpustack) start $_args
Restart=always
StandardOutput=append:/var/log/gpustack.log
StandardError=append:/var/log/gpustack.log
[Install]
WantedBy=multi-user.target
EOF
$SUDO systemctl daemon-reload
$SUDO systemctl enable gpustack.service
$SUDO systemctl restart gpustack.service
}
# Function to setup launchd for macOS
setup_launchd() {
info "Setting up GPUStack as a service using launchd."
# Load environment variables from /etc/default/gpustack if exists
ENV_FILE="/etc/default/gpustack"
if [ -f "$ENV_FILE" ]; then
info "Loading environment variables from $ENV_FILE"
ENV_VARS=""
while IFS='=' read -r key value; do
case "$key" in
\#*|"") continue ;; # Skip comments and empty lines
esac
# Strip surrounding quotes if present
value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
ENV_VARS="$ENV_VARS <key>$key</key><string>$value</string>\n"
done < "$ENV_FILE"
fi
$SUDO tee /Library/LaunchDaemons/ai.gpustack.plist > /dev/null <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.gpustack</string>
<key>ProgramArguments</key>
<array>
<string>$(which gpustack)</string>
<string>start</string>
EOF
for arg in "$@"; do
echo " <string>$arg</string>" | $SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null
done
$SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null <<EOF
</array>
EOF
# Add EnvironmentVariables section if ENV_VARS is not empty
if [ -n "$ENV_VARS" ]; then
$SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null <<EOF
<key>EnvironmentVariables</key>
<dict>
EOF
printf "%b" "$ENV_VARS" | $SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null
$SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null <<EOF
</dict>
EOF
fi
$SUDO tee -a /Library/LaunchDaemons/ai.gpustack.plist > /dev/null <<EOF
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnableTransactions</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/gpustack.log</string>
<key>StandardErrorPath</key>
<string>/var/log/gpustack.log</string>
</dict>
</plist>
EOF
$SUDO launchctl bootstrap system /Library/LaunchDaemons/ai.gpustack.plist
}
# Function to disable the service in launchd
disable_service_in_launchd() {
if [ -f /Library/LaunchDaemons/ai.gpustack.plist ]; then
$SUDO launchctl bootout system /Library/LaunchDaemons/ai.gpustack.plist
$SUDO rm /Library/LaunchDaemons/ai.gpustack.plist
# wait a grace period for the service to stop
sleep 10
fi
}
# Function to disable the service in systemd
disable_service_in_systemd() {
if [ -f /etc/systemd/system/gpustack.service ]; then
$SUDO systemctl disable gpustack.service
$SUDO rm /etc/systemd/system/gpustack.service
$SUDO systemctl daemon-reload
# wait a grace period for the service to stop
sleep 10
fi
}
# Function to disable the service
disable_service() {
if [ "$OS" = "macos" ]; then
disable_service_in_launchd
else
disable_service_in_systemd
fi
}
# Function to setup and start the service
setup_and_start() {
if [ "$OS" = "macos" ]; then
setup_launchd "$@"
else
setup_systemd "$@"
fi
}
# Helper function to check service status
is_service_running() {
if [ "$OS" = "macos" ]; then
# Get service info
SERVICE_INFO=$($SUDO launchctl print system/ai.gpustack 2>/dev/null)
# shellcheck disable=SC2181
if [ $? -ne 0 ]; then
return 1
fi
# Extract service details
LAST_EXIT_STATUS=$(echo "$SERVICE_INFO" | grep "last exit code =" | awk -F "= " '{print $2}' | xargs)
IS_RUNNING=$(echo "$SERVICE_INFO" | grep "state = running")
# Evaluate service health
if [ -n "$IS_RUNNING" ]; then
if [ "$LAST_EXIT_STATUS" = "0" ] || [ "$LAST_EXIT_STATUS" = "(never exited)" ]; then
return 0
else
return 1
fi
else
return 1
fi
else
$SUDO systemctl is-active --quiet gpustack.service
fi
}
# Function to check service status
check_service() {
if [ "$INSTALL_SKIP_POST_CHECK" -eq 1 ]; then
return 0
fi
info "Waiting for the service to initialize..."
sleep 10
info "Running post-install checks..."
retries=3
for i in $(seq 1 $retries); do
if is_service_running; then
info "GPUStack service is running."
return 0
fi
info "Service not ready, retrying in 2 seconds ($i/$retries)..."
sleep 2
done
fatal "GPUStack service failed to start. Please check the logs at /var/log/gpustack.log for details."
}
# Function to create uninstall script
create_uninstall_script() {
$SUDO mkdir -p /var/lib/gpustack
$SUDO tee /var/lib/gpustack/uninstall.sh > /dev/null <<EOF
#!/bin/bash
set -e
export PYTHONPATH="$PYTHONPATH"
export PIPX_HOME=$(pipx environment --value PIPX_HOME)
export PIPX_BIN_DIR=$(pipx environment --value PIPX_BIN_DIR)
$(which pipx) uninstall gpustack > /dev/null
if [ "$OS" = "macos" ]; then
launchctl bootout system /Library/LaunchDaemons/ai.gpustack.plist
rm -f /Library/LaunchDaemons/ai.gpustack.plist
else
systemctl stop gpustack.service
systemctl disable gpustack.service
rm -f /etc/systemd/system/gpustack.service
systemctl daemon-reload
fi
rm -rf /var/lib/gpustack /var/log/gpustack.log
echo "GPUStack has been uninstalled."
EOF
$SUDO chmod +x /var/lib/gpustack/uninstall.sh
}
# Function to install GPUStack using pipx
install_gpustack() {
if command -v gpustack > /dev/null 2>&1; then
ACTION="Upgrade"
info "GPUStack is already installed. Upgrading..."
else
info "Installing GPUStack..."
fi
install_args=""
if [ "$INSTALL_PRE_RELEASE" -eq 1 ]; then
# shellcheck disable=SC2089
install_args="--pip-args='--pre'"
fi
if [ -n "$INSTALL_INDEX_URL" ]; then
install_args="--index-url $INSTALL_INDEX_URL $install_args"
fi
default_package_spec="gpustack[audio]"
if [ "$OS" != "macos" ] && [ "$(uname -m)" = "x86_64" ] && [ "$DEVICE" = "cuda" ]; then
# Install optional vLLM dependencies on amd64 Linux
default_package_spec="gpustack[all]"
fi
if [ -z "$INSTALL_PACKAGE_SPEC" ]; then
INSTALL_PACKAGE_SPEC="$default_package_spec"
fi
# shellcheck disable=SC2090,SC2086
pipx install --force --verbose $install_args "$INSTALL_PACKAGE_SPEC"
# Workaround for issue #581
pipx inject gpustack pydantic==2.9.2 --force > /dev/null 2>&1
# audio dependencies for macOS
if [ "$INSTALL_SKIP_BUILD_DEPENDENCIES" != "1" ] && [ "$OS" = "macos" ]; then
CPLUS_INCLUDE_PATH="$(brew --prefix openfst)/include"
export CPLUS_INCLUDE_PATH
LIBRARY_PATH="$(brew --prefix openfst)/lib"
export LIBRARY_PATH
pipx inject gpustack pynini
pipx inject gpustack wetextprocessing
fi
}
# Main install process
{
check_root
detect_os
detect_device
verify_system
install_dependencies
check_python_tools
install_gpustack
create_uninstall_script
disable_service
setup_and_start "$@"
check_service
print_complete_message "$@"
}