|
#!/usr/bin/env bash |
|
# ============================================================================= |
|
# Bonsai 1-bit LLM — Installer & Runner |
|
# Supports: macOS (Apple Silicon + Intel), Linux (CUDA / CPU) |
|
# ============================================================================= |
|
|
|
set -euo pipefail |
|
|
|
# ── Colours ────────────────────────────────────────────────────────────────── |
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' |
|
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' |
|
|
|
info() { echo -e "${CYAN}${BOLD}[INFO]${RESET} $*"; } |
|
success() { echo -e "${GREEN}${BOLD}[OK]${RESET} $*"; } |
|
warn() { echo -e "${YELLOW}${BOLD}[WARN]${RESET} $*"; } |
|
error() { echo -e "${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; } |
|
step() { echo -e "\n${BOLD}━━━ $* ━━━${RESET}"; } |
|
|
|
# ── Defaults ───────────────────────────────────────────────────────────────── |
|
INSTALL_DIR="$HOME/bonsai" |
|
REPO_URL="https://github.com/PrismML-Eng/llama.cpp.git" |
|
MODEL_SIZE="${BONSAI_MODEL:-8B}" # 1.7B | 4B | 8B |
|
SERVER_PORT="${BONSAI_PORT:-8080}" |
|
CTX_SIZE=65536 |
|
GPU_LAYERS=99 |
|
|
|
# ── Banner ──────────────────────────────────────────────────────────────────── |
|
echo -e " |
|
${CYAN}${BOLD} |
|
██████╗ ██████╗ ███╗ ██╗███████╗ █████╗ ██╗ |
|
██╔══██╗██╔═══██╗████╗ ██║██╔════╝██╔══██╗██║ |
|
██████╔╝██║ ██║██╔██╗ ██║███████╗███████║██║ |
|
██╔══██╗██║ ██║██║╚██╗██║╚════██║██╔══██║██║ |
|
██████╔╝╚██████╔╝██║ ╚████║███████║██║ ██║██║ |
|
╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ |
|
${RESET}${BOLD} 1-bit LLM Installer & Runner${RESET} |
|
PrismML • Apache 2.0 • ~1 GB for 8B params |
|
" |
|
|
|
# ── Menu ────────────────────────────────────────────────────────────────────── |
|
echo -e "${BOLD}What would you like to do?${RESET}" |
|
echo " 1) Install + run interactive chat (CLI)" |
|
echo " 2) Install + run as local server with WebUI" |
|
echo " 3) Run interactive chat (already installed)" |
|
echo " 4) Run local server with WebUI (already installed)" |
|
echo " 5) Change model size (current: ${MODEL_SIZE})" |
|
echo " 6) Uninstall" |
|
echo " q) Quit" |
|
echo "" |
|
read -rp "Choice [1]: " CHOICE |
|
CHOICE="${CHOICE:-1}" |
|
|
|
# ── Model size picker ───────────────────────────────────────────────────────── |
|
pick_model_size() { |
|
echo -e "\n${BOLD}Choose model size:${RESET}" |
|
echo " 1) 8B — 1.15 GB (best quality, recommended)" |
|
echo " 2) 4B — 0.5 GB (balanced)" |
|
echo " 3) 1.7B — 0.24 GB (smallest, Raspberry Pi / edge)" |
|
read -rp "Choice [1]: " SZ |
|
case "${SZ:-1}" in |
|
1) MODEL_SIZE="8B" ;; |
|
2) MODEL_SIZE="4B" ;; |
|
3) MODEL_SIZE="1.7B" ;; |
|
*) MODEL_SIZE="8B" ;; |
|
esac |
|
success "Model size set to ${MODEL_SIZE}" |
|
} |
|
|
|
if [[ "$CHOICE" == "5" ]]; then |
|
pick_model_size |
|
# Re-show menu after picking |
|
exec "$0" |
|
fi |
|
|
|
if [[ "$CHOICE" == "6" ]]; then |
|
step "Uninstall" |
|
if [[ -d "$INSTALL_DIR" ]]; then |
|
read -rp "Remove $INSTALL_DIR? [y/N] " CONFIRM |
|
[[ "${CONFIRM,,}" == "y" ]] && rm -rf "$INSTALL_DIR" && success "Removed $INSTALL_DIR" || info "Aborted." |
|
else |
|
warn "Nothing to remove at $INSTALL_DIR" |
|
fi |
|
exit 0 |
|
fi |
|
|
|
if [[ "$CHOICE" == "q" ]]; then exit 0; fi |
|
|
|
# ── Detect platform ─────────────────────────────────────────────────────────── |
|
detect_platform() { |
|
OS="$(uname -s)" |
|
ARCH="$(uname -m)" |
|
|
|
case "$OS" in |
|
Darwin) |
|
PLATFORM="macos" |
|
if [[ "$ARCH" == "arm64" ]]; then |
|
BUILD_FLAGS="" # Metal on by default for Apple Silicon |
|
HW_DESC="macOS Apple Silicon (Metal)" |
|
else |
|
BUILD_FLAGS="-DGGML_METAL=OFF" |
|
HW_DESC="macOS Intel (CPU)" |
|
fi |
|
;; |
|
Linux) |
|
PLATFORM="linux" |
|
if command -v nvcc &>/dev/null || [[ -d /usr/local/cuda ]]; then |
|
BUILD_FLAGS="-DGGML_CUDA=ON" |
|
HW_DESC="Linux + NVIDIA CUDA" |
|
else |
|
BUILD_FLAGS="" |
|
HW_DESC="Linux CPU" |
|
warn "No CUDA detected — building CPU-only (slower). Install CUDA toolkit for GPU acceleration." |
|
fi |
|
;; |
|
*) |
|
error "Unsupported OS: $OS. Use macOS or Linux (for Windows, use WSL2)." |
|
;; |
|
esac |
|
|
|
info "Platform: ${HW_DESC}" |
|
} |
|
|
|
# ── Dependency checks ───────────────────────────────────────────────────────── |
|
check_deps() { |
|
step "Checking dependencies" |
|
local missing=() |
|
|
|
for cmd in git cmake make; do |
|
if ! command -v "$cmd" &>/dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
|
|
if [[ "$PLATFORM" == "macos" ]] && ! xcode-select -p &>/dev/null; then |
|
warn "Xcode Command Line Tools not found. Installing..." |
|
xcode-select --install |
|
read -rp "Press Enter once Xcode CLT installation completes..." |
|
fi |
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then |
|
error "Missing required tools: ${missing[*]}\n\n macOS: brew install ${missing[*]}\n Ubuntu: sudo apt install ${missing[*]}" |
|
fi |
|
|
|
success "All dependencies found" |
|
} |
|
|
|
# ── HuggingFace model URLs ──────────────────────────────────────────────────── |
|
get_model_url() { |
|
case "$MODEL_SIZE" in |
|
8B) echo "https://huggingface.co/prism-ml/Bonsai-8B-gguf/resolve/main/Bonsai-8B-Q1_0.gguf" ;; |
|
4B) echo "https://huggingface.co/prism-ml/Bonsai-4B-gguf/resolve/main/Bonsai-4B-Q1_0.gguf" ;; |
|
1.7B) echo "https://huggingface.co/prism-ml/Bonsai-1.7B-gguf/resolve/main/Bonsai-1.7B-Q1_0.gguf" ;; |
|
*) error "Unknown model size: $MODEL_SIZE" ;; |
|
esac |
|
} |
|
|
|
get_model_filename() { |
|
case "$MODEL_SIZE" in |
|
8B) echo "Bonsai-8B-Q1_0.gguf" ;; |
|
4B) echo "Bonsai-4B-Q1_0.gguf" ;; |
|
1.7B) echo "Bonsai-1.7B-Q1_0.gguf" ;; |
|
esac |
|
} |
|
|
|
# ── Install ─────────────────────────────────────────────────────────────────── |
|
install_bonsai() { |
|
step "Installing Bonsai to $INSTALL_DIR" |
|
mkdir -p "$INSTALL_DIR/models" |
|
|
|
# ── Clone PrismML llama.cpp fork ────────────────────────────────────────── |
|
if [[ -d "$INSTALL_DIR/llama.cpp/.git" ]]; then |
|
info "llama.cpp fork already cloned — pulling latest..." |
|
git -C "$INSTALL_DIR/llama.cpp" pull --ff-only || warn "Could not pull updates (offline?)" |
|
else |
|
info "Cloning PrismML llama.cpp fork (includes Q1_0 1-bit kernels)..." |
|
git clone --depth=1 "$REPO_URL" "$INSTALL_DIR/llama.cpp" |
|
fi |
|
|
|
# ── Build ────────────────────────────────────────────────────────────────── |
|
BINARY="$INSTALL_DIR/llama.cpp/build/bin/llama-cli" |
|
if [[ -f "$BINARY" ]]; then |
|
info "Binaries already built — skipping build step" |
|
else |
|
info "Building llama.cpp (this takes 1–3 minutes)..." |
|
cmake -S "$INSTALL_DIR/llama.cpp" \ |
|
-B "$INSTALL_DIR/llama.cpp/build" \ |
|
-DCMAKE_BUILD_TYPE=Release \ |
|
$BUILD_FLAGS |
|
cmake --build "$INSTALL_DIR/llama.cpp/build" \ |
|
--config Release \ |
|
-j "$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" |
|
success "Build complete" |
|
fi |
|
|
|
# ── Download model ───────────────────────────────────────────────────────── |
|
MODEL_FILE="$(get_model_filename)" |
|
MODEL_PATH="$INSTALL_DIR/models/$MODEL_FILE" |
|
|
|
if [[ -f "$MODEL_PATH" ]]; then |
|
success "Model already downloaded: $MODEL_FILE" |
|
else |
|
MODEL_URL="$(get_model_url)" |
|
info "Downloading $MODEL_FILE from Hugging Face..." |
|
info "URL: $MODEL_URL" |
|
echo "" |
|
|
|
if command -v wget &>/dev/null; then |
|
wget --show-progress -O "$MODEL_PATH" "$MODEL_URL" |
|
elif command -v curl &>/dev/null; then |
|
curl -L --progress-bar -o "$MODEL_PATH" "$MODEL_URL" |
|
else |
|
error "Neither wget nor curl found. Please install one and retry." |
|
fi |
|
|
|
success "Model downloaded: $MODEL_PATH" |
|
fi |
|
} |
|
|
|
# ── Run: interactive CLI ────────────────────────────────────────────────────── |
|
run_chat() { |
|
MODEL_PATH="$INSTALL_DIR/models/$(get_model_filename)" |
|
BINARY="$INSTALL_DIR/llama.cpp/build/bin/llama-cli" |
|
|
|
[[ -f "$BINARY" ]] || error "Binary not found. Run install first (option 1 or 2)." |
|
[[ -f "$MODEL_PATH" ]] || error "Model not found at $MODEL_PATH. Run install first." |
|
|
|
step "Starting Bonsai ${MODEL_SIZE} — Interactive Chat" |
|
echo -e " ${YELLOW}Type your message and press Enter. Ctrl+C to exit.${RESET}\n" |
|
|
|
"$BINARY" \ |
|
-m "$MODEL_PATH" \ |
|
--interactive-first \ |
|
--color \ |
|
-n -1 \ |
|
--temp 0.5 \ |
|
--top-p 0.85 \ |
|
--top-k 20 \ |
|
--repeat-penalty 1.1 \ |
|
-ngl "$GPU_LAYERS" \ |
|
--ctx-size 4096 \ |
|
-p "You are a helpful assistant." |
|
} |
|
|
|
# ── Run: local server + WebUI ───────────────────────────────────────────────── |
|
run_server() { |
|
MODEL_PATH="$INSTALL_DIR/models/$(get_model_filename)" |
|
BINARY="$INSTALL_DIR/llama.cpp/build/bin/llama-server" |
|
|
|
[[ -f "$BINARY" ]] || error "Server binary not found. Run install first (option 1 or 2)." |
|
[[ -f "$MODEL_PATH" ]] || error "Model not found at $MODEL_PATH. Run install first." |
|
|
|
step "Starting Bonsai ${MODEL_SIZE} — Local Server" |
|
echo "" |
|
echo -e " ${GREEN}${BOLD}WebUI:${RESET} http://localhost:${SERVER_PORT}" |
|
echo -e " ${GREEN}${BOLD}API:${RESET} http://localhost:${SERVER_PORT}/v1/chat/completions" |
|
echo -e " ${YELLOW}Ctrl+C to stop the server${RESET}" |
|
echo "" |
|
|
|
# Open browser after a short delay (best-effort) |
|
( |
|
sleep 3 |
|
if command -v open &>/dev/null; then |
|
open "http://localhost:${SERVER_PORT}" |
|
elif command -v xdg-open &>/dev/null; then |
|
xdg-open "http://localhost:${SERVER_PORT}" &>/dev/null |
|
fi |
|
) & |
|
|
|
"$BINARY" \ |
|
-m "$MODEL_PATH" \ |
|
--host 0.0.0.0 \ |
|
--port "$SERVER_PORT" \ |
|
-ngl "$GPU_LAYERS" \ |
|
--ctx-size "$CTX_SIZE" \ |
|
--temp 0.5 \ |
|
--top-p 0.85 \ |
|
--top-k 20 |
|
} |
|
|
|
# ── Main dispatcher ─────────────────────────────────────────────────────────── |
|
detect_platform |
|
check_deps |
|
|
|
case "$CHOICE" in |
|
1) |
|
install_bonsai |
|
echo "" |
|
run_chat |
|
;; |
|
2) |
|
install_bonsai |
|
echo "" |
|
run_server |
|
;; |
|
3) |
|
run_chat |
|
;; |
|
4) |
|
run_server |
|
;; |
|
*) |
|
error "Invalid choice: $CHOICE" |
|
;; |
|
esac |