feat: Add simple 2-cell Colab notebook (no tunnel needed)

- Create studio/backend/colab.py using Colab's built-in proxy
- Uses google.colab.kernel.proxyPort() for URL (no cloudflare)
- Shows nice clickable link with IPython.display.HTML
- Notebook has just 2 cells: setup and start
- Much simpler than external tunneling approach
This commit is contained in:
Leo Borcherding 2026-02-17 04:57:30 -06:00
commit 17df5bf3bf
2 changed files with 674 additions and 0 deletions

584
Unsloth_Studio_Colab.ipynb Normal file
View file

@ -0,0 +1,584 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "27e68f91",
"metadata": {},
"outputs": [],
"source": [
"# ===========================================\n",
"# Setup: Clone repo and build frontend\n",
"# ===========================================\n",
"\n",
"# Clone repository\n",
"!git clone https://github.com/unslothai/new-ui-prototype.git\n",
"%cd /content/new-ui-prototype\n",
"\n",
"# Install Node.js 20.x\n",
"!curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - > /dev/null 2>&1\n",
"!sudo apt-get install -y nodejs > /dev/null 2>&1\n",
"print(\"✅ Node.js installed\")\n",
"\n",
"# Build frontend\n",
"%cd studio/frontend\n",
"!npm install --silent\n",
"!npm run build\n",
"print(\"✅ Frontend built\")\n",
"\n",
"# Install Python dependencies\n",
"%cd /content/new-ui-prototype\n",
"!pip install -q unsloth\n",
"!pip install -q -r studio/backend/requirements.txt\n",
"!pip install -q huggingface_hub datasets python-jose[cryptography] passlib[bcrypt] python-multipart matplotlib pandas ujson\n",
"print(\"✅ Python dependencies installed\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "277e431e",
"metadata": {},
"outputs": [],
"source": [
"# ===========================================\n",
"# Start Unsloth Studio\n",
"# ===========================================\n",
"import sys\n",
"sys.path.insert(0, '/content/new-ui-prototype/studio/backend')\n",
"\n",
"from colab import start\n",
"start()"
]
},
{
"cell_type": "markdown",
"id": "c98b28eb",
"metadata": {},
"source": [
"<div align=\"center\">\n",
" <img src=\"https://raw.githubusercontent.com/unslothai/unsloth/main/images/unsloth%20logo%20white%20text.png\" width=\"400\"/>\n",
"</div>\n",
"\n",
"# 🦥 Unsloth Studio on Google Colab\n",
"\n",
"A modern, full-stack web interface for fine-tuning, managing, and chatting with large language models.\n",
"\n",
"**Features:**\n",
"- 🎯 **Training**: LoRA/QLoRA fine-tuning with real-time progress streaming\n",
"- 🤖 **Model Management**: Browse and load Hugging Face models\n",
"- 💬 **Inference**: Interactive chat playground\n",
"- 📊 **Dataset Tools**: Upload and preview datasets\n",
"- 🚀 **Export**: Push trained adapters to Hugging Face Hub\n",
"\n",
"---\n",
"\n",
"**⚠️ Important Notes:**\n",
"- **Private Repository**: You'll need a GitHub Personal Access Token to clone\n",
"- Use a **GPU runtime** for training (Runtime → Change runtime type → T4 GPU)\n",
"- The notebook will expose the UI via **Cloudflare Tunnel** (no account needed)\n",
"- Your first launch will generate a **setup token** for creating an admin account\n",
"\n",
"---\n",
"\n",
"**Repository**: [github.com/unslothai/new-ui-prototype](https://github.com/unslothai/new-ui-prototype/tree/nightly)"
]
},
{
"cell_type": "markdown",
"id": "9a0cb9d9",
"metadata": {},
"source": [
"## 📋 Step 1: Install System Dependencies\n",
"\n",
"Install Node.js and required system packages."
]
},
{
"cell_type": "markdown",
"id": "0b08e19f",
"metadata": {},
"source": [
"## 🔐 Step 1.5: Authenticate with GitHub (Private Repo)\n",
"\n",
"Since this is a private repository, you need to authenticate with GitHub. \n",
"\n",
"**Get a Personal Access Token (classic):**\n",
"1. Go to https://github.com/settings/tokens\n",
"2. Click \"Generate new token (classic)\"\n",
"3. Give it `repo` scope\n",
"4. Copy the token\n",
"\n",
"**Or use GitHub CLI:**"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a16a1ed9",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"# Option 1: Use GitHub Personal Access Token\n",
"print(\"🔐 GitHub Authentication Required (Private Repo)\")\n",
"print(\"=\" * 60)\n",
"print(\"Get a token from: https://github.com/settings/tokens\")\n",
"print(\"Required scope: 'repo'\")\n",
"print(\"=\" * 60)\n",
"\n",
"github_token = getpass(\"Enter your GitHub Personal Access Token: \")\n",
"\n",
"if github_token:\n",
" # Store token for git operations\n",
" os.environ['GITHUB_TOKEN'] = github_token\n",
" print(\"✅ Token stored (will be used for cloning)\")\n",
"else:\n",
" print(\"⚠️ No token provided - clone may fail for private repo\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4b4ad8e1",
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"# Install Node.js 20.x\n",
"echo \"📦 Installing Node.js...\"\n",
"curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\n",
"sudo apt-get install -y nodejs\n",
"\n",
"# Verify installation\n",
"echo \"✅ Node.js $(node -v) installed\"\n",
"echo \"✅ npm $(npm -v) installed\""
]
},
{
"cell_type": "markdown",
"id": "7065daf9",
"metadata": {},
"source": [
"## 📥 Step 2: Clone Repository and Install Python Dependencies\n",
"\n",
"Clone the repository and install Unsloth + backend dependencies."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6c563597",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from pathlib import Path\n",
"\n",
"# Clone repository (use nightly branch)\n",
"repo_path = Path(\"/content/new-ui-prototype\")\n",
"if not repo_path.exists():\n",
" print(\"📥 Cloning repository...\")\n",
" \n",
" # Use token if available (for private repo)\n",
" github_token = os.environ.get('GITHUB_TOKEN', '')\n",
" if github_token:\n",
" # Clone with token embedded in URL\n",
" repo_url = f\"https://{github_token}@github.com/unslothai/new-ui-prototype.git\"\n",
" !git clone -b nightly {repo_url}\n",
" else:\n",
" # Try without token (will work for public repo)\n",
" !git clone -b nightly https://github.com/unslothai/new-ui-prototype.git\n",
"else:\n",
" print(\"✅ Repository already cloned\")\n",
"\n",
"# Change to repo directory\n",
"os.chdir(repo_path)\n",
"print(f\"📂 Working directory: {os.getcwd()}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1266536b",
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"# Install Unsloth\n",
"echo \"📦 Installing Unsloth...\"\n",
"pip install --no-cache-dir unsloth\n",
"\n",
"# Install backend dependencies\n",
"echo \"📦 Installing backend dependencies...\"\n",
"cd studio/backend\n",
"pip install --no-cache-dir -r requirements.txt\n",
"\n",
"# Install additional required packages\n",
"pip install --no-cache-dir \\\n",
" huggingface_hub \\\n",
" datasets \\\n",
" python-jose[cryptography] \\\n",
" passlib[bcrypt] \\\n",
" python-multipart \\\n",
" matplotlib \\\n",
" pandas \\\n",
" ujson\n",
"\n",
"echo \"✅ Python dependencies installed\""
]
},
{
"cell_type": "markdown",
"id": "30ca4f1d",
"metadata": {},
"source": [
"## 🎨 Step 3: Build Frontend\n",
"\n",
"Build the React/TypeScript frontend."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fb65b9b2",
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"cd studio/frontend\n",
"\n",
"echo \"📦 Installing frontend dependencies...\"\n",
"npm install --legacy-peer-deps\n",
"\n",
"echo \"🏗️ Building frontend...\"\n",
"npm run build\n",
"\n",
"echo \"✅ Frontend built to studio/frontend/dist\""
]
},
{
"cell_type": "markdown",
"id": "87ecf129",
"metadata": {},
"source": [
"## 🌐 Step 4: Set Up Cloudflare Tunnel\n",
"\n",
"Install cloudflared to expose the backend server to the internet."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e6bb2086",
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"# Install cloudflared\n",
"if ! command -v cloudflared &> /dev/null; then\n",
" echo \"📦 Installing Cloudflare Tunnel...\"\n",
" wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
" sudo dpkg -i cloudflared-linux-amd64.deb\n",
" rm cloudflared-linux-amd64.deb\n",
" echo \"✅ Cloudflared installed\"\n",
"else\n",
" echo \"✅ Cloudflared already installed\"\n",
"fi\n",
"\n",
"cloudflared version"
]
},
{
"cell_type": "markdown",
"id": "3c48401b",
"metadata": {},
"source": [
"## 🚀 Step 5: Launch Unsloth Studio\n",
"\n",
"Start the backend server and create a public tunnel.\n",
"\n",
"**⚠️ IMPORTANT:**\n",
"1. Look for the **🔗 Public URL** in the output below\n",
"2. On first launch, look for the **🔑 Setup Token** (one-time use)\n",
"3. Open the URL in your browser\n",
"4. Use the setup token to create your admin account\n",
"\n",
"**Note**: This cell will run continuously. To stop the server, click the ⏹️ stop button or interrupt the kernel."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "80f2f7c5",
"metadata": {},
"outputs": [],
"source": [
"import subprocess\n",
"import time\n",
"import sys\n",
"import re\n",
"from threading import Thread\n",
"from queue import Queue, Empty\n",
"\n",
"def stream_output(pipe, queue, prefix=\"\"):\n",
" \"\"\"Stream subprocess output to queue\"\"\"\n",
" for line in iter(pipe.readline, b''):\n",
" queue.put((prefix, line.decode('utf-8')))\n",
" pipe.close()\n",
"\n",
"# Change to backend directory\n",
"os.chdir(\"/content/new-ui-prototype/studio/backend\")\n",
"\n",
"print(\"🚀 Starting Unsloth Studio...\\n\")\n",
"print(\"=\" * 70)\n",
"\n",
"# Start backend server\n",
"backend_process = subprocess.Popen(\n",
" [\"python\", \"run.py\"],\n",
" stdout=subprocess.PIPE,\n",
" stderr=subprocess.PIPE,\n",
" bufsize=1\n",
")\n",
"\n",
"# Wait for server to start\n",
"print(\"⏳ Waiting for backend server to start...\")\n",
"time.sleep(10)\n",
"\n",
"# Start cloudflared tunnel\n",
"tunnel_process = subprocess.Popen(\n",
" [\"cloudflared\", \"tunnel\", \"--url\", \"http://localhost:8000\"],\n",
" stdout=subprocess.PIPE,\n",
" stderr=subprocess.PIPE,\n",
" bufsize=1\n",
")\n",
"\n",
"# Create queues for output\n",
"backend_queue = Queue()\n",
"tunnel_queue = Queue()\n",
"\n",
"# Start output streaming threads\n",
"Thread(target=stream_output, args=(backend_process.stdout, backend_queue, \"[BACKEND]\"), daemon=True).start()\n",
"Thread(target=stream_output, args=(backend_process.stderr, backend_queue, \"[BACKEND]\"), daemon=True).start()\n",
"Thread(target=stream_output, args=(tunnel_process.stdout, tunnel_queue, \"[TUNNEL]\"), daemon=True).start()\n",
"Thread(target=stream_output, args=(tunnel_process.stderr, tunnel_queue, \"[TUNNEL]\"), daemon=True).start()\n",
"\n",
"# Monitor output and extract public URL and setup token\n",
"public_url = None\n",
"setup_token = None\n",
"url_pattern = re.compile(r'https://[a-zA-Z0-9-]+\\.trycloudflare\\.com')\n",
"token_pattern = re.compile(r'Setup token: ([a-f0-9-]+)')\n",
"\n",
"print(\"\\n📡 Monitoring server output...\\n\")\n",
"print(\"=\" * 70)\n",
"\n",
"try:\n",
" while True:\n",
" # Check backend output\n",
" try:\n",
" prefix, line = backend_queue.get(timeout=0.1)\n",
" print(f\"{prefix} {line}\", end='')\n",
" \n",
" # Look for setup token\n",
" if setup_token is None:\n",
" token_match = token_pattern.search(line)\n",
" if token_match:\n",
" setup_token = token_match.group(1)\n",
" print(f\"\\n{'=' * 70}\")\n",
" print(f\"🔑 SETUP TOKEN (save this!): {setup_token}\")\n",
" print(f\"{'=' * 70}\\n\")\n",
" except Empty:\n",
" pass\n",
" \n",
" # Check tunnel output\n",
" try:\n",
" prefix, line = tunnel_queue.get(timeout=0.1)\n",
" print(f\"{prefix} {line}\", end='')\n",
" \n",
" # Look for public URL\n",
" if public_url is None:\n",
" url_match = url_pattern.search(line)\n",
" if url_match:\n",
" public_url = url_match.group(0)\n",
" print(f\"\\n{'=' * 70}\")\n",
" print(f\"🔗 PUBLIC URL: {public_url}\")\n",
" print(f\"{'=' * 70}\\n\")\n",
" print(\"✅ Unsloth Studio is now accessible!\\n\")\n",
" if setup_token:\n",
" print(f\"📝 Next steps:\")\n",
" print(f\" 1. Open: {public_url}\")\n",
" print(f\" 2. Use setup token: {setup_token}\")\n",
" print(f\" 3. Create your admin account\\n\")\n",
" print(f\"{'=' * 70}\\n\")\n",
" except Empty:\n",
" pass\n",
" \n",
" # Check if processes are still running\n",
" if backend_process.poll() is not None:\n",
" print(\"\\n❌ Backend server stopped unexpectedly\")\n",
" break\n",
" if tunnel_process.poll() is not None:\n",
" print(\"\\n❌ Tunnel stopped unexpectedly\")\n",
" break\n",
" \n",
" time.sleep(0.1)\n",
"\n",
"except KeyboardInterrupt:\n",
" print(\"\\n\\n🛑 Shutting down...\")\n",
"finally:\n",
" backend_process.terminate()\n",
" tunnel_process.terminate()\n",
" backend_process.wait()\n",
" tunnel_process.wait()\n",
" print(\"✅ Server stopped\")"
]
},
{
"cell_type": "markdown",
"id": "356ec946",
"metadata": {},
"source": [
"## ⚙️ Optional: Configure Hugging Face Token\n",
"\n",
"If you want to use gated models or push to Hugging Face Hub, set your token here."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d18f8728",
"metadata": {},
"outputs": [],
"source": [
"# Optional: Set your Hugging Face token\n",
"# Get your token from: https://huggingface.co/settings/tokens\n",
"\n",
"from huggingface_hub import login\n",
"\n",
"# Uncomment and set your token\n",
"# HF_TOKEN = \"hf_...\"\n",
"# login(token=HF_TOKEN)\n",
"# print(\"✅ Logged in to Hugging Face\")\n",
"\n",
"print(\"💡 To set your HF token, uncomment the code above or use the UI settings\")"
]
},
{
"cell_type": "markdown",
"id": "77b1b874",
"metadata": {},
"source": [
"## 🔧 Troubleshooting\n",
"\n",
"### Server won't start\n",
"- Make sure you're using a **GPU runtime** (Runtime → Change runtime type)\n",
"- Check that all previous cells completed successfully\n",
"- Try restarting the runtime and running all cells again\n",
"\n",
"### Can't access the URL\n",
"- The Cloudflare tunnel URL is temporary and changes each time\n",
"- Make sure the server is still running (cell shows \"🔗 PUBLIC URL\")\n",
"- Try opening the URL in an incognito/private window\n",
"\n",
"### Training fails\n",
"- Check that you have enough GPU memory for your model\n",
"- Try using 4-bit quantization (enabled by default)\n",
"- Use smaller batch sizes if you get OOM errors\n",
"\n",
"### Lost setup token\n",
"- The token is shown once in the server output above\n",
"- If you need a new one, delete `/content/new-ui-prototype/studio/backend/auth.db` and restart\n",
"\n",
"---\n",
"\n",
"## 📚 Additional Resources\n",
"\n",
"- **GitHub Repository**: https://github.com/unslothai/new-ui-prototype/tree/nightly\n",
"- **Unsloth Documentation**: https://github.com/unslothai/unsloth\n",
"- **API Documentation**: Access `/docs` on your running instance\n",
"\n",
"---\n",
"\n",
"## 💾 Saving Your Work\n",
"\n",
"To save trained models and datasets:\n",
"\n",
"1. **Mount Google Drive:**"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8543c470",
"metadata": {},
"outputs": [],
"source": [
"from google.colab import drive\n",
"drive.mount('/content/drive')\n",
"\n",
"# Create directory for outputs\n",
"!mkdir -p /content/drive/MyDrive/unsloth_studio_outputs"
]
},
{
"cell_type": "markdown",
"id": "f964762d",
"metadata": {},
"source": [
"2. **Copy outputs to Drive:**"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e2a3c1c7",
"metadata": {},
"outputs": [],
"source": [
"# Copy trained models to Google Drive\n",
"!cp -r /content/new-ui-prototype/outputs/* /content/drive/MyDrive/unsloth_studio_outputs/\n",
"print(\"✅ Outputs saved to Google Drive\")"
]
},
{
"cell_type": "markdown",
"id": "67fcb4db",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 Quick Training Tips\n",
"\n",
"### Recommended Settings for Colab Free (T4 GPU)\n",
"\n",
"| Setting | Value | Note |\n",
"|---------|-------|------|\n",
"| Model | `unsloth/Qwen2.5-1.5B-Instruct` | Small but capable |\n",
"| Training Method | LoRA/QLoRA | Memory efficient |\n",
"| 4-bit Quantization | ✅ Enabled | Reduces memory usage |\n",
"| Batch Size | 2-4 | Depends on model size |\n",
"| Context Length | 2048 | Balance memory/performance |\n",
"| LoRA Rank | 16-64 | Higher = more capacity |\n",
"| Gradient Accumulation | 4 | Simulates larger batch |\n",
"\n",
"### Sample Datasets (Quick Start)\n",
"- `mlabonne/FineTome-100k` - General instruction following\n",
"- `HuggingFaceH4/no_robots` - Clean chat conversations\n",
"- `vicgalle/alpaca-gpt4` - High-quality instructions\n",
"\n",
"---\n",
"\n",
"**🎉 Happy Fine-tuning!**"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

90
studio/backend/colab.py Normal file
View file

@ -0,0 +1,90 @@
"""
Colab-specific helpers for running Unsloth Studio.
Uses Colab's built-in proxy - no external tunneling needed!
"""
from pathlib import Path
def get_colab_url(port: int = 8000) -> str:
"""
Get the actual Colab proxy URL for a port.
"""
try:
from google.colab.output import eval_js
# Use Colab's proxy mechanism
url = eval_js(f"google.colab.kernel.proxyPort({port})", timeout_sec=5)
return url if url else f"http://localhost:{port}"
except Exception as e:
print(f"Note: Could not get Colab URL ({e})")
return f"http://localhost:{port}"
def show_link(port: int = 8000):
"""Display a styled clickable link to the UI."""
from IPython.display import display, HTML
# Get real Colab proxy URL
url = get_colab_url(port)
html = f"""
<div style="padding: 20px; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
border-radius: 12px; margin: 10px 0; font-family: system-ui, -apple-system, sans-serif;">
<h2 style="color: white; margin: 0 0 12px 0; font-size: 24px;">
🦥 Unsloth Studio is Ready!
</h2>
<a href="{url}" target="_blank"
style="display: inline-block; padding: 14px 28px; background: white; color: #16a34a;
text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
🚀 Open Unsloth Studio
</a>
<p style="color: rgba(255,255,255,0.9); margin: 16px 0 0 0; font-size: 13px;
word-break: break-all; font-family: monospace;">
{url}
</p>
</div>
"""
display(HTML(html))
def start(port: int = 8000):
"""
Start Unsloth Studio server in Colab and display the URL.
Usage:
from colab import start
start()
"""
import sys
print("🦥 Starting Unsloth Studio...")
# Add backend to path
backend_path = str(Path(__file__).parent)
if backend_path not in sys.path:
sys.path.insert(0, backend_path)
print(" Loading backend...")
from run import run_server
# Auto-detect frontend path
repo_root = Path(__file__).parent.parent
frontend_path = repo_root / "frontend" / "dist"
if not frontend_path.exists():
print("❌ Frontend not built! Please run the setup cell first.")
return
print(" Starting server...")
# Start server silently
run_server(host="0.0.0.0", port=port, frontend_path=frontend_path, silent=True)
print(" Server started!")
# Show the clickable link with real URL
show_link(port)
if __name__ == "__main__":
start()