use anyhow::{Error as E, Result};
use candle_core::{Device, Tensor};
use clap::Parser;
use hf_hub::{api::sync::Api, Repo, RepoType};
use std::path::PathBuf;
use std::time::Instant;
use tokenizers::Tokenizer;
const CLS_TOKEN_ID: i64 = 101;
const SEP_TOKEN_ID: i64 = 102;
const PAD_TOKEN_ID: i64 = 0;
const DISTILBERT_VOCAB_SIZE: usize = 30522;
const ANE_SEQUENCE_LENGTH: usize = 128;
fn tokenize_text(
text: &str,
tokenizer: &Tokenizer,
max_length: usize,
) -> Result<(Vec<i64>, Vec<i64>)> {
let encoding = tokenizer
.encode(text, true)
.map_err(|e| E::msg(format!("Tokenization failed: {e}")))?;
let mut input_ids: Vec<i64> = encoding.get_ids().iter().map(|&id| id as i64).collect();
if input_ids.len() > max_length {
input_ids.truncate(max_length - 1);
input_ids.push(SEP_TOKEN_ID);
}
let mut attention_mask = vec![1i64; input_ids.len()];
while input_ids.len() < max_length {
input_ids.push(PAD_TOKEN_ID);
attention_mask.push(0);
}
Ok((input_ids, attention_mask))
}
fn download_tokenizer(api: &hf_hub::api::sync::ApiRepo) -> Result<Tokenizer> {
println!("🔄 Downloading tokenizer...");
let tokenizer_file = api
.get("tokenizer.json")
.map_err(|e| E::msg(format!("Failed to download tokenizer.json: {e}")))?;
let tokenizer = Tokenizer::from_file(&tokenizer_file)
.map_err(|e| E::msg(format!("Failed to load tokenizer: {e}")))?;
println!("✅ Tokenizer loaded successfully");
Ok(tokenizer)
}
fn get_local_model_path() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(format!(
"{manifest_dir}/models/ane-distilbert/DistilBERT_fp16.mlpackage"
))
}
fn download_model_from_hub(args: &Args) -> Result<PathBuf> {
println!("🔄 Downloading model from {}...", args.model_id);
let repo = Repo::with_revision(
args.model_id.clone(),
RepoType::Model,
args.revision.clone(),
);
let api = Api::new()?;
let api = api.repo(repo);
println!("🔍 Looking for ANE-optimized DistilBERT files...");
let weight_file_path = "DistilBERT_fp16.mlpackage/Data/com.apple.CoreML/weights/weight.bin";
let model_path = match api.get(weight_file_path) {
Ok(weight_file) => {
println!("✅ Successfully connected to model repository");
let weight_parent = weight_file
.parent()
.ok_or_else(|| E::msg("Invalid weight file path: missing parent directory"))?;
let coreml_dir = weight_parent.parent().ok_or_else(|| {
E::msg("Invalid CoreML directory structure: missing com.apple.CoreML parent")
})?;
let data_dir = coreml_dir
.parent()
.ok_or_else(|| E::msg("Invalid data directory structure: missing Data parent"))?;
let mlpackage_path = data_dir.parent().ok_or_else(|| {
E::msg("Invalid mlpackage structure: missing .mlpackage parent directory")
})?;
if args.verbose {
println!("📂 Found model at: {}", mlpackage_path.display());
}
download_additional_model_files(&api, args.verbose)?;
if mlpackage_path.is_dir() {
mlpackage_path.to_path_buf()
} else {
return Err(E::msg(format!(
"Model path exists but is not a valid .mlpackage directory: {}\n\
The model may be incomplete or corrupted.",
mlpackage_path.display()
)));
}
}
Err(e) => {
return Err(E::msg(format!(
"Could not download ANE-optimized DistilBERT model from {}.\n\
Error: {}\n\n\
💡 Try:\n\
- Use --local flag if you have the model locally\n\
- Use --model-path to specify a different model path\n\
- Download manually from: https://huggingface.co/apple/ane-distilbert-base-uncased-finetuned-sst-2-english/tree/main/DistilBERT_fp16.mlpackage\n\
- Check your internet connection",
args.model_id, e
)));
}
};
Ok(model_path)
}
fn download_additional_model_files(api: &hf_hub::api::sync::ApiRepo, verbose: bool) -> Result<()> {
println!("🔄 Downloading additional required files...");
let additional_files = [
"DistilBERT_fp16.mlpackage/Manifest.json",
"DistilBERT_fp16.mlpackage/Data/com.apple.CoreML/model.mlmodel",
];
for file_path in &additional_files {
match api.get(file_path) {
Ok(_) => {
if verbose {
println!("✅ Downloaded: {file_path}");
}
}
Err(e) => {
if verbose {
println!("⚠️ Could not download {file_path}: {e}");
}
}
}
}
Ok(())
}
fn compile_model_if_needed(model_path: PathBuf, verbose: bool) -> Result<PathBuf> {
if model_path.exists() && model_path.extension().and_then(|s| s.to_str()) == Some("mlpackage") {
let cache_dir = model_path
.parent()
.ok_or_else(|| E::msg("Cannot determine parent directory for model compilation cache"))?
.join("compiled_models");
let compiled_model_path = cache_dir.join("DistilBERT_fp16.mlmodelc");
if !compiled_model_path.exists() {
println!(
"🔨 Compiling CoreML model for optimized performance (this may take a moment)..."
);
std::fs::create_dir_all(&cache_dir)?;
let output = std::process::Command::new("xcrun")
.args([
"coremlc",
"compile",
&model_path.to_string_lossy(),
&compiled_model_path.to_string_lossy()
])
.output()
.map_err(|e| E::msg(format!("Failed to run coremlc: {e}. Make sure Xcode command line tools are installed.")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
println!("⚠️ Compilation failed, using .mlpackage directly: {stderr}");
Ok(model_path) } else {
println!("✅ CoreML model compiled successfully");
let nested_path = compiled_model_path.join("DistilBERT_fp16.mlmodelc");
if nested_path.exists() {
if verbose {
println!(
"📁 Using nested compiled model structure: {}",
nested_path.display()
);
}
Ok(nested_path)
} else {
Ok(compiled_model_path)
}
}
} else {
println!("✅ Using cached compiled model");
let nested_path = compiled_model_path.join("DistilBERT_fp16.mlmodelc");
if nested_path.exists() {
Ok(nested_path)
} else {
Ok(compiled_model_path)
}
}
} else {
Ok(model_path)
}
}
fn determine_model_path(args: &Args) -> Result<PathBuf> {
let model_path = if let Some(path) = &args.model_path {
PathBuf::from(path)
} else if args.local {
get_local_model_path()
} else {
download_model_from_hub(args)?
};
compile_model_if_needed(model_path, args.verbose)
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long, default_value = "The Neural Engine is really fast!")]
text: String,
#[arg(short, long)]
model_path: Option<String>,
#[arg(
long,
default_value = "apple/ane-distilbert-base-uncased-finetuned-sst-2-english"
)]
model_id: String,
#[arg(long, default_value = "main")]
revision: String,
#[arg(long, default_value = "128")]
max_length: usize,
#[arg(long)]
show_scores: bool,
#[arg(long)]
local: bool,
#[arg(short, long)]
verbose: bool,
}
#[cfg(target_os = "macos")]
fn run_coreml_inference(args: &Args) -> Result<()> {
use candle_coreml::{Config as CoreMLConfig, CoreMLModel};
println!("🍎 CoreML DistilBERT Sentiment Analysis (ANE-Optimized)");
println!("=========================================================");
println!("Input text: \"{}\"", args.text);
let model_path = determine_model_path(args)?;
let tokenizer = if !args.local && args.model_path.is_none() {
let repo = Repo::with_revision(
args.model_id.clone(),
RepoType::Model,
args.revision.clone(),
);
let api = Api::new()?;
let api = api.repo(repo);
Some(download_tokenizer(&api)?)
} else {
println!(
"⚠️ Using dummy tokenization (real tokenizer not available for local/manual paths)"
);
None
};
if args.verbose {
println!("📂 Final model path: {}", model_path.display());
}
if !model_path.exists() {
return Err(E::msg(format!(
"Model file not found: {}\n\n\
💡 Try:\n\
- Use --local flag for test models\n\
- Use --model-path to specify model location\n\
- Use --model-id to specify HuggingFace repository",
model_path.display()
)));
}
let config = CoreMLConfig {
input_names: vec!["input_ids".to_string(), "attention_mask".to_string()],
output_name: "logits".to_string(),
max_sequence_length: args.max_length,
vocab_size: DISTILBERT_VOCAB_SIZE,
model_type: "ane-distilbert-base-uncased-finetuned-sst-2-english".to_string(),
};
let start = Instant::now();
let model = CoreMLModel::load_from_file(&model_path, &config)
.map_err(|e| E::msg(format!("Failed to load CoreML model: {e}")))?;
let loading_time = start.elapsed();
println!("✅ Model loaded in {loading_time:?}");
println!("📋 Config: {config:?}");
let device = Device::Cpu;
let (input_ids, attention_mask) = if let Some(ref tokenizer) = tokenizer {
println!("🔤 Tokenizing text with DistilBERT tokenizer...");
let (ids, mask) = tokenize_text(&args.text, tokenizer, ANE_SEQUENCE_LENGTH)?;
if args.verbose {
let token_preview: Vec<i64> = ids.iter().take(10).cloned().collect();
println!("🔍 Token IDs (first 10): {token_preview:?}");
if let Ok(encoding) = tokenizer.encode(args.text.as_str(), true) {
let tokens: Vec<String> = encoding
.get_tokens()
.iter()
.take(10)
.map(|s| s.to_string())
.collect();
println!("🔍 Tokens (first 10): {tokens:?}");
}
}
(ids, mask)
} else {
println!("🔤 Using dummy tokenization (demo purposes only)...");
let mut input_ids = Vec::with_capacity(ANE_SEQUENCE_LENGTH);
input_ids.push(CLS_TOKEN_ID);
let demo_tokens: Vec<i64> = (1000..1010).collect();
input_ids.extend(demo_tokens);
input_ids.push(SEP_TOKEN_ID);
while input_ids.len() < ANE_SEQUENCE_LENGTH {
input_ids.push(PAD_TOKEN_ID);
}
let mut attention_mask = vec![1i64; 12]; while attention_mask.len() < ANE_SEQUENCE_LENGTH {
attention_mask.push(0);
}
(input_ids, attention_mask)
};
let input_ids_tensor = Tensor::from_vec(input_ids, (1, ANE_SEQUENCE_LENGTH), &device)?;
let attention_mask_tensor =
Tensor::from_vec(attention_mask, (1, ANE_SEQUENCE_LENGTH), &device)?;
if args.verbose {
println!("🔢 Input shape: {:?}", input_ids_tensor.shape());
println!(
"🎭 Attention mask shape: {:?}",
attention_mask_tensor.shape()
);
}
println!("\n🚀 Running inference...");
let start = Instant::now();
let output = model
.forward(&[&input_ids_tensor, &attention_mask_tensor])
.map_err(|e| E::msg(format!("Inference failed: {e}")))?;
let inference_time = start.elapsed();
println!("✅ Inference completed in {inference_time:?}");
println!("📊 Output shape: {:?}", output.shape());
if let Ok(output_data) = output.to_vec2::<f32>() {
if !output_data.is_empty() && output_data[0].len() >= 2 {
let logits = &output_data[0];
if args.verbose {
println!("🔍 Raw logits: {logits:?}");
}
let negative_score = logits[0];
let positive_score = logits[1];
let exp_neg = negative_score.exp();
let exp_pos = positive_score.exp();
let sum = exp_neg + exp_pos;
let negative_prob = exp_neg / sum;
let positive_prob = exp_pos / sum;
let sentiment = if positive_prob > negative_prob {
"POSITIVE"
} else {
"NEGATIVE"
};
let confidence = positive_prob.max(negative_prob);
println!("\n🎯 Sentiment Analysis Results:");
println!(
" 📊 Prediction: {} ({:.1}% confidence)",
sentiment,
confidence * 100.0
);
if sentiment == "NEGATIVE" && confidence > 0.7 {
let has_positive_words = args.text.to_lowercase().contains("good")
|| args.text.to_lowercase().contains("great")
|| args.text.to_lowercase().contains("love")
|| args.text.to_lowercase().contains("amazing")
|| args.text.to_lowercase().contains("excellent")
|| args.text.to_lowercase().contains("fantastic")
|| args.text.to_lowercase().contains("wonderful");
if has_positive_words {
println!(" ⚠️ Note: This ANE-optimized model shows bias toward negative predictions");
println!(" Consider using standard DistilBERT with Metal backend for better accuracy");
}
}
if args.show_scores {
println!(" 📈 Detailed scores:");
println!(
" • Negative: {:.4} (probability: {:.1}%)",
negative_score,
negative_prob * 100.0
);
println!(
" • Positive: {:.4} (probability: {:.1}%)",
positive_score,
positive_prob * 100.0
);
}
if confidence > 0.8 {
println!(" ⚡ High confidence suggests ANE acceleration was likely used!");
}
} else {
println!("⚠️ Unexpected output format: {:?}", output.shape());
}
} else {
println!("⚠️ Could not process output tensor");
}
println!("\n💡 Performance Summary:");
println!(" • Loading time: {loading_time:?}");
println!(" • Inference time: {inference_time:?}");
println!(" • Total time: {:?}", loading_time + inference_time);
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn run_coreml_inference(_args: &Args) -> Result<()> {
println!("❌ CoreML inference is only available on macOS with the 'coreml' feature enabled.");
println!("\n💡 To use CoreML:");
println!(" • Run on macOS");
println!(" • Build with: cargo run --example bert_inference");
Ok(())
}
fn print_help() {
println!("🤖 ANE-Optimized DistilBERT Sentiment Analysis");
println!("=============================================");
println!();
println!("This example demonstrates sentiment analysis using Apple's ANE-optimized DistilBERT");
println!("model that actually runs on the Apple Neural Engine for maximum performance.");
println!();
println!("📋 Requirements:");
println!(" • macOS (for CoreML support)");
println!(" • ANE-optimized DistilBERT model (.mlpackage format)");
println!(" • Candle built with 'coreml' feature");
println!();
println!("🚀 Quick Start:");
println!(" 1. cargo run --example bert_inference");
println!(" 2. Try different text: --text \"I love this product!\"");
println!(" 3. Show confidence: --show-scores");
println!(" 4. Use local model: --model-path \"/path/to/DistilBERT_fp16.mlpackage\"");
println!();
println!("⚡ This model is specifically optimized to run on Apple's Neural Engine!");
println!("🔗 For more examples, see the benchmarks/ and advanced/ directories.");
println!();
}
fn main() -> Result<()> {
let args = Args::parse();
if args.verbose {
print_help();
}
run_coreml_inference(&args)
}