mirror of
https://github.com/signerless/llm-checker.git
synced 2026-10-21 14:12:01 +00:00
This commit adds a comprehensive AI-powered model selection system using machine learning: 🧠 **AI Model Selector Framework** - TabTransformer model architecture for hardware-aware model selection - Lightweight quantized ONNX model (<150KB) for fast inference - Comprehensive Python training pipeline with data collection and preprocessing - JavaScript runtime with ONNX integration for real-time model selection 🔬 **Machine Learning Pipeline** - benchmark_collector.py: Automated performance data collection across different hardware - dataset_aggregator.py: Data preprocessing and training label generation - train_model.py: TabTransformer training with quantization and ONNX export - Hardware feature engineering with categorical and numerical features 🎯 **Smart Selection Logic** - AI-powered predictions based on hardware specifications - Robust fallback heuristics when AI model unavailable - Confidence scoring and multi-model ranking - Hardware tier classification (CPU, RAM, VRAM) ⚡ **New CLI Command: ai-check** - `ai-check --status`: Check AI model training status - `ai-check --benchmark`: Collect performance benchmarks - `ai-check --train`: Train the AI model - `ai-check --models <list>`: Select from specific models - `ai-check --prompt <text>`: Run with selected model 🔧 **Technical Features** - Optional ONNX Runtime dependency with graceful fallback - System information detection with multiple fallback methods - Cross-platform compatibility (macOS, Linux, Windows) - Integration with existing Ollama client and hardware detection 📊 **Model Architecture** - 2-layer TabTransformer with 32-dim embeddings - Binary classification for optimal model selection - Features: model size, CPU cores/freq, RAM/VRAM, GPU model, platform - Target: >90% AUC, <150KB model size, <100ms inference This enables intelligent, hardware-optimized model selection that learns from real performance data across diverse system configurations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
352 lines
No EOL
13 KiB
Python
352 lines
No EOL
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Performance benchmark collector for Ollama models.
|
|
Collects hardware specs and model performance data for ML training.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import subprocess
|
|
import platform
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, asdict
|
|
from typing import List, Dict, Optional, Tuple
|
|
import pandas as pd
|
|
import psutil
|
|
import sys
|
|
|
|
@dataclass
|
|
class HardwareSpec:
|
|
"""Hardware specification for a given system."""
|
|
host_id: str
|
|
cpu_cores: int
|
|
cpu_freq_max: float # GHz
|
|
total_ram_gb: float
|
|
gpu_model: str
|
|
gpu_vram_gb: float
|
|
platform: str
|
|
architecture: str
|
|
|
|
@dataclass
|
|
class ModelBenchmark:
|
|
"""Benchmark result for a specific model on specific hardware."""
|
|
host_id: str
|
|
model_id: str
|
|
model_size: str
|
|
tokens_per_second: float
|
|
peak_ram_mb: float
|
|
peak_vram_mb: float
|
|
response_time: float
|
|
success: bool
|
|
error_message: Optional[str] = None
|
|
timestamp: str = ""
|
|
|
|
class HardwareDetector:
|
|
"""Detects hardware specifications of the current system."""
|
|
|
|
def __init__(self):
|
|
self.host_id = self._generate_host_id()
|
|
|
|
def _generate_host_id(self) -> str:
|
|
"""Generate a unique but anonymous host identifier."""
|
|
system_info = f"{platform.node()}-{platform.system()}-{platform.machine()}"
|
|
return hashlib.md5(system_info.encode()).hexdigest()[:12]
|
|
|
|
def get_hardware_spec(self) -> HardwareSpec:
|
|
"""Collect comprehensive hardware specifications."""
|
|
# CPU information
|
|
cpu_cores = psutil.cpu_count(logical=False)
|
|
cpu_freq = psutil.cpu_freq()
|
|
cpu_freq_max = cpu_freq.max / 1000 if cpu_freq else 3.0 # Default to 3GHz
|
|
|
|
# Memory information
|
|
memory = psutil.virtual_memory()
|
|
total_ram_gb = memory.total / (1024**3)
|
|
|
|
# GPU information
|
|
gpu_model, gpu_vram_gb = self._detect_gpu()
|
|
|
|
return HardwareSpec(
|
|
host_id=self.host_id,
|
|
cpu_cores=cpu_cores,
|
|
cpu_freq_max=cpu_freq_max,
|
|
total_ram_gb=total_ram_gb,
|
|
gpu_model=gpu_model,
|
|
gpu_vram_gb=gpu_vram_gb,
|
|
platform=platform.system(),
|
|
architecture=platform.machine()
|
|
)
|
|
|
|
def _detect_gpu(self) -> Tuple[str, float]:
|
|
"""Detect GPU model and VRAM."""
|
|
try:
|
|
# Try nvidia-smi first
|
|
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total',
|
|
'--format=csv,noheader,nounits'],
|
|
capture_output=True, text=True, timeout=10)
|
|
if result.returncode == 0:
|
|
lines = result.stdout.strip().split('\n')
|
|
if lines and lines[0]:
|
|
parts = lines[0].split(', ')
|
|
if len(parts) >= 2:
|
|
gpu_name = parts[0].strip()
|
|
vram_mb = float(parts[1].strip())
|
|
return gpu_name, vram_mb / 1024 # Convert to GB
|
|
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
|
|
pass
|
|
|
|
# Try other methods or return CPU-only
|
|
try:
|
|
# Check for Apple Silicon
|
|
if platform.system() == "Darwin" and platform.machine() == "arm64":
|
|
# Apple Silicon - use unified memory
|
|
memory = psutil.virtual_memory()
|
|
return "Apple Silicon GPU", memory.total / (1024**3) * 0.75 # Assume 75% available for GPU
|
|
except:
|
|
pass
|
|
|
|
# Default to CPU-only
|
|
return "CPU Only", 0.0
|
|
|
|
class OllamaBenchmarker:
|
|
"""Benchmarks Ollama model performance."""
|
|
|
|
def __init__(self):
|
|
self.test_prompt = "Explain in one sentence what machine learning is."
|
|
|
|
def get_available_models(self) -> List[str]:
|
|
"""Get list of locally available Ollama models."""
|
|
try:
|
|
result = subprocess.run(['ollama', 'list'], capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
print(f"Error getting models: {result.stderr}")
|
|
return []
|
|
|
|
lines = result.stdout.strip().split('\n')[1:] # Skip header
|
|
models = []
|
|
|
|
for line in lines:
|
|
if line.strip():
|
|
parts = line.split()
|
|
if parts:
|
|
model_name = parts[0]
|
|
models.append(model_name)
|
|
|
|
return models
|
|
|
|
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
print(f"Error: {e}")
|
|
return []
|
|
|
|
def benchmark_model(self, model_id: str, hardware_spec: HardwareSpec) -> ModelBenchmark:
|
|
"""Benchmark a specific model."""
|
|
print(f"Benchmarking {model_id}...")
|
|
|
|
# Initialize benchmark result
|
|
benchmark = ModelBenchmark(
|
|
host_id=hardware_spec.host_id,
|
|
model_id=model_id,
|
|
model_size=self._extract_size_from_model_id(model_id),
|
|
tokens_per_second=0.0,
|
|
peak_ram_mb=0.0,
|
|
peak_vram_mb=0.0,
|
|
response_time=0.0,
|
|
success=False,
|
|
timestamp=time.strftime("%Y-%m-%d %H:%M:%S")
|
|
)
|
|
|
|
try:
|
|
# Record initial memory
|
|
process = psutil.Process()
|
|
initial_memory = process.memory_info().rss / (1024**2) # MB
|
|
|
|
# Run the benchmark
|
|
start_time = time.time()
|
|
|
|
result = subprocess.run([
|
|
'ollama', 'run', model_id, self.test_prompt
|
|
], capture_output=True, text=True, timeout=120)
|
|
|
|
end_time = time.time()
|
|
response_time = end_time - start_time
|
|
|
|
if result.returncode == 0:
|
|
# Parse output for tokens/second if available
|
|
output = result.stderr if result.stderr else result.stdout
|
|
tokens_per_second = self._extract_tokens_per_second(output)
|
|
|
|
# Estimate peak memory usage (simplified)
|
|
final_memory = process.memory_info().rss / (1024**2) # MB
|
|
peak_ram_mb = max(final_memory - initial_memory, 0)
|
|
|
|
benchmark.tokens_per_second = tokens_per_second
|
|
benchmark.peak_ram_mb = peak_ram_mb
|
|
benchmark.peak_vram_mb = 0.0 # TODO: Implement GPU memory tracking
|
|
benchmark.response_time = response_time
|
|
benchmark.success = True
|
|
|
|
print(f" ✅ {model_id}: {tokens_per_second:.2f} tokens/s, {response_time:.2f}s")
|
|
|
|
else:
|
|
benchmark.error_message = result.stderr
|
|
print(f" ❌ {model_id}: {result.stderr}")
|
|
|
|
except subprocess.TimeoutExpired:
|
|
benchmark.error_message = "Timeout"
|
|
print(f" ⏰ {model_id}: Timeout")
|
|
except Exception as e:
|
|
benchmark.error_message = str(e)
|
|
print(f" ❌ {model_id}: {e}")
|
|
|
|
return benchmark
|
|
|
|
def _extract_size_from_model_id(self, model_id: str) -> str:
|
|
"""Extract model size from model identifier."""
|
|
# Extract patterns like 7b, 13b, 70b, etc.
|
|
import re
|
|
size_match = re.search(r'(\d+\.?\d*[kmb])', model_id.lower())
|
|
return size_match.group(1) if size_match else "unknown"
|
|
|
|
def _extract_tokens_per_second(self, output: str) -> float:
|
|
"""Extract tokens per second from Ollama output."""
|
|
import re
|
|
|
|
# Look for patterns like "45.67 tokens/s" or similar
|
|
patterns = [
|
|
r'(\d+\.?\d*)\s*tokens?/s',
|
|
r'(\d+\.?\d*)\s*tok/s',
|
|
r'speed:\s*(\d+\.?\d*)',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, output, re.IGNORECASE)
|
|
if match:
|
|
try:
|
|
return float(match.group(1))
|
|
except ValueError:
|
|
continue
|
|
|
|
# If no explicit rate found, estimate from response time and output length
|
|
# This is a rough approximation
|
|
word_count = len(output.split())
|
|
if word_count > 10: # Reasonable response
|
|
return word_count * 1.3 # Rough tokens per word estimate
|
|
|
|
return 0.0
|
|
|
|
def benchmark_all_models(self, hardware_spec: HardwareSpec) -> List[ModelBenchmark]:
|
|
"""Benchmark all available models."""
|
|
models = self.get_available_models()
|
|
|
|
if not models:
|
|
print("No models found. Please install some models first:")
|
|
print(" ollama pull llama2:7b")
|
|
print(" ollama pull mistral:7b")
|
|
print(" ollama pull phi3:mini")
|
|
return []
|
|
|
|
print(f"Found {len(models)} models to benchmark")
|
|
benchmarks = []
|
|
|
|
for model_id in models:
|
|
benchmark = self.benchmark_model(model_id, hardware_spec)
|
|
benchmarks.append(benchmark)
|
|
|
|
# Small delay between benchmarks
|
|
time.sleep(2)
|
|
|
|
return benchmarks
|
|
|
|
class BenchmarkCollector:
|
|
"""Main collector that coordinates hardware detection and model benchmarking."""
|
|
|
|
def __init__(self, output_dir: str = "data/raw"):
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.hardware_detector = HardwareDetector()
|
|
self.benchmarker = OllamaBenchmarker()
|
|
|
|
def collect_benchmark_data(self) -> str:
|
|
"""Collect complete benchmark data for this system."""
|
|
print("🔍 Detecting hardware specifications...")
|
|
hardware_spec = self.hardware_detector.get_hardware_spec()
|
|
|
|
print(f"Hardware detected:")
|
|
print(f" Host ID: {hardware_spec.host_id}")
|
|
print(f" CPU: {hardware_spec.cpu_cores} cores @ {hardware_spec.cpu_freq_max:.1f} GHz")
|
|
print(f" RAM: {hardware_spec.total_ram_gb:.1f} GB")
|
|
print(f" GPU: {hardware_spec.gpu_model} ({hardware_spec.gpu_vram_gb:.1f} GB VRAM)")
|
|
|
|
print("\n🚀 Starting model benchmarks...")
|
|
benchmarks = self.benchmarker.benchmark_all_models(hardware_spec)
|
|
|
|
if not benchmarks:
|
|
print("No benchmarks collected.")
|
|
return ""
|
|
|
|
# Convert to DataFrame
|
|
benchmark_data = [asdict(b) for b in benchmarks]
|
|
df = pd.DataFrame(benchmark_data)
|
|
|
|
# Add hardware info to each row
|
|
hardware_dict = asdict(hardware_spec)
|
|
for key, value in hardware_dict.items():
|
|
if key != 'host_id': # host_id already in benchmark data
|
|
df[f'hw_{key}'] = value
|
|
|
|
# Save to parquet
|
|
output_file = self.output_dir / f"benchmark_{hardware_spec.host_id}_{int(time.time())}.parquet"
|
|
df.to_parquet(output_file, index=False)
|
|
|
|
print(f"\n✅ Benchmark data saved to: {output_file}")
|
|
print(f"Collected {len(benchmarks)} model benchmarks")
|
|
|
|
return str(output_file)
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
if len(sys.argv) > 1 and sys.argv[1] == "--help":
|
|
print("Ollama Model Performance Benchmark Collector")
|
|
print("")
|
|
print("This script benchmarks all locally installed Ollama models")
|
|
print("and collects hardware specifications for ML training.")
|
|
print("")
|
|
print("Prerequisites:")
|
|
print(" - Ollama installed and running")
|
|
print(" - At least one model installed (ollama pull <model>)")
|
|
print(" - Python packages: pandas, psutil, pyarrow")
|
|
print("")
|
|
print("Usage:")
|
|
print(" python benchmark_collector.py")
|
|
print("")
|
|
return
|
|
|
|
print("🧠 Ollama Model Performance Benchmark Collector")
|
|
print("=" * 50)
|
|
|
|
try:
|
|
collector = BenchmarkCollector()
|
|
output_file = collector.collect_benchmark_data()
|
|
|
|
if output_file:
|
|
print(f"\n🎉 Collection complete! Data saved to: {output_file}")
|
|
print("\nNext steps:")
|
|
print(" 1. Run this on multiple machines with different hardware")
|
|
print(" 2. Aggregate all parquet files for training")
|
|
print(" 3. Train the ML model selector")
|
|
else:
|
|
print("\n❌ No data collected. Please install Ollama models first.")
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n⏹️ Collection cancelled by user")
|
|
except Exception as e:
|
|
print(f"\n❌ Error during collection: {e}")
|
|
if "--debug" in sys.argv:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
if __name__ == "__main__":
|
|
main() |