#[cfg(feature = "mkl")]
extern crate intel_mkl_src;
#[cfg(feature = "accelerate")]
extern crate accelerate_src;
use anyhow::{Error as E, Result};
use clap::Parser;
use std::io::Write;
use std::sync::Arc;
use fuel::lazy::Tensor;
use fuel::lazy_llava::{HFLlavaConfig, LlavaModel, LlavaWeights, select_best_resolution};
use fuel::safetensors::MmapedSafetensors;
use fuel::{Device, Shape};
use hf_hub::{Repo, RepoType, api::sync::Api};
use tokenizers::Tokenizer;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(long)]
image: String,
#[arg(long)]
prompt: String,
#[arg(long)]
model_id: Option<String>,
#[arg(long, default_value = "main")]
revision: String,
#[arg(long)]
tokenizer_file: Option<String>,
#[arg(long)]
weight_files: Option<String>,
#[arg(long)]
config_file: Option<String>,
#[arg(long)]
cpu: bool,
#[arg(long, default_value_t = 256)]
sample_len: usize,
#[arg(long)]
grid_pinpoints: Option<String>,
}
fn parse_pinpoints(s: &str) -> Result<Vec<(u32, u32)>> {
s.split(',')
.filter(|p| !p.is_empty())
.map(|p| {
let (w, h) = p
.split_once('x')
.ok_or_else(|| E::msg(format!("pinpoint {p:?} not in WxH form")))?;
let w: u32 = w.trim().parse().map_err(E::msg)?;
let h: u32 = h.trim().parse().map_err(E::msg)?;
Ok((w, h))
})
.collect()
}
fn load_image_chw<P: AsRef<std::path::Path>>(path: P, image_size: usize) -> Result<Vec<f32>> {
let img = image::ImageReader::open(path)?.decode()?;
let img = img.resize_to_fill(
image_size as u32,
image_size as u32,
image::imageops::FilterType::Triangle,
);
let raw = img.to_rgb8().into_raw();
let mean = [0.481_454_66_f32, 0.457_827_5, 0.408_210_73];
let std = [0.268_629_54_f32, 0.261_302_6, 0.275_777_1];
let h = image_size;
let w = image_size;
let mut out = vec![0.0_f32; 3 * h * w];
for y in 0..h {
for x in 0..w {
for c in 0..3 {
let v = raw[(y * w + x) * 3 + c] as f32 / 255.0;
out[(c * h + y) * w + x] = (v - mean[c]) / std[c];
}
}
}
Ok(out)
}
fn main() -> Result<()> {
let args = Args::parse();
let _ = args.cpu;
let model_id = args
.model_id
.clone()
.unwrap_or_else(|| "llava-hf/llava-1.5-7b-hf".to_string());
let api = Api::new()?;
let repo = api.repo(Repo::with_revision(
model_id.clone(),
RepoType::Model,
args.revision.clone(),
));
let config_path = match args.config_file {
Some(p) => std::path::PathBuf::from(p),
None => repo.get("config.json")?,
};
let tokenizer_path = match args.tokenizer_file {
Some(p) => std::path::PathBuf::from(p),
None => repo.get("tokenizer.json")?,
};
let weight_files: Vec<std::path::PathBuf> = match &args.weight_files {
Some(files) => files.split(',').map(std::path::PathBuf::from).collect(),
None => fuel_examples::hub_load_safetensors(&repo, "model.safetensors.index.json")?,
};
let config_str = std::fs::read_to_string(&config_path)?;
let config = HFLlavaConfig::from_hf_json_str(&config_str)
.map_err(|e| E::msg(format!("parse llava config: {e}")))?;
let img_size = config.vision_config.image_size;
println!(
"config: vision {}x{} (patch {}), text dim {} / {} layers / vocab {}",
img_size,
img_size,
config.vision_config.patch_size,
config.text_config.dim,
config.text_config.n_layers,
config.text_config.vocab_size,
);
if let Some(s) = args.grid_pinpoints.as_deref() {
let pins = parse_pinpoints(s)?;
if !pins.is_empty() {
let (orig_w, orig_h) = {
let img = image::ImageReader::open(&args.image)?.decode()?;
(img.width(), img.height())
};
let pick = select_best_resolution((orig_w, orig_h), &pins);
println!(
"pinpoints: original {orig_w}x{orig_h} → best fit {}x{} \
(v1 still resizes to {img_size}x{img_size} after this)",
pick.0, pick.1,
);
}
}
let tokenizer = Tokenizer::from_file(&tokenizer_path).map_err(E::msg)?;
let pixel_chw = load_image_chw(&args.image, img_size)?;
println!("loaded image (1, 3, {img_size}, {img_size})");
let pixel_values = Tensor::from_f32(
Arc::<[f32]>::from(pixel_chw),
Shape::from_dims(&[1, 3, img_size, img_size]),
&Device::cpu(),
)?;
let load_start = std::time::Instant::now();
let st = unsafe { MmapedSafetensors::multi(&weight_files) }
.map_err(|e| E::msg(format!("mmap safetensors: {e}")))?;
let weights = LlavaWeights::load_from_mmapped(&st, &config)
.map_err(|e| E::msg(format!("load llava weights: {e}")))?;
let model = LlavaModel {
config: config.clone(),
weights,
};
println!("loaded weights in {:?}", load_start.elapsed());
let prompt = args.prompt.clone();
print!("{prompt}");
std::io::stdout().flush()?;
let mut tokens: Vec<u32> = tokenizer
.encode(prompt, true)
.map_err(E::msg)?
.get_ids()
.to_vec();
let np = config.vision_config.num_patches();
let vocab_size = config.text_config.vocab_size;
let eos_token_id = tokenizer
.token_to_id("</s>")
.or_else(|| tokenizer.token_to_id("<|endoftext|>"));
let gen_start = std::time::Instant::now();
let mut generated = 0_usize;
for _ in 0..args.sample_len {
let logits = model
.forward(&pixel_values, &tokens)
.map_err(|e| E::msg(format!("llava forward: {e}")))?;
let data = logits.realize_f32();
let seq = np + tokens.len();
let off = (seq - 1) * vocab_size;
let last = &data[off..off + vocab_size];
let mut best_i = 0_usize;
let mut best = last[0];
for (i, &v) in last.iter().enumerate().skip(1) {
if v > best {
best = v;
best_i = i;
}
}
let next = best_i as u32;
if Some(next) == eos_token_id {
break;
}
tokens.push(next);
generated += 1;
let piece = tokenizer.decode(&[next], true).map_err(E::msg)?;
print!("{piece}");
std::io::stdout().flush()?;
}
let dt = gen_start.elapsed();
println!(
"\n{generated} tokens generated in {:?} ({:.2} tok/s)",
dt,
generated as f64 / dt.as_secs_f64(),
);
Ok(())
}