git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
600 lines
19 KiB
Rust
600 lines
19 KiB
Rust
#![allow(
|
|
clippy::all,
|
|
unused_imports,
|
|
unused_variables,
|
|
dead_code,
|
|
unused_mut,
|
|
unused_assignments,
|
|
non_camel_case_types,
|
|
clippy::approx_constant,
|
|
unexpected_cfgs,
|
|
unused_must_use,
|
|
unused_parens
|
|
)]
|
|
//! Download small GGUF models for testing
|
|
//!
|
|
//! This utility downloads small, quantized models suitable for testing RuvLLM.
|
|
//! Now includes support for RuvLTRA models via the HuggingFace Hub integration.
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! ```bash
|
|
//! # Download RuvLTRA Small (recommended for quick tests)
|
|
//! cargo run -p ruvllm --example download_test_model -- --model ruvltra-small
|
|
//!
|
|
//! # Download RuvLTRA Medium
|
|
//! cargo run -p ruvllm --example download_test_model -- --model ruvltra-medium
|
|
//!
|
|
//! # Download TinyLlama (legacy)
|
|
//! cargo run -p ruvllm --example download_test_model -- --model tinyllama
|
|
//!
|
|
//! # Download to custom directory
|
|
//! cargo run -p ruvllm --example download_test_model -- --model ruvltra-small --output ./my_models
|
|
//!
|
|
//! # List available models
|
|
//! cargo run -p ruvllm --example download_test_model -- --list
|
|
//! ```
|
|
//!
|
|
//! ## Available Models
|
|
//!
|
|
//! | Model | Size | Params | Use Case |
|
|
//! |-------|------|--------|----------|
|
|
//! | ruvltra-small | ~662MB | 0.5B | Edge devices, includes SONA weights |
|
|
//! | ruvltra-medium | ~2.1GB | 3B | General purpose, extended context |
|
|
//! | tinyllama | ~600MB | 1.1B | Fast iteration, general testing |
|
|
//! | qwen-0.5b | ~400MB | 0.5B | Smallest, fastest tests |
|
|
//!
|
|
//! ## Environment Variables
|
|
//!
|
|
//! - `HF_TOKEN`: HuggingFace token for gated models (optional for most models)
|
|
//! - `RUVLLM_MODELS_DIR`: Default output directory for models
|
|
|
|
use ruvllm::hub::{default_cache_dir, DownloadConfig, ModelDownloader, RuvLtraRegistry};
|
|
use std::env;
|
|
use std::fs::{self, File};
|
|
use std::io::{self, BufWriter, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
|
|
/// Model definitions with HuggingFace URLs
|
|
const MODELS: &[ModelDef] = &[
|
|
ModelDef {
|
|
name: "tinyllama",
|
|
display_name: "TinyLlama 1.1B Chat Q4_K_M",
|
|
url: "https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf",
|
|
filename: "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf",
|
|
size_mb: 669,
|
|
architecture: "llama",
|
|
description: "Fast, small model ideal for testing. Good general performance.",
|
|
},
|
|
ModelDef {
|
|
name: "qwen-0.5b",
|
|
display_name: "Qwen2 0.5B Instruct Q4_K_M",
|
|
url: "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf",
|
|
filename: "qwen2-0_5b-instruct-q4_k_m.gguf",
|
|
size_mb: 400,
|
|
architecture: "qwen2",
|
|
description: "Smallest recommended model. Excellent for quick iteration.",
|
|
},
|
|
ModelDef {
|
|
name: "phi-3-mini",
|
|
display_name: "Phi-3 Mini 4K Instruct Q4_K_M",
|
|
url: "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf",
|
|
filename: "Phi-3-mini-4k-instruct-q4.gguf",
|
|
size_mb: 2200,
|
|
architecture: "phi3",
|
|
description: "Microsoft's efficient model. Higher quality outputs.",
|
|
},
|
|
ModelDef {
|
|
name: "gemma-2b",
|
|
display_name: "Gemma 2B Instruct Q4_K_M",
|
|
url: "https://huggingface.co/google/gemma-2b-it-GGUF/resolve/main/gemma-2b-it.Q4_K_M.gguf",
|
|
filename: "gemma-2b-it.Q4_K_M.gguf",
|
|
size_mb: 1500,
|
|
architecture: "gemma",
|
|
description: "Google's efficient model with good instruction following.",
|
|
},
|
|
ModelDef {
|
|
name: "stablelm-2-1.6b",
|
|
display_name: "StableLM 2 1.6B Chat Q4_K_M",
|
|
url: "https://huggingface.co/TheBloke/stablelm-2-1_6b-chat-GGUF/resolve/main/stablelm-2-1_6b-chat.Q4_K_M.gguf",
|
|
filename: "stablelm-2-1_6b-chat.Q4_K_M.gguf",
|
|
size_mb: 1000,
|
|
architecture: "stablelm",
|
|
description: "Stability AI's efficient chat model.",
|
|
},
|
|
];
|
|
|
|
struct ModelDef {
|
|
name: &'static str,
|
|
display_name: &'static str,
|
|
url: &'static str,
|
|
filename: &'static str,
|
|
size_mb: usize,
|
|
architecture: &'static str,
|
|
description: &'static str,
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
if args.len() < 2 || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) {
|
|
print_help();
|
|
return;
|
|
}
|
|
|
|
if args.contains(&"--list".to_string()) || args.contains(&"-l".to_string()) {
|
|
list_models();
|
|
list_ruvltra_models();
|
|
return;
|
|
}
|
|
|
|
// Parse arguments
|
|
let mut model_name: Option<&str> = None;
|
|
let mut output_dir: Option<PathBuf> = None;
|
|
let mut force = false;
|
|
|
|
let mut i = 1;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--model" | "-m" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
model_name = Some(args[i].as_str());
|
|
}
|
|
}
|
|
"--output" | "-o" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
output_dir = Some(PathBuf::from(&args[i]));
|
|
}
|
|
}
|
|
"--force" | "-f" => {
|
|
force = true;
|
|
}
|
|
arg if !arg.starts_with('-') && model_name.is_none() => {
|
|
model_name = Some(arg);
|
|
}
|
|
_ => {}
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
let model_name = match model_name {
|
|
Some(name) => name,
|
|
None => {
|
|
eprintln!("Error: No model specified.");
|
|
eprintln!("Use --list to see available models.");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Check if this is a RuvLTRA model first
|
|
let registry = RuvLtraRegistry::new();
|
|
if let Some(ruvltra_model) = registry.get(model_name) {
|
|
download_ruvltra_model(ruvltra_model, output_dir, force);
|
|
return;
|
|
}
|
|
|
|
// Find the legacy model definition
|
|
let model = match MODELS.iter().find(|m| m.name == model_name) {
|
|
Some(m) => m,
|
|
None => {
|
|
eprintln!("Error: Unknown model '{}'", model_name);
|
|
eprintln!("Available models:");
|
|
eprintln!("\nRuvLTRA models:");
|
|
for id in registry.model_ids() {
|
|
eprintln!(" - {}", id);
|
|
}
|
|
eprintln!("\nLegacy models:");
|
|
for m in MODELS {
|
|
eprintln!(" - {}", m.name);
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Determine output directory
|
|
let output_dir = output_dir
|
|
.or_else(|| env::var("RUVLLM_MODELS_DIR").ok().map(PathBuf::from))
|
|
.unwrap_or_else(|| PathBuf::from("./test_models"));
|
|
|
|
// Create output directory
|
|
if let Err(e) = fs::create_dir_all(&output_dir) {
|
|
eprintln!("Error creating output directory: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let output_path = output_dir.join(model.filename);
|
|
|
|
// Check if file already exists
|
|
if output_path.exists() && !force {
|
|
println!("Model already exists: {}", output_path.display());
|
|
println!("Use --force to re-download.");
|
|
|
|
// Verify file size
|
|
if let Ok(metadata) = fs::metadata(&output_path) {
|
|
let size_mb = metadata.len() as f64 / (1024.0 * 1024.0);
|
|
let expected_mb = model.size_mb as f64;
|
|
if (size_mb - expected_mb).abs() / expected_mb > 0.1 {
|
|
println!(
|
|
"Warning: File size ({:.1} MB) differs from expected ({} MB)",
|
|
size_mb, model.size_mb
|
|
);
|
|
println!("Consider re-downloading with --force");
|
|
} else {
|
|
println!("File size verified: {:.1} MB", size_mb);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Print download info
|
|
println!("Downloading: {}", model.display_name);
|
|
println!("Architecture: {}", model.architecture);
|
|
println!("Size: ~{} MB", model.size_mb);
|
|
println!("Destination: {}", output_path.display());
|
|
println!();
|
|
|
|
// Estimate download time
|
|
let estimated_time = estimate_download_time(model.size_mb);
|
|
println!(
|
|
"Estimated download time: {}",
|
|
format_duration(estimated_time)
|
|
);
|
|
println!();
|
|
|
|
// Download the model
|
|
match download_model(model.url, &output_path, model.size_mb) {
|
|
Ok(()) => {
|
|
println!("\nDownload complete!");
|
|
println!("Model saved to: {}", output_path.display());
|
|
println!();
|
|
println!("To run tests with this model:");
|
|
println!(
|
|
" TEST_MODEL_PATH={} cargo test -p ruvllm --test real_model_test -- --ignored",
|
|
output_path.display()
|
|
);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("\nDownload failed: {}", e);
|
|
// Clean up partial download
|
|
let _ = fs::remove_file(&output_path);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print_help() {
|
|
println!("RuvLLM Test Model Downloader");
|
|
println!();
|
|
println!("USAGE:");
|
|
println!(" cargo run -p ruvllm --example download_test_model -- [OPTIONS] <MODEL>");
|
|
println!();
|
|
println!("ARGUMENTS:");
|
|
println!(" <MODEL> Model to download (use --list to see options)");
|
|
println!();
|
|
println!("OPTIONS:");
|
|
println!(" -m, --model <MODEL> Model to download");
|
|
println!(" -o, --output <DIR> Output directory (default: ./test_models)");
|
|
println!(" -f, --force Force re-download even if file exists");
|
|
println!(" -l, --list List available models");
|
|
println!(" -h, --help Print help information");
|
|
println!();
|
|
println!("ENVIRONMENT VARIABLES:");
|
|
println!(" HF_TOKEN HuggingFace token for gated models");
|
|
println!(" RUVLLM_MODELS_DIR Default output directory");
|
|
println!();
|
|
println!("EXAMPLES:");
|
|
println!(" # Download TinyLlama (recommended for quick tests)");
|
|
println!(" cargo run -p ruvllm --example download_test_model -- tinyllama");
|
|
println!();
|
|
println!(" # Download to custom directory");
|
|
println!(" cargo run -p ruvllm --example download_test_model -- -m qwen-0.5b -o ./models");
|
|
}
|
|
|
|
fn list_models() {
|
|
println!("Available models for testing:\n");
|
|
println!("{:<15} {:>8} {:<40}", "NAME", "SIZE", "DESCRIPTION");
|
|
println!("{}", "-".repeat(70));
|
|
|
|
for model in MODELS {
|
|
println!(
|
|
"{:<15} {:>6}MB {}",
|
|
model.name, model.size_mb, model.description
|
|
);
|
|
}
|
|
|
|
println!();
|
|
println!("Recommendations:");
|
|
println!(" - For quick tests: tinyllama or qwen-0.5b");
|
|
println!(" - For quality testing: phi-3-mini");
|
|
println!(" - For architecture variety: download multiple models");
|
|
}
|
|
|
|
fn estimate_download_time(size_mb: usize) -> Duration {
|
|
// Assume ~10 MB/s average download speed
|
|
let speed_mbps = 10.0;
|
|
let seconds = size_mb as f64 / speed_mbps;
|
|
Duration::from_secs_f64(seconds)
|
|
}
|
|
|
|
fn format_duration(d: Duration) -> String {
|
|
let secs = d.as_secs();
|
|
if secs < 60 {
|
|
format!("{} seconds", secs)
|
|
} else if secs < 3600 {
|
|
format!("{} min {} sec", secs / 60, secs % 60)
|
|
} else {
|
|
format!("{} hr {} min", secs / 3600, (secs % 3600) / 60)
|
|
}
|
|
}
|
|
|
|
fn download_model(url: &str, output_path: &Path, expected_size_mb: usize) -> io::Result<()> {
|
|
// Use curl or wget if available, otherwise fall back to pure Rust
|
|
if which_cmd("curl") {
|
|
download_with_curl(url, output_path, expected_size_mb)
|
|
} else if which_cmd("wget") {
|
|
download_with_wget(url, output_path)
|
|
} else {
|
|
download_with_rust(url, output_path, expected_size_mb)
|
|
}
|
|
}
|
|
|
|
fn which_cmd(cmd: &str) -> bool {
|
|
std::process::Command::new("which")
|
|
.arg(cmd)
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn download_with_curl(url: &str, output_path: &Path, _expected_size_mb: usize) -> io::Result<()> {
|
|
println!("Downloading with curl...");
|
|
|
|
let status = std::process::Command::new("curl")
|
|
.args([
|
|
"-L", // Follow redirects
|
|
"-#", // Progress bar
|
|
"--fail", // Fail on HTTP errors
|
|
"-o",
|
|
output_path.to_str().unwrap(),
|
|
url,
|
|
])
|
|
.status()?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
format!("curl exited with status: {}", status),
|
|
))
|
|
}
|
|
}
|
|
|
|
fn download_with_wget(url: &str, output_path: &Path) -> io::Result<()> {
|
|
println!("Downloading with wget...");
|
|
|
|
let status = std::process::Command::new("wget")
|
|
.args([
|
|
"-q", // Quiet
|
|
"--show-progress", // But show progress
|
|
"-O",
|
|
output_path.to_str().unwrap(),
|
|
url,
|
|
])
|
|
.status()?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
format!("wget exited with status: {}", status),
|
|
))
|
|
}
|
|
}
|
|
|
|
fn download_with_rust(url: &str, output_path: &Path, _expected_size_mb: usize) -> io::Result<()> {
|
|
println!("Downloading with built-in HTTP client...");
|
|
println!("Note: For faster downloads, install curl or wget.");
|
|
|
|
// Simple HTTP download using std library
|
|
// This is a basic implementation - production code should use reqwest or similar
|
|
|
|
let url_parts: Vec<&str> = url.split('/').collect();
|
|
let _host = url_parts
|
|
.get(2)
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid URL"))?;
|
|
|
|
let _path = format!("/{}", url_parts[3..].join("/"));
|
|
|
|
// For HTTPS, we need to use a TLS library
|
|
// This simple example shows the structure but won't work for HTTPS
|
|
println!("Warning: Built-in downloader doesn't support HTTPS.");
|
|
println!("Please install curl: brew install curl (macOS) or apt install curl (Linux)");
|
|
|
|
// Create a placeholder file to show where the model should go
|
|
let mut file = BufWriter::new(File::create(output_path)?);
|
|
writeln!(file, "# Placeholder - download failed")?;
|
|
writeln!(file, "# Download manually from: {}", url)?;
|
|
writeln!(file, "# Or install curl and re-run this command")?;
|
|
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
"HTTPS download requires curl or wget. Please install curl.",
|
|
))
|
|
}
|
|
|
|
/// Format bytes with appropriate unit
|
|
#[allow(dead_code)]
|
|
fn format_bytes(bytes: u64) -> String {
|
|
const KB: u64 = 1024;
|
|
const MB: u64 = KB * 1024;
|
|
const GB: u64 = MB * 1024;
|
|
|
|
if bytes >= GB {
|
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
|
} else if bytes >= MB {
|
|
format!("{:.2} MB", bytes as f64 / MB as f64)
|
|
} else if bytes >= KB {
|
|
format!("{:.2} KB", bytes as f64 / KB as f64)
|
|
} else {
|
|
format!("{} B", bytes)
|
|
}
|
|
}
|
|
|
|
/// Download a RuvLTRA model using the hub integration
|
|
fn download_ruvltra_model(
|
|
model_info: &ruvllm::hub::ModelInfo,
|
|
output_dir: Option<PathBuf>,
|
|
force: bool,
|
|
) {
|
|
use ruvllm::hub::DownloadConfig;
|
|
|
|
println!("Downloading RuvLTRA model: {}", model_info.name);
|
|
println!("Repository: {}", model_info.repo);
|
|
println!("Size: ~{} MB", model_info.size_bytes / (1024 * 1024));
|
|
println!("Quantization: {:?}", model_info.quantization);
|
|
if model_info.has_sona_weights {
|
|
println!("Includes: SONA pre-trained weights");
|
|
}
|
|
println!();
|
|
|
|
// Create config
|
|
let cache_dir = output_dir
|
|
.or_else(|| env::var("RUVLLM_MODELS_DIR").ok().map(PathBuf::from))
|
|
.unwrap_or_else(default_cache_dir);
|
|
|
|
let config = DownloadConfig {
|
|
cache_dir,
|
|
hf_token: env::var("HF_TOKEN").ok(),
|
|
resume: !force,
|
|
show_progress: true,
|
|
verify_checksum: model_info.checksum.is_some(),
|
|
max_retries: 3,
|
|
};
|
|
|
|
// Create downloader
|
|
let downloader = ModelDownloader::with_config(config);
|
|
|
|
// Download the model
|
|
match downloader.download(model_info, None) {
|
|
Ok(path) => {
|
|
println!("\nDownload complete!");
|
|
println!("Model saved to: {}", path.display());
|
|
println!();
|
|
println!("Hardware requirements:");
|
|
println!(" - Minimum RAM: {:.1} GB", model_info.hardware.min_ram_gb);
|
|
println!(
|
|
" - Recommended RAM: {:.1} GB",
|
|
model_info.hardware.recommended_ram_gb
|
|
);
|
|
if model_info.hardware.supports_ane {
|
|
println!(" - Apple Neural Engine: ✓ Supported");
|
|
}
|
|
if model_info.hardware.supports_metal {
|
|
println!(" - Metal GPU: ✓ Supported");
|
|
}
|
|
println!();
|
|
println!("To use this model:");
|
|
println!(" cargo test -p ruvllm --test real_model_test -- --ignored");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("\nDownload failed: {}", e);
|
|
eprintln!("\nTroubleshooting:");
|
|
eprintln!(" - Ensure you have curl or wget installed");
|
|
eprintln!(" - Check your internet connection");
|
|
eprintln!(" - If downloading from a gated repo, set HF_TOKEN environment variable");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// List available RuvLTRA models
|
|
fn list_ruvltra_models() {
|
|
use ruvllm::hub::RuvLtraRegistry;
|
|
|
|
let registry = RuvLtraRegistry::new();
|
|
|
|
println!("\nRuvLTRA models (recommended):\n");
|
|
println!(
|
|
"{:<20} {:>8} {:>6} {:<50}",
|
|
"NAME", "SIZE", "PARAMS", "DESCRIPTION"
|
|
);
|
|
println!("{}", "-".repeat(90));
|
|
|
|
for model in registry.list_all() {
|
|
if !model.is_adapter {
|
|
println!(
|
|
"{:<20} {:>6}MB {:>5.1}B {}",
|
|
model.id,
|
|
model.size_bytes / (1024 * 1024),
|
|
model.params_b,
|
|
model.description.chars().take(48).collect::<String>()
|
|
);
|
|
}
|
|
}
|
|
|
|
println!("\nAdapters:\n");
|
|
for model in registry.list_all() {
|
|
if model.is_adapter {
|
|
println!(
|
|
"{:<20} {:>6}MB (requires: {})",
|
|
model.id,
|
|
model.size_bytes / (1024 * 1024),
|
|
model.base_model.as_ref().unwrap()
|
|
);
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!("Recommendations:");
|
|
println!(" - For edge devices: ruvltra-small");
|
|
println!(" - For general use: ruvltra-medium");
|
|
println!(" - For code completion: ruvltra-small + ruvltra-small-coder adapter");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_format_bytes() {
|
|
assert_eq!(format_bytes(500), "500 B");
|
|
assert_eq!(format_bytes(1500), "1.46 KB");
|
|
assert_eq!(format_bytes(1_500_000), "1.43 MB");
|
|
assert_eq!(format_bytes(1_500_000_000), "1.40 GB");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_duration() {
|
|
assert_eq!(format_duration(Duration::from_secs(30)), "30 seconds");
|
|
assert_eq!(format_duration(Duration::from_secs(90)), "1 min 30 sec");
|
|
assert_eq!(format_duration(Duration::from_secs(3700)), "1 hr 1 min");
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_definitions() {
|
|
// Verify all models have valid data
|
|
for model in MODELS {
|
|
assert!(!model.name.is_empty());
|
|
assert!(!model.url.is_empty());
|
|
assert!(model.url.starts_with("https://"));
|
|
assert!(model.size_mb > 0);
|
|
assert!(model.filename.ends_with(".gguf"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ruvltra_registry() {
|
|
use ruvllm::hub::RuvLtraRegistry;
|
|
|
|
let registry = RuvLtraRegistry::new();
|
|
assert!(registry.get("ruvltra-small").is_some());
|
|
assert!(registry.get("ruvltra-medium").is_some());
|
|
assert!(registry.list_all().len() > 0);
|
|
}
|
|
}
|