diff --git a/Cargo.lock b/Cargo.lock
index 58f18ce..9c7d449 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -44,7 +44,7 @@ dependencies = [
"num",
"rayon",
"realfft",
- "reqwest 0.12.28",
+ "reqwest 0.13.2",
"rocket",
"sentencepiece",
"serde",
@@ -2805,9 +2805,9 @@ dependencies = [
[[package]]
name = "modelscope"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c0ce1b3f708ede609c998fc460d5dce1b8b9458f99d71715fc8735f8335d8f3"
+checksum = "e70d3712581a1c6148246dc7cefd63d2dbb3a40ba70602170adbec34bb7319bf"
dependencies = [
"anyhow",
"clap",
@@ -3810,21 +3810,16 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
- "encoding_rs",
- "futures-channel",
"futures-core",
"futures-util",
- "h2 0.4.13",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
- "hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
- "mime",
"mime_guess",
"native-tls",
"percent-encoding",
@@ -3858,6 +3853,7 @@ dependencies = [
"cookie",
"cookie_store",
"encoding_rs",
+ "futures-channel",
"futures-core",
"futures-util",
"h2 0.4.13",
diff --git a/Cargo.toml b/Cargo.toml
index ab4596c..6db4f22 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ serde_json = "1.0.145"
anyhow = "1.0.100"
ffmpeg-next = { version = "8.0.0", optional = true }
image = "0.25.8"
-reqwest = { version = "0.12.23", features = ["blocking"] }
+reqwest = { version = "0.13", features = ["blocking"] }
base64 = "0.22.1"
num = "0.4.3"
minijinja = "2.12.0"
@@ -28,7 +28,7 @@ rocket = { version = "0.5.1", features = ["serde_json", "json"] }
tokio = "1.47.1"
hound = "3.5.1"
clap = { version = "4.5.51", features = ["derive"] }
-modelscope = "0.1.4"
+modelscope = "0.1.5"
dirs = "6.0.0"
sysinfo = "0.33"
url = "2.5.7"
diff --git a/README.md b/README.md
index fcc6932..d63a326 100644
--- a/README.md
+++ b/README.md
@@ -214,7 +214,7 @@ Apache-2.0 — See [LICENSE](LICENSE) for details.
- All model authors and contributors
## Wechat
-
+
---
diff --git a/README.zh-CN.md b/README.zh-CN.md
index dbebe5b..c23ce81 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -218,6 +218,9 @@ Apache-2.0 — 详见 [LICENSE](LICENSE)
- [Candle](https://github.com/huggingface/candle) - 优秀的 Rust 机器学习框架
- 所有模型作者和贡献者
+## Wechat
+
+
---
diff --git a/assets/img/aha_weixingqun.jpg b/assets/img/aha_weixingqun.jpg
deleted file mode 100644
index e6f25db..0000000
Binary files a/assets/img/aha_weixingqun.jpg and /dev/null differ
diff --git a/assets/img/aha_weixinqun.png b/assets/img/aha_weixinqun.png
new file mode 100644
index 0000000..59a59b7
Binary files /dev/null and b/assets/img/aha_weixinqun.png differ
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 4ade969..6cd72cc 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -249,8 +249,10 @@ fn which_model_to_id(which_model: WhichModel) -> &'static str {
WhichModel::MiniCPM4_0_5B => "minicpm4-0.5b",
WhichModel::LFM2_1_2B => "lfm2-1.2b",
WhichModel::LFM2_5_1_2BInstruct => "lfm2.5-1.2b-instruct",
- WhichModel::Qwen2_5vl3B => "qwen2.5vl-3b",
- WhichModel::Qwen2_5vl7B => "qwen2.5vl-7b",
+ WhichModel::LFM2_5VL1_6B => "lfm2.5-vl-1.6b",
+ WhichModel::LFM2VL1_6B => "lfm2-vl-1.6b",
+ WhichModel::Qwen2_5VL3B => "qwen2.5vl-3b",
+ WhichModel::Qwen2_5VL7B => "qwen2.5vl-7b",
WhichModel::Qwen3_0_6B => "qwen3-0.6b",
WhichModel::Qwen3_5_0_8B => "qwen3.5-0.8b",
WhichModel::Qwen3_5_2B => "qwen3.5-2b",
@@ -259,10 +261,10 @@ fn which_model_to_id(which_model: WhichModel) -> &'static str {
WhichModel::Qwen3_5Gguf => "qwen3.5-gguf",
WhichModel::Qwen3ASR0_6B => "qwen3asr-0.6b",
WhichModel::Qwen3ASR1_7B => "qwen3asr-1.7b",
- WhichModel::Qwen3vl2B => "qwen3vl-2b",
- WhichModel::Qwen3vl4B => "qwen3vl-4b",
- WhichModel::Qwen3vl8B => "qwen3vl-8b",
- WhichModel::Qwen3vl32B => "qwen3vl-32b",
+ WhichModel::Qwen3VL2B => "qwen3vl-2b",
+ WhichModel::Qwen3VL4B => "qwen3vl-4b",
+ WhichModel::Qwen3VL8B => "qwen3vl-8b",
+ WhichModel::Qwen3VL32B => "qwen3vl-32b",
WhichModel::DeepSeekOCR => "deepseek-ocr",
WhichModel::DeepSeekOCR2 => "deepseek-ocr2",
WhichModel::HunyuanOCR => "hunyuan-ocr",
@@ -281,12 +283,12 @@ fn which_model_to_id(which_model: WhichModel) -> &'static str {
fn which_model_to_owner(which_model: WhichModel) -> &'static str {
match which_model {
WhichModel::MiniCPM4_0_5B => "OpenBMB",
- WhichModel::Qwen2_5vl3B | WhichModel::Qwen2_5vl7B => "Qwen",
+ WhichModel::Qwen2_5VL3B | WhichModel::Qwen2_5VL7B => "Qwen",
WhichModel::Qwen3_0_6B | WhichModel::Qwen3ASR0_6B | WhichModel::Qwen3ASR1_7B => "Qwen",
- WhichModel::Qwen3vl2B
- | WhichModel::Qwen3vl4B
- | WhichModel::Qwen3vl8B
- | WhichModel::Qwen3vl32B
+ WhichModel::Qwen3VL2B
+ | WhichModel::Qwen3VL4B
+ | WhichModel::Qwen3VL8B
+ | WhichModel::Qwen3VL32B
| WhichModel::Qwen3_5Gguf => "Qwen",
WhichModel::Qwen3_5_0_8B
| WhichModel::Qwen3_5_2B
@@ -299,7 +301,10 @@ fn which_model_to_owner(which_model: WhichModel) -> &'static str {
WhichModel::VoxCPM | WhichModel::VoxCPM1_5 => "OpenBMB",
WhichModel::GlmASRNano2512 | WhichModel::GlmOCR => "ZhipuAI",
WhichModel::FunASRNano2512 => "FunAudioLLM",
- WhichModel::LFM2_1_2B | WhichModel::LFM2_5_1_2BInstruct => "LiquidAI",
+ WhichModel::LFM2_1_2B
+ | WhichModel::LFM2_5_1_2BInstruct
+ | WhichModel::LFM2_5VL1_6B
+ | WhichModel::LFM2VL1_6B => "LiquidAI",
}
}
@@ -380,13 +385,13 @@ mod tests {
#[test]
fn test_get_model_type_llm() {
assert_eq!(WhichModel::Qwen3_0_6B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen3vl2B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen3VL2B.model_type(), "llm");
assert_eq!(WhichModel::MiniCPM4_0_5B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen2_5vl3B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen2_5vl7B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen3vl4B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen3vl8B.model_type(), "llm");
- assert_eq!(WhichModel::Qwen3vl32B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen2_5VL3B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen2_5VL7B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen3VL4B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen3VL8B.model_type(), "llm");
+ assert_eq!(WhichModel::Qwen3VL32B.model_type(), "llm");
}
#[test]
@@ -407,6 +412,10 @@ mod tests {
#[test]
fn test_get_model_type_image() {
assert_eq!(WhichModel::RMBG2_0.model_type(), "image");
+ }
+
+ #[test]
+ fn test_get_model_type_tts() {
assert_eq!(WhichModel::VoxCPM.model_type(), "image");
assert_eq!(WhichModel::VoxCPM1_5.model_type(), "image");
}
diff --git a/src/exec/lfm2.rs b/src/exec/lfm2.rs
index 41986ff..2c99099 100644
--- a/src/exec/lfm2.rs
+++ b/src/exec/lfm2.rs
@@ -1,4 +1,4 @@
-//! MiniCPM4-0.5B exec implementation for CLI `run` subcommand
+//! LFM2 exec implementation for CLI `run` subcommand
use std::time::Instant;
diff --git a/src/exec/lfm2vl.rs b/src/exec/lfm2vl.rs
new file mode 100644
index 0000000..53be6b9
--- /dev/null
+++ b/src/exec/lfm2vl.rs
@@ -0,0 +1,76 @@
+//! LFM2.5VL exec implementation for CLI `run` subcommand
+
+use std::time::Instant;
+
+use anyhow::{Ok, Result};
+
+use crate::exec::ExecModel;
+use crate::models::GenerateModel;
+use crate::models::lfm2vl::generate::Lfm2VLGenerateModel;
+use crate::utils::get_file_path;
+
+pub struct Lfm2VLExec;
+
+impl ExecModel for Lfm2VLExec {
+ fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
+ let input_text = &input[0];
+ let target_text = if input_text.starts_with("file://") {
+ let path = get_file_path(input_text)?;
+ std::fs::read_to_string(path)?
+ } else {
+ input_text.clone()
+ };
+ let url = &input[1];
+ let input_url = if url.starts_with("http://")
+ || url.starts_with("https://")
+ || url.starts_with("file://")
+ {
+ url.clone()
+ } else {
+ format!("file://{}", url)
+ };
+ let i_start = Instant::now();
+ let mut model = Lfm2VLGenerateModel::init(weight_path, None, None)?;
+ let i_duration = i_start.elapsed();
+ println!("Time elapsed in load model is: {:?}", i_duration);
+
+ let message = format!(
+ r#"{{
+ "model": "lfm2.5vl",
+ "messages": [
+ {{
+ "role": "user",
+ "content": [
+ {{
+ "type": "image",
+ "image_url": {{
+ "url": "{}"
+ }}
+ }},
+ {{
+ "type": "text",
+ "text": "{}"
+ }}
+ ]
+ }}
+ ]
+ }}"#,
+ input_url, target_text
+ );
+ let mes = serde_json::from_str(&message)?;
+
+ let i_start = Instant::now();
+ let result = model.generate(mes)?;
+ let i_duration = i_start.elapsed();
+ println!("Time elapsed in generate is: {:?}", i_duration);
+
+ println!("Result: {:?}", result);
+
+ if let Some(out) = output {
+ std::fs::write(out, format!("{:?}", result))?;
+ println!("Output saved to: {}", out);
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/exec/mod.rs b/src/exec/mod.rs
index 0e59094..a2922c8 100644
--- a/src/exec/mod.rs
+++ b/src/exec/mod.rs
@@ -9,6 +9,7 @@ pub mod glm_asr_nano;
pub mod glm_ocr;
pub mod hunyuan_ocr;
pub mod lfm2;
+pub mod lfm2vl;
pub mod minicpm4;
pub mod paddleocr_vl;
pub mod qwen2_5vl;
diff --git a/src/exec/qwen2_5vl.rs b/src/exec/qwen2_5vl.rs
index ecaaa2e..f374902 100644
--- a/src/exec/qwen2_5vl.rs
+++ b/src/exec/qwen2_5vl.rs
@@ -8,9 +8,9 @@ use crate::exec::ExecModel;
use crate::models::{GenerateModel, qwen2_5vl::generate::Qwen2_5VLGenerateModel};
use crate::utils::get_file_path;
-pub struct Qwen2_5vlExec;
+pub struct Qwen2_5VLExec;
-impl ExecModel for Qwen2_5vlExec {
+impl ExecModel for Qwen2_5VLExec {
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
diff --git a/src/exec/qwen3_asr.rs b/src/exec/qwen3_asr.rs
index 2b6bde0..acd13f3 100644
--- a/src/exec/qwen3_asr.rs
+++ b/src/exec/qwen3_asr.rs
@@ -1,4 +1,4 @@
-//! Fun-ASR-Nano-2512 exec implementation for CLI `run` subcommand
+//! Qwen3ASR exec implementation for CLI `run` subcommand
use std::time::Instant;
diff --git a/src/exec/qwen3vl.rs b/src/exec/qwen3vl.rs
index 26c1c0f..7622047 100644
--- a/src/exec/qwen3vl.rs
+++ b/src/exec/qwen3vl.rs
@@ -8,9 +8,9 @@ use crate::exec::ExecModel;
use crate::models::{GenerateModel, qwen3vl::generate::Qwen3VLGenerateModel};
use crate::utils::get_file_path;
-pub struct Qwen3vlExec;
+pub struct Qwen3VLExec;
-impl ExecModel for Qwen3vlExec {
+impl ExecModel for Qwen3VLExec {
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
diff --git a/src/main.rs b/src/main.rs
index 299d77a..19422b3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -246,8 +246,8 @@ fn run_list(args: ListArgs) -> anyhow::Result<()> {
WhichModel::MiniCPM4_0_5B,
WhichModel::LFM2_1_2B,
WhichModel::LFM2_5_1_2BInstruct,
- WhichModel::Qwen2_5vl3B,
- WhichModel::Qwen2_5vl7B,
+ WhichModel::Qwen2_5VL3B,
+ WhichModel::Qwen2_5VL7B,
WhichModel::Qwen3_0_6B,
WhichModel::Qwen3_5_0_8B,
WhichModel::Qwen3_5_2B,
@@ -255,10 +255,10 @@ fn run_list(args: ListArgs) -> anyhow::Result<()> {
WhichModel::Qwen3_5_9B,
WhichModel::Qwen3ASR0_6B,
WhichModel::Qwen3ASR1_7B,
- WhichModel::Qwen3vl2B,
- WhichModel::Qwen3vl4B,
- WhichModel::Qwen3vl8B,
- WhichModel::Qwen3vl32B,
+ WhichModel::Qwen3VL2B,
+ WhichModel::Qwen3VL4B,
+ WhichModel::Qwen3VL8B,
+ WhichModel::Qwen3VL32B,
WhichModel::DeepSeekOCR,
WhichModel::DeepSeekOCR2,
WhichModel::HunyuanOCR,
@@ -471,13 +471,21 @@ fn run_run(args: RunArgs) -> anyhow::Result<()> {
use aha::exec::lfm2::Lfm2Exec;
Lfm2Exec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen2_5vl3B => {
- use aha::exec::qwen2_5vl::Qwen2_5vlExec;
- Qwen2_5vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::LFM2_5VL1_6B => {
+ use aha::exec::lfm2vl::Lfm2VLExec;
+ Lfm2VLExec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen2_5vl7B => {
- use aha::exec::qwen2_5vl::Qwen2_5vlExec;
- Qwen2_5vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::LFM2VL1_6B => {
+ use aha::exec::lfm2vl::Lfm2VLExec;
+ Lfm2VLExec::run(&input, output.as_deref(), &weight_path)?;
+ }
+ WhichModel::Qwen2_5VL3B => {
+ use aha::exec::qwen2_5vl::Qwen2_5VLExec;
+ Qwen2_5VLExec::run(&input, output.as_deref(), &weight_path)?;
+ }
+ WhichModel::Qwen2_5VL7B => {
+ use aha::exec::qwen2_5vl::Qwen2_5VLExec;
+ Qwen2_5VLExec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::Qwen3_0_6B => {
use aha::exec::qwen3::Qwen3Exec;
@@ -511,21 +519,21 @@ fn run_run(args: RunArgs) -> anyhow::Result<()> {
use aha::exec::qwen3_asr::Qwen3ASRExec;
Qwen3ASRExec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen3vl2B => {
- use aha::exec::qwen3vl::Qwen3vlExec;
- Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::Qwen3VL2B => {
+ use aha::exec::qwen3vl::Qwen3VLExec;
+ Qwen3VLExec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen3vl4B => {
- use aha::exec::qwen3vl::Qwen3vlExec;
- Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::Qwen3VL4B => {
+ use aha::exec::qwen3vl::Qwen3VLExec;
+ Qwen3VLExec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen3vl8B => {
- use aha::exec::qwen3vl::Qwen3vlExec;
- Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::Qwen3VL8B => {
+ use aha::exec::qwen3vl::Qwen3VLExec;
+ Qwen3VLExec::run(&input, output.as_deref(), &weight_path)?;
}
- WhichModel::Qwen3vl32B => {
- use aha::exec::qwen3vl::Qwen3vlExec;
- Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?;
+ WhichModel::Qwen3VL32B => {
+ use aha::exec::qwen3vl::Qwen3VLExec;
+ Qwen3VLExec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::DeepSeekOCR => {
use aha::exec::deepseek_ocr::DeepSeekORExec;
diff --git a/src/models/lfm2/model.rs b/src/models/lfm2/model.rs
index 7e25a33..09070a6 100644
--- a/src/models/lfm2/model.rs
+++ b/src/models/lfm2/model.rs
@@ -197,7 +197,7 @@ impl Lfm2DecoderLayer {
}
pub struct Lfm2Decoder {
- embed_tokens: Embedding,
+ pub embed_tokens: Embedding,
layers: Vec,
// rotary_emb: RoPE,
pos_emb: RoPE,
diff --git a/src/models/lfm2vl/config.rs b/src/models/lfm2vl/config.rs
index 6d6872e..483c211 100644
--- a/src/models/lfm2vl/config.rs
+++ b/src/models/lfm2vl/config.rs
@@ -2,7 +2,6 @@ use candle_nn::Activation;
use crate::models::lfm2::config::Lfm2Config;
-
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct Lfm2VLConfig {
pub do_image_splitting: bool,
@@ -19,7 +18,7 @@ pub struct Lfm2VLConfig {
pub projector_bias: bool,
pub projector_hidden_act: Activation,
pub projector_hidden_size: usize,
- pub projector_use_layernorm: bool,
+ pub projector_use_layernorm: Option,
pub text_config: Lfm2Config,
pub tile_size: usize,
pub use_image_special_tokens: bool,
@@ -27,7 +26,6 @@ pub struct Lfm2VLConfig {
pub vision_config: Lfm2VLVisionConfig,
}
-
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct Lfm2VLVisionConfig {
pub attention_dropout: f64,
@@ -37,8 +35,8 @@ pub struct Lfm2VLVisionConfig {
pub intermediate_size: usize,
pub layer_norm_eps: f64,
pub model_type: String,
- pub num_attention_heads: u32,
- pub num_channels: u32,
+ pub num_attention_heads: usize,
+ pub num_channels: usize,
pub num_hidden_layers: usize,
pub num_patches: usize,
pub patch_size: usize,
@@ -79,4 +77,3 @@ pub struct Size {
pub struct Lfm2ProcessorConfig {
pub image_processor: Lfm2ImageConfig,
}
-
diff --git a/src/models/lfm2vl/generate.rs b/src/models/lfm2vl/generate.rs
index cc3ba78..8e32718 100644
--- a/src/models/lfm2vl/generate.rs
+++ b/src/models/lfm2vl/generate.rs
@@ -1,22 +1,28 @@
-use aha_openai_dive::v1::resources::chat::ChatCompletionParameters;
+use aha_openai_dive::v1::resources::chat::{ChatCompletionParameters, ChatCompletionResponse};
use anyhow::Result;
-use candle_core::{DType, Device};
+use candle_core::{DType, Device, Tensor};
+use candle_nn::VarBuilder;
use crate::{
chat_template::ChatTemplate,
models::{
+ GenerateModel,
lfm2::config::Lfm2GenerateConfig,
- lfm2vl::{config::Lfm2VLConfig, processor::Lfm2VLProcessor},
+ lfm2vl::{config::Lfm2VLConfig, model::Lfm2VLModel, processor::Lfm2VLProcessor},
},
tokenizer::TokenizerModel,
- utils::{find_type_files, get_device, get_dtype, get_logit_processor},
+ utils::{
+ build_completion_chunk_response, build_completion_response, find_type_files, get_device,
+ get_dtype, get_logit_processor,
+ },
};
+use rocket::async_stream::stream;
pub struct Lfm2VLGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
device: Device,
- // model: Lfm2VLModel,
+ model: Lfm2VLModel,
processor: Lfm2VLProcessor,
eos_token_id: u32,
model_name: String,
@@ -33,8 +39,8 @@ impl<'a> Lfm2VLGenerateModel<'a> {
let model_path = find_type_files(path, "safetensors")?;
let dtype = get_dtype(dtype, &cfg.dtype);
- // let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_path, dtype, &device)? };
- // let model = Lfm2Model::new(vb, &cfg)?;
+ let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_path, dtype, &device)? };
+ let model = Lfm2VLModel::new(vb, &cfg)?;
let processor = Lfm2VLProcessor::new(path, dtype, &device)?;
let eos_token_id = gen_cfg.eos_token_id;
let model_name = std::path::Path::new(path)
@@ -46,14 +52,16 @@ impl<'a> Lfm2VLGenerateModel<'a> {
chat_template,
tokenizer,
device,
- // model,
+ model,
processor,
eos_token_id,
model_name,
})
}
+}
- pub fn generate(&mut self, mes: ChatCompletionParameters) -> Result<()> {
+impl<'a> GenerateModel for Lfm2VLGenerateModel<'a> {
+ fn generate(&mut self, mes: ChatCompletionParameters) -> Result {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let mut logits = get_logit_processor(
mes.temperature,
@@ -63,11 +71,125 @@ impl<'a> Lfm2VLGenerateModel<'a> {
);
let (pixel_values, pixel_attention_mask, spatial_shapes, text) =
self.processor.process_info(&mes, &mes_render)?;
- let input_ids = self.tokenizer.text_encode(text, &self.device)?;
- println!("pixel_values: {}", pixel_values);
- println!("pixel_attention_mask: {}", pixel_attention_mask);
- println!("spatial_shapes: {}", spatial_shapes);
- println!("input_ids: {}", input_ids);
- Ok(())
+ let mut input_ids = self.tokenizer.text_encode(text, &self.device)?;
+ let mut seq_len = input_ids.dim(1)?;
+ let prompt_tokens = seq_len as u32;
+ let mut seqlen_offset = 0;
+ let mut generate = vec![];
+ let sample_len = mes.max_tokens.unwrap_or(1024);
+ let mut pixel_values = Some(pixel_values);
+ let mut pixel_attention_mask = Some(pixel_attention_mask);
+ let mut spatial_shapes = Some(spatial_shapes);
+ for _ in 0..sample_len {
+ let logit = self.model.forward(
+ &input_ids,
+ pixel_values.as_ref(),
+ pixel_attention_mask.as_ref(),
+ spatial_shapes.as_ref(),
+ seqlen_offset,
+ )?;
+ let logit = logit.squeeze(0)?.squeeze(0)?;
+ let next_token = logits.sample(&logit)?;
+ generate.push(next_token);
+ if next_token == self.eos_token_id {
+ break;
+ }
+ input_ids = Tensor::new(vec![next_token], &self.device)?.unsqueeze(0)?;
+ seqlen_offset += seq_len;
+ seq_len = 1;
+ pixel_values = None;
+ pixel_attention_mask = None;
+ spatial_shapes = None;
+ }
+ self.model.clear_cache();
+ let completion_tokens = generate.len() as u32;
+ let decode = self.tokenizer.token_decode(generate)?;
+ let mes = build_completion_response(
+ decode,
+ &self.model_name,
+ Some(completion_tokens),
+ Some(prompt_tokens),
+ );
+ Ok(mes)
+ }
+
+ fn generate_stream(
+ &mut self,
+ mes: ChatCompletionParameters,
+ ) -> Result<
+ Box<
+ dyn rocket::futures::Stream<
+ Item = Result<
+ aha_openai_dive::v1::resources::chat::ChatCompletionChunkResponse,
+ anyhow::Error,
+ >,
+ > + Send
+ + Unpin
+ + '_,
+ >,
+ > {
+ let mes_render = self.chat_template.apply_chat_template(&mes)?;
+ let mut logits = get_logit_processor(
+ mes.temperature,
+ mes.top_p,
+ None,
+ mes.seed.unwrap_or(34562) as u64,
+ );
+ let (pixel_values, pixel_attention_mask, spatial_shapes, text) =
+ self.processor.process_info(&mes, &mes_render)?;
+ let mut input_ids = self.tokenizer.text_encode(text, &self.device)?;
+ let mut seq_len = input_ids.dim(1)?;
+ let mut seqlen_offset = 0;
+ let sample_len = mes.max_tokens.unwrap_or(1024);
+ let mut pixel_values = Some(pixel_values);
+ let mut pixel_attention_mask = Some(pixel_attention_mask);
+ let mut spatial_shapes = Some(spatial_shapes);
+ let stream = stream! {
+ let mut err_tokens = vec![];
+ for _ in 0..sample_len {
+ let logit = self.model.forward(
+ &input_ids,
+ pixel_values.as_ref(),
+ pixel_attention_mask.as_ref(),
+ spatial_shapes.as_ref(),
+ seqlen_offset,
+ )?;
+ let logit = logit.squeeze(0)?.squeeze(0)?;
+ let next_token = logits.sample(&logit)?;
+ let mut decode_ids = vec![];
+ if !err_tokens.is_empty() {
+ decode_ids.extend_from_slice(&err_tokens);
+ }
+ decode_ids.push(next_token);
+ let decode = self.tokenizer.token_decode(decode_ids)?;
+ if decode.contains("�") {
+ err_tokens.push(next_token);
+ if err_tokens.len() > 3 {
+ err_tokens.clear();
+ }
+ input_ids = Tensor::new(vec![next_token], &self.device)?.unsqueeze(0)?;
+ seqlen_offset += seq_len;
+ seq_len = 1;
+ pixel_values = None;
+ pixel_attention_mask = None;
+ spatial_shapes = None;
+ continue;
+ }
+ err_tokens.clear();
+ let chunk = build_completion_chunk_response(decode, &self.model_name, None, None);
+ yield Ok(chunk);
+ if next_token == self.eos_token_id {
+ break;
+ }
+ input_ids = Tensor::new(vec![next_token], &self.device)?.unsqueeze(0)?;
+ seqlen_offset += seq_len;
+ seq_len = 1;
+ pixel_values = None;
+ pixel_attention_mask = None;
+ spatial_shapes = None;
+ }
+ self.model.clear_cache();
+ };
+ Ok(Box::new(Box::pin(stream)))
}
}
diff --git a/src/models/lfm2vl/mod.rs b/src/models/lfm2vl/mod.rs
index ce9b2a0..8b1baf7 100644
--- a/src/models/lfm2vl/mod.rs
+++ b/src/models/lfm2vl/mod.rs
@@ -1,4 +1,4 @@
pub mod config;
pub mod generate;
pub mod model;
-pub mod processor;
\ No newline at end of file
+pub mod processor;
diff --git a/src/models/lfm2vl/model.rs b/src/models/lfm2vl/model.rs
index e69de29..1199e96 100644
--- a/src/models/lfm2vl/model.rs
+++ b/src/models/lfm2vl/model.rs
@@ -0,0 +1,325 @@
+use crate::{
+ models::{
+ common::{NaiveAttnTwoLinearMLPBlock, get_layer_norm},
+ lfm2::model::Lfm2Decoder,
+ lfm2vl::config::{Lfm2VLConfig, Lfm2VLVisionConfig},
+ },
+ utils::{
+ interpolate::interpolate_bilinear,
+ tensor_utils::{get_equal_mask, masked_scatter_dim0, prepare_mask},
+ },
+};
+use anyhow::{Result, anyhow};
+use candle_core::{D, IndexOp, Tensor};
+use candle_nn::{Activation, LayerNorm, Linear, Module, VarBuilder, embedding, linear_b};
+use num::integer::Roots;
+
+pub struct Siglip2VisionEmbeddings {
+ // embed_dim: usize,
+ // patch_size: usize,
+ patch_embedding: Linear,
+ // position_embedding_size: usize,
+ // position_embedding: Embedding,
+ postitional_embeddings: Tensor,
+}
+
+impl Siglip2VisionEmbeddings {
+ pub fn new(vb: VarBuilder, cfg: &Lfm2VLVisionConfig) -> Result {
+ let embed_dim = cfg.hidden_size;
+ let patch_size = cfg.patch_size;
+ let patch_embedding = linear_b(
+ cfg.num_channels * patch_size * patch_size,
+ embed_dim,
+ true,
+ vb.pp("patch_embedding"),
+ )?;
+ let position_embedding_size = cfg.num_patches.sqrt();
+ let position_embedding =
+ embedding(cfg.num_patches, embed_dim, vb.pp("position_embedding"))?;
+ // (num_patches, embed_dim) -> (h, w, embed_dim)
+ // ->(embed_dim, h, w) -> (1, embe_dim, h, w)
+ let postitional_embeddings = position_embedding
+ .embeddings()
+ .reshape((position_embedding_size, position_embedding_size, ()))?
+ .permute((2, 0, 1))?
+ .unsqueeze(0)?;
+ Ok(Self {
+ // embed_dim,
+ // patch_size,
+ patch_embedding,
+ // position_embedding_size,
+ // position_embedding,
+ postitional_embeddings,
+ })
+ }
+
+ fn resize_positional_embeddings(
+ &self,
+ spatial_shapes: &Tensor,
+ max_length: usize,
+ ) -> Result {
+ let mut result_pos_embeddings = vec![];
+ let bs = spatial_shapes.dim(0)?;
+ for i in 0..bs {
+ let shape_i = spatial_shapes.i(i)?.to_vec1::()?;
+ let height = *shape_i.first().unwrap_or(&32) as usize;
+ let width = *shape_i.get(1).unwrap_or(&32) as usize;
+
+ if height == 0 || width == 0 || height * width > max_length {
+ return Err(anyhow!("img height or width illigal"));
+ }
+ // (1, dim, height, width) -> (height*width, dim)
+ let resize_embeddings = interpolate_bilinear(
+ &self.postitional_embeddings,
+ (height, width),
+ Some(false),
+ Some(true),
+ )?
+ .reshape(((), height * width))?
+ .transpose(0, 1)?;
+ let resize_embeddings = if height * width < max_length {
+ let pad = max_length - height * width;
+ let pad_embedding = resize_embeddings.i(0)?.unsqueeze(0)?.repeat((pad, 1))?;
+ Tensor::cat(&[&resize_embeddings, &pad_embedding], 0)?
+ } else {
+ resize_embeddings
+ };
+ result_pos_embeddings.push(resize_embeddings);
+ }
+
+ let result_pos_embeddings = Tensor::stack(&result_pos_embeddings, 0)?;
+ Ok(result_pos_embeddings)
+ }
+
+ pub fn forward(&self, pixel_values: &Tensor, spatial_shapes: &Tensor) -> Result {
+ let patch_embeds = self.patch_embedding.forward(pixel_values)?;
+ let max_length = pixel_values.dim(1)?;
+ let resize_pos_embeddings =
+ self.resize_positional_embeddings(spatial_shapes, max_length)?;
+ let embedding = patch_embeds.add(&resize_pos_embeddings)?;
+ Ok(embedding)
+ }
+}
+
+pub struct Siglip2Encoder {
+ layers: Vec,
+}
+
+impl Siglip2Encoder {
+ pub fn new(vb: VarBuilder, cfg: &Lfm2VLVisionConfig) -> Result {
+ let vb_layers = vb.pp("layers");
+ let mut layers = vec![];
+ for i in 0..cfg.num_hidden_layers {
+ let layer = NaiveAttnTwoLinearMLPBlock::new(
+ vb_layers.pp(i),
+ cfg.hidden_size,
+ cfg.num_attention_heads,
+ None,
+ None,
+ true,
+ "self_attn",
+ Some("out_proj"),
+ cfg.intermediate_size,
+ cfg.hidden_act,
+ true,
+ "mlp",
+ "fc1",
+ "fc2",
+ cfg.layer_norm_eps,
+ "layer_norm1",
+ "layer_norm2",
+ )?;
+ layers.push(layer);
+ }
+ Ok(Self { layers })
+ }
+
+ pub fn forward(&self, xs: &Tensor, attention_mask: Option<&Tensor>) -> Result {
+ let mut xs = xs.clone();
+ for layer in &self.layers {
+ xs = layer.forward(&xs, None, None, attention_mask, false)?;
+ }
+ Ok(xs)
+ }
+}
+
+pub struct Siglip2VisionModel {
+ embeddings: Siglip2VisionEmbeddings,
+ encoder: Siglip2Encoder,
+ post_layernorm: LayerNorm,
+}
+
+impl Siglip2VisionModel {
+ pub fn new(vb: VarBuilder, cfg: &Lfm2VLVisionConfig) -> Result {
+ let vb = vb.pp("vision_model");
+ let embeddings = Siglip2VisionEmbeddings::new(vb.pp("embeddings"), cfg)?;
+ let encoder = Siglip2Encoder::new(vb.pp("encoder"), cfg)?;
+ let post_layernorm = get_layer_norm(
+ vb.pp("post_layernorm"),
+ cfg.layer_norm_eps,
+ cfg.hidden_size,
+ true,
+ )?;
+ Ok(Self {
+ embeddings,
+ encoder,
+ post_layernorm,
+ })
+ }
+
+ pub fn forward(
+ &self,
+ pixel_values: &Tensor,
+ attention_mask: &Tensor,
+ spatial_shapes: &Tensor,
+ ) -> Result {
+ let xs = self.embeddings.forward(pixel_values, spatial_shapes)?;
+ let mask = prepare_mask(attention_mask)?.to_dtype(xs.dtype())?;
+ let xs = self.encoder.forward(&xs, Some(&mask))?;
+ let xs = self.post_layernorm.forward(&xs)?;
+ Ok(xs)
+ }
+}
+
+pub struct Lfm2VlMultiModalProjector {
+ factor: usize,
+ layer_norm: Option,
+ linear_1: Linear,
+ act: Activation,
+ linear_2: Linear,
+}
+
+impl Lfm2VlMultiModalProjector {
+ pub fn new(vb: VarBuilder, cfg: &Lfm2VLConfig) -> Result {
+ let in_channels = cfg.vision_config.hidden_size * (cfg.downsample_factor).pow(2);
+ let factor = cfg.downsample_factor;
+ let layer_norm = if let Some(flag) = cfg.projector_use_layernorm
+ && !flag
+ {
+ None
+ } else {
+ let layer_norm = get_layer_norm(
+ vb.pp("layer_norm"),
+ cfg.vision_config.layer_norm_eps,
+ in_channels,
+ true,
+ )?;
+ Some(layer_norm)
+ };
+ let linear_1 = linear_b(
+ in_channels,
+ cfg.projector_hidden_size,
+ cfg.projector_bias,
+ vb.pp("linear_1"),
+ )?;
+ let act = cfg.projector_hidden_act;
+ let linear_2 = linear_b(
+ cfg.projector_hidden_size,
+ cfg.text_config.hidden_size,
+ cfg.projector_bias,
+ vb.pp("linear_2"),
+ )?;
+ Ok(Self {
+ factor,
+ layer_norm,
+ linear_1,
+ act,
+ linear_2,
+ })
+ }
+
+ pub fn forward(&self, xs: &Tensor) -> Result {
+ let (bs, w, h, c) = xs.dims4()?;
+ let xs = xs.reshape((bs, w, h / self.factor, c * self.factor))?;
+ let xs = xs.permute((0, 2, 1, 3))?;
+ let xs = xs.reshape((
+ bs,
+ h / self.factor,
+ w / self.factor,
+ c * self.factor * self.factor,
+ ))?;
+ let mut xs = xs.permute((0, 2, 1, 3))?.contiguous()?;
+ if let Some(norm) = &self.layer_norm {
+ xs = norm.forward(&xs)?;
+ }
+ xs = self.linear_1.forward(&xs)?.apply(&self.act)?;
+ xs = self.linear_2.forward(&xs)?;
+ Ok(xs)
+ }
+}
+
+pub struct Lfm2VLModel {
+ vision_tower: Siglip2VisionModel,
+ multi_modal_projector: Lfm2VlMultiModalProjector,
+ language_model: Lfm2Decoder,
+ lm_head: Linear,
+ img_id: u32,
+}
+
+impl Lfm2VLModel {
+ pub fn new(vb: VarBuilder, cfg: &Lfm2VLConfig) -> Result {
+ let vb = vb.pp("model");
+ let vision_tower = Siglip2VisionModel::new(vb.pp("vision_tower"), &cfg.vision_config)?;
+ let multi_modal_projector =
+ Lfm2VlMultiModalProjector::new(vb.pp("multi_modal_projector"), cfg)?;
+ let language_model = Lfm2Decoder::new(vb.pp("language_model"), &cfg.text_config)?;
+ let lm_head = Linear::new(language_model.embed_tokens.embeddings().clone(), None);
+ Ok(Self {
+ vision_tower,
+ multi_modal_projector,
+ language_model,
+ lm_head,
+ img_id: cfg.image_token_id,
+ })
+ }
+
+ pub fn forward(
+ &mut self,
+ input_ids: &Tensor,
+ pixel_values: Option<&Tensor>,
+ pixel_attention_mask: Option<&Tensor>,
+ spatial_shapes: Option<&Tensor>,
+ seqlen_offset: usize,
+ ) -> Result {
+ let mut inputs_embeds = self.language_model.embed_tokens.forward(input_ids)?;
+ if let Some(pixel) = pixel_values
+ && let Some(mask) = pixel_attention_mask
+ && let Some(shapes) = spatial_shapes
+ {
+ let image_embeds = self.vision_tower.forward(pixel, mask, shapes)?;
+ println!("image_embeds: {}", image_embeds);
+ let bs = image_embeds.dim(0)?;
+ let img_featrure_length = mask.sum(1)?.to_vec1::()?;
+ let mut image_features = vec![];
+ for img_idx in 0..bs {
+ let feature = image_embeds.i(img_idx)?;
+ let feature = feature.narrow(0, 0, img_featrure_length[img_idx] as usize)?;
+ let shape = shapes.i(img_idx)?.to_vec1::()?;
+ let h = shape[0];
+ let w = shape[1];
+ let feature = feature
+ .reshape((1, h as usize, w as usize, ()))?
+ .contiguous()?;
+ let img_embedding = self.multi_modal_projector.forward(&feature)?;
+ let dim = img_embedding.dim(D::Minus1)?;
+ let img_embedding = img_embedding.reshape(((), dim))?;
+ image_features.push(img_embedding);
+ }
+ let image_embeds = Tensor::cat(&image_features, 0)?;
+ println!("image_embeds: {}", image_embeds);
+ let image_mask = get_equal_mask(input_ids, self.img_id)?;
+ inputs_embeds = masked_scatter_dim0(&inputs_embeds, &image_embeds, &image_mask)?;
+ }
+ let output = self
+ .language_model
+ .forward(input_ids, Some(&inputs_embeds), seqlen_offset)?;
+ let seq_len = output.dim(1)?;
+ let last = output.narrow(1, seq_len - 1, 1)?;
+ let logits = self.lm_head.forward(&last)?;
+ Ok(logits)
+ }
+
+ pub fn clear_cache(&mut self) {
+ self.language_model.clear_cache();
+ }
+}
diff --git a/src/models/lfm2vl/processor.rs b/src/models/lfm2vl/processor.rs
index 327020e..74236dd 100644
--- a/src/models/lfm2vl/processor.rs
+++ b/src/models/lfm2vl/processor.rs
@@ -33,17 +33,24 @@ pub struct Lfm2VLProcessor {
image_thumbnail_token: String,
}
+#[allow(clippy::type_complexity)]
impl Lfm2VLProcessor {
pub fn new(path: &str, dtype: DType, device: &Device) -> Result {
- let path = path.to_string();
assert!(
- std::path::Path::new(&path).exists(),
+ std::path::Path::new(path).exists(),
"model path file not exists"
);
- let processor_cfg_path = path + "/processor_config.json";
- let processor_cfg: Lfm2ProcessorConfig =
- serde_json::from_slice(&std::fs::read(processor_cfg_path)?)?;
- let image_config = processor_cfg.image_processor;
+ let processor_cfg_path = path.to_string() + "/processor_config.json";
+ let processor_cfg =
+ serde_json::from_slice::(&std::fs::read(processor_cfg_path)?);
+
+ let image_config = match processor_cfg {
+ Ok(cfg) => cfg.image_processor,
+ Err(_) => {
+ let processor_cfg_path = path.to_string() + "/preprocessor_config.json";
+ serde_json::from_slice::(&std::fs::read(processor_cfg_path)?)?
+ }
+ };
// 256
let max_thumbnail_image_patches =
image_config.max_image_tokens * image_config.downsample_factor.pow(2);
@@ -160,7 +167,7 @@ impl Lfm2VLProcessor {
let (new_height, new_width) = img_smart_resize(
height,
width,
- self.total_factor as u32,
+ self.total_factor,
self.smart_resize_min_pixels as u32,
self.smart_resize_max_pixels as u32,
)?;
@@ -331,7 +338,6 @@ impl Lfm2VLProcessor {
) = self.process_imgs(imgs)?;
let text =
self.expand_text_with_placeholders(text, num_cols_list, num_rows_list, image_size_list);
-
Ok((pixel_values, pixel_attention_mask, spatial_shapes, text))
}
}
diff --git a/src/models/mod.rs b/src/models/mod.rs
index c15debb..200780d 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -33,70 +33,74 @@ use crate::models::{
fun_asr_nano::generate::FunAsrNanoGenerateModel,
glm_asr_nano::generate::GlmAsrNanoGenerateModel, glm_ocr::generate::GlmOcrGenerateModel,
hunyuan_ocr::generate::HunyuanOCRGenerateModel, lfm2::generate::Lfm2GenerateModel,
- minicpm4::generate::MiniCPMGenerateModel, paddleocr_vl::generate::PaddleOCRVLGenerateModel,
- qwen2_5vl::generate::Qwen2_5VLGenerateModel, qwen3::generate::Qwen3GenerateModel,
- qwen3_5::generate::Qwen3_5GenerateModel, qwen3_asr::generate::Qwen3AsrGenerateModel,
- qwen3vl::generate::Qwen3VLGenerateModel, rmbg2_0::generate::RMBG2_0Model,
- voxcpm::generate::VoxCPMGenerate,
+ lfm2vl::generate::Lfm2VLGenerateModel, minicpm4::generate::MiniCPMGenerateModel,
+ paddleocr_vl::generate::PaddleOCRVLGenerateModel, qwen2_5vl::generate::Qwen2_5VLGenerateModel,
+ qwen3::generate::Qwen3GenerateModel, qwen3_5::generate::Qwen3_5GenerateModel,
+ qwen3_asr::generate::Qwen3AsrGenerateModel, qwen3vl::generate::Qwen3VLGenerateModel,
+ rmbg2_0::generate::RMBG2_0Model, voxcpm::generate::VoxCPMGenerate,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum WhichModel {
- #[value(name = "lfm2-1.2b", hide = true)]
+ #[value(name = "lfm2-1.2b")]
LFM2_1_2B,
- #[value(name = "lfm2.5-1.2b-instruct", hide = true)]
+ #[value(name = "lfm2.5-1.2b-instruct")]
LFM2_5_1_2BInstruct,
- #[value(name = "minicpm4-0.5b", hide = true)]
+ #[value(name = "lfm2.5-vl-1.6b")]
+ LFM2_5VL1_6B,
+ #[value(name = "lfm2-vl-1.6b")]
+ LFM2VL1_6B,
+ #[value(name = "minicpm4-0.5b")]
MiniCPM4_0_5B,
- #[value(name = "qwen2.5vl-3b", hide = true)]
- Qwen2_5vl3B,
- #[value(name = "qwen2.5vl-7b", hide = true)]
- Qwen2_5vl7B,
- #[value(name = "qwen3-0.6b", hide = true)]
+ #[value(name = "qwen2.5vl-3b")]
+ Qwen2_5VL3B,
+ #[value(name = "qwen2.5vl-7b")]
+ Qwen2_5VL7B,
+ #[value(name = "qwen3-0.6b")]
Qwen3_0_6B,
- #[value(name = "qwen3.5-0.8b", hide = true)]
+ #[value(name = "qwen3.5-0.8b")]
Qwen3_5_0_8B,
- #[value(name = "qwen3.5-2b", hide = true)]
+ #[value(name = "qwen3.5-2b")]
Qwen3_5_2B,
- #[value(name = "qwen3.5-4b", hide = true)]
+ #[value(name = "qwen3.5-4b")]
Qwen3_5_4B,
- #[value(name = "qwen3.5-9b", hide = true)]
+ #[value(name = "qwen3.5-9b")]
Qwen3_5_9B,
- #[value(name = "qwen3.5-gguf", hide = true)]
+ #[value(name = "qwen3.5-gguf")]
Qwen3_5Gguf,
- #[value(name = "qwen3asr-0.6b", hide = true)]
+ #[value(name = "qwen3asr-0.6b")]
Qwen3ASR0_6B,
- #[value(name = "qwen3asr-1.7b", hide = true)]
+ #[value(name = "qwen3asr-1.7b")]
Qwen3ASR1_7B,
- #[value(name = "qwen3vl-2b", hide = true)]
- Qwen3vl2B,
- #[value(name = "qwen3vl-4b", hide = true)]
- Qwen3vl4B,
- #[value(name = "qwen3vl-8b", hide = true)]
- Qwen3vl8B,
- #[value(name = "qwen3vl-32b", hide = true)]
- Qwen3vl32B,
- #[value(name = "deepseek-ocr", hide = true)]
+ #[value(name = "qwen3vl-2b")]
+ Qwen3VL2B,
+ #[value(name = "qwen3vl-4b")]
+ Qwen3VL4B,
+ #[value(name = "qwen3vl-8b")]
+ Qwen3VL8B,
+ #[value(name = "qwen3vl-32b")]
+ Qwen3VL32B,
+ #[value(name = "deepseek-ocr")]
DeepSeekOCR,
- #[value(name = "deepseek-ocr2", hide = true)]
+ #[value(name = "deepseek-ocr2")]
DeepSeekOCR2,
- #[value(name = "hunyuan-ocr", hide = true)]
+ #[value(name = "hunyuan-ocr")]
HunyuanOCR,
- #[value(name = "paddleocr-vl", hide = true)]
+ #[value(name = "paddleocr-vl")]
PaddleOCRVL,
- #[value(name = "paddleocr-vl1.5", hide = true)]
+ #[value(name = "paddleocr-vl1.5")]
PaddleOCRVL1_5,
#[value(name = "rmbg2.0")]
RMBG2_0,
- #[value(name = "voxcpm", hide = true)]
+ #[value(name = "voxcpm")]
VoxCPM,
- #[value(name = "voxcpm1.5", hide = true)]
+ #[value(name = "voxcpm1.5")]
VoxCPM1_5,
- #[value(name = "glm-asr-nano-2512", hide = true)]
+ #[value(name = "glm-asr-nano-2512")]
GlmASRNano2512,
- #[value(name = "fun-asr-nano-2512", hide = true)]
+ #[value(name = "fun-asr-nano-2512")]
FunASRNano2512,
- #[value(name = "glm-ocr", hide = true)]
+ #[value(name = "glm-ocr")]
GlmOCR,
}
@@ -106,9 +110,11 @@ impl WhichModel {
match self {
WhichModel::LFM2_1_2B => "LiquidAI/LFM2-1.2B",
WhichModel::LFM2_5_1_2BInstruct => "LiquidAI/LFM2.5-1.2B-Instruct",
+ WhichModel::LFM2_5VL1_6B => "LiquidAI/LFM2.5-VL-1.6B",
+ WhichModel::LFM2VL1_6B => "LiquidAI/LFM2-VL-1.6B",
WhichModel::MiniCPM4_0_5B => "OpenBMB/MiniCPM4-0.5B",
- WhichModel::Qwen2_5vl3B => "Qwen/Qwen2.5-VL-3B-Instruct",
- WhichModel::Qwen2_5vl7B => "Qwen/Qwen2.5-VL-7B-Instruct",
+ WhichModel::Qwen2_5VL3B => "Qwen/Qwen2.5-VL-3B-Instruct",
+ WhichModel::Qwen2_5VL7B => "Qwen/Qwen2.5-VL-7B-Instruct",
WhichModel::Qwen3_0_6B => "Qwen/Qwen3-0.6B",
WhichModel::Qwen3_5_0_8B => "Qwen/Qwen3.5-0.8B",
WhichModel::Qwen3_5_2B => "Qwen/Qwen3.5-2B",
@@ -117,10 +123,10 @@ impl WhichModel {
WhichModel::Qwen3_5Gguf => "GGUF",
WhichModel::Qwen3ASR0_6B => "Qwen/Qwen3-ASR-0.6B",
WhichModel::Qwen3ASR1_7B => "Qwen/Qwen3-ASR-1.7B",
- WhichModel::Qwen3vl2B => "Qwen/Qwen3-VL-2B-Instruct",
- WhichModel::Qwen3vl4B => "Qwen/Qwen3-VL-4B-Instruct",
- WhichModel::Qwen3vl8B => "Qwen/Qwen3-VL-8B-Instruct",
- WhichModel::Qwen3vl32B => "Qwen/Qwen3-VL-32B-Instruct",
+ WhichModel::Qwen3VL2B => "Qwen/Qwen3-VL-2B-Instruct",
+ WhichModel::Qwen3VL4B => "Qwen/Qwen3-VL-4B-Instruct",
+ WhichModel::Qwen3VL8B => "Qwen/Qwen3-VL-8B-Instruct",
+ WhichModel::Qwen3VL32B => "Qwen/Qwen3-VL-32B-Instruct",
WhichModel::DeepSeekOCR => "deepseek-ai/DeepSeek-OCR",
WhichModel::DeepSeekOCR2 => "deepseek-ai/DeepSeek-OCR-2",
WhichModel::HunyuanOCR => "Tencent-Hunyuan/HunyuanOCR",
@@ -143,17 +149,19 @@ impl WhichModel {
| WhichModel::Qwen3_0_6B
| WhichModel::LFM2_1_2B
| WhichModel::LFM2_5_1_2BInstruct => "llm",
- WhichModel::Qwen2_5vl3B
- | WhichModel::Qwen2_5vl7B
- | WhichModel::Qwen3vl2B
- | WhichModel::Qwen3vl4B
- | WhichModel::Qwen3vl8B
- | WhichModel::Qwen3vl32B
+ WhichModel::Qwen2_5VL3B
+ | WhichModel::Qwen2_5VL7B
+ | WhichModel::Qwen3VL2B
+ | WhichModel::Qwen3VL4B
+ | WhichModel::Qwen3VL8B
+ | WhichModel::Qwen3VL32B
| WhichModel::Qwen3_5_0_8B
| WhichModel::Qwen3_5_2B
| WhichModel::Qwen3_5_4B
| WhichModel::Qwen3_5_9B
- | WhichModel::Qwen3_5Gguf => "vlm",
+ | WhichModel::Qwen3_5Gguf
+ | WhichModel::LFM2_5VL1_6B
+ | WhichModel::LFM2VL1_6B => "vlm",
// OCR models
WhichModel::DeepSeekOCR
| WhichModel::DeepSeekOCR2
@@ -167,7 +175,8 @@ impl WhichModel {
| WhichModel::GlmASRNano2512
| WhichModel::FunASRNano2512 => "asr",
// Image models
- WhichModel::RMBG2_0 | WhichModel::VoxCPM | WhichModel::VoxCPM1_5 => "image",
+ WhichModel::RMBG2_0 => "image",
+ WhichModel::VoxCPM | WhichModel::VoxCPM1_5 => "tts",
}
}
}
@@ -190,6 +199,7 @@ pub trait GenerateModel {
pub enum ModelInstance<'a> {
MiniCPM4(MiniCPMGenerateModel<'a>),
Lfm2(Lfm2GenerateModel<'a>),
+ Lfm2VL(Lfm2VLGenerateModel<'a>),
Qwen2_5VL(Qwen2_5VLGenerateModel<'a>),
Qwen3(Qwen3GenerateModel<'a>),
Qwen3_5(Qwen3_5GenerateModel<'a>),
@@ -210,6 +220,7 @@ impl<'a> GenerateModel for ModelInstance<'a> {
match self {
ModelInstance::MiniCPM4(model) => model.generate(mes),
ModelInstance::Lfm2(model) => model.generate(mes),
+ ModelInstance::Lfm2VL(model) => model.generate(mes),
ModelInstance::Qwen2_5VL(model) => model.generate(mes),
ModelInstance::Qwen3(model) => model.generate(mes),
ModelInstance::Qwen3_5(model) => model.generate(mes),
@@ -240,6 +251,7 @@ impl<'a> GenerateModel for ModelInstance<'a> {
match self {
ModelInstance::MiniCPM4(model) => model.generate_stream(mes),
ModelInstance::Lfm2(model) => model.generate_stream(mes),
+ ModelInstance::Lfm2VL(model) => model.generate_stream(mes),
ModelInstance::Qwen2_5VL(model) => model.generate_stream(mes),
ModelInstance::Qwen3(model) => model.generate_stream(mes),
ModelInstance::Qwen3_5(model) => model.generate_stream(mes),
@@ -276,11 +288,19 @@ pub fn load_model<'a>(
let model = Lfm2GenerateModel::init(path, None, None)?;
ModelInstance::Lfm2(model)
}
- WhichModel::Qwen2_5vl3B => {
+ WhichModel::LFM2_5VL1_6B => {
+ let model = Lfm2VLGenerateModel::init(path, None, None)?;
+ ModelInstance::Lfm2VL(model)
+ }
+ WhichModel::LFM2VL1_6B => {
+ let model = Lfm2VLGenerateModel::init(path, None, None)?;
+ ModelInstance::Lfm2VL(model)
+ }
+ WhichModel::Qwen2_5VL3B => {
let model = Qwen2_5VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen2_5VL(model)
}
- WhichModel::Qwen2_5vl7B => {
+ WhichModel::Qwen2_5VL7B => {
let model = Qwen2_5VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen2_5VL(model)
}
@@ -320,19 +340,19 @@ pub fn load_model<'a>(
let model = Qwen3AsrGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3ASR(model)
}
- WhichModel::Qwen3vl2B => {
+ WhichModel::Qwen3VL2B => {
let model = Qwen3VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3VL(Box::new(model))
}
- WhichModel::Qwen3vl4B => {
+ WhichModel::Qwen3VL4B => {
let model = Qwen3VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3VL(Box::new(model))
}
- WhichModel::Qwen3vl8B => {
+ WhichModel::Qwen3VL8B => {
let model = Qwen3VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3VL(Box::new(model))
}
- WhichModel::Qwen3vl32B => {
+ WhichModel::Qwen3VL32B => {
let model = Qwen3VLGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3VL(Box::new(model))
}
diff --git a/src/utils/tensor_utils.rs b/src/utils/tensor_utils.rs
index fbf2953..e491af3 100644
--- a/src/utils/tensor_utils.rs
+++ b/src/utils/tensor_utils.rs
@@ -1,3 +1,5 @@
+use std::f32;
+
use anyhow::{Result, anyhow};
use candle_core::{D, DType, Device, IndexOp, Tensor, shape::Dim};
use candle_nn::ops::sigmoid;
@@ -32,6 +34,26 @@ pub fn attn_masked_fill(on_true: &Tensor, mask: &Tensor, on_false: f32) -> Resul
Ok(filled)
}
+pub fn prepare_mask(mask: &Tensor) -> Result {
+ //(bs, seq_len)
+ // [[1, 1, 1, 1, 0, 0]]
+ // ->
+ // [[1, 1, 1, 1, 0, 0],
+ // [1, 1, 1, 1, 0, 0],
+ // [1, 1, 1, 1, 0, 0],
+ // [1, 1, 1, 1, 0, 0],
+ // [1, 1, 1, 1, 0, 0],
+ // [1, 1, 1, 1, 0, 0],]
+ // (bs, 1, 1, seq_len)
+ let seq_len = mask.dim(1)?;
+ let mask = mask.unsqueeze(1)?.unsqueeze(1)?;
+ let mask = mask.repeat((1, 1, seq_len, 1))?;
+ let on_true = mask.zeros_like()?.to_dtype(DType::F32)?;
+ let on_false = Tensor::new(f32::NEG_INFINITY, mask.device())?.broadcast_as(mask.shape())?;
+ let mask = mask.where_cond(&on_true, &on_false)?;
+ Ok(mask)
+}
+
pub fn prepare_causal_attention_mask(
b_size: usize,
tgt_len: usize,
diff --git a/tests/config_tests.rs b/tests/config_tests.rs
index 62ca94b..b13fdd6 100644
--- a/tests/config_tests.rs
+++ b/tests/config_tests.rs
@@ -1,5 +1,13 @@
use aha::models::{
- deepseek_ocr::config::DeepseekOCRConfig, hunyuan_ocr::config::HunYuanVLConfig, lfm2::config::Lfm2Config, lfm2vl::config::{Lfm2ProcessorConfig, Lfm2VLConfig}, minicpm4::config::MiniCPM4Config, paddleocr_vl::config::PaddleOCRVLConfig, qwen2_5vl::config::Qwen2_5VLConfig, qwen3vl::config::Qwen3VLConfig, voxcpm::config::VoxCPMConfig
+ deepseek_ocr::config::DeepseekOCRConfig,
+ hunyuan_ocr::config::HunYuanVLConfig,
+ lfm2::config::Lfm2Config,
+ lfm2vl::config::{Lfm2ProcessorConfig, Lfm2VLConfig},
+ minicpm4::config::MiniCPM4Config,
+ paddleocr_vl::config::PaddleOCRVLConfig,
+ qwen2_5vl::config::Qwen2_5VLConfig,
+ qwen3vl::config::Qwen3VLConfig,
+ voxcpm::config::VoxCPMConfig,
};
use anyhow::Result;
@@ -104,7 +112,8 @@ fn lfm2vl_config() -> Result<()> {
let config: Lfm2VLConfig = serde_json::from_slice(&std::fs::read(config_path)?)?;
println!("{:?}", config);
let processor_config_path = model_path.to_string() + "/processor_config.json";
- let processor_config: Lfm2ProcessorConfig = serde_json::from_slice(&std::fs::read(processor_config_path)?)?;
+ let processor_config: Lfm2ProcessorConfig =
+ serde_json::from_slice(&std::fs::read(processor_config_path)?)?;
println!("{:?}", processor_config);
Ok(())
-}
\ No newline at end of file
+}
diff --git a/tests/test_health_models.rs b/tests/test_health_models.rs
index ed0e334..1e3b1eb 100644
--- a/tests/test_health_models.rs
+++ b/tests/test_health_models.rs
@@ -8,8 +8,8 @@ fn test_model_type_classification() {
// Since get_model_type and get_model_id are private to api.rs,
// we document the expected behavior here for reference:
//
- // LLM models: MiniCPM4_0_5B, Qwen2_5vl3B, Qwen2_5vl7B, Qwen3_0_6B,
- // Qwen3vl2B, Qwen3vl4B, Qwen3vl8B, Qwen3vl32B
+ // LLM models: MiniCPM4_0_5B, Qwen2_5VL3B, Qwen2_5VL7B, Qwen3_0_6B,
+ // Qwen3VL2B, Qwen3VL4B, Qwen3VL8B, Qwen3VL32B
// OCR models: DeepSeekOCR, HunyuanOCR, PaddleOCRVL
// ASR models: Qwen3ASR0_6B, Qwen3ASR1_7B, GlmASRNano2512, FunASRNano2512
// Image models: RMBG2_0, VoxCPM, VoxCPM1_5
@@ -17,13 +17,13 @@ fn test_model_type_classification() {
// This test documents the expected model type classification
let llm_models = [
WhichModel::MiniCPM4_0_5B,
- WhichModel::Qwen2_5vl3B,
- WhichModel::Qwen2_5vl7B,
+ WhichModel::Qwen2_5VL3B,
+ WhichModel::Qwen2_5VL7B,
WhichModel::Qwen3_0_6B,
- WhichModel::Qwen3vl2B,
- WhichModel::Qwen3vl4B,
- WhichModel::Qwen3vl8B,
- WhichModel::Qwen3vl32B,
+ WhichModel::Qwen3VL2B,
+ WhichModel::Qwen3VL4B,
+ WhichModel::Qwen3VL8B,
+ WhichModel::Qwen3VL32B,
];
let ocr_models = [
diff --git a/tests/test_lfm2.rs b/tests/test_lfm2.rs
index aea053a..08cff28 100644
--- a/tests/test_lfm2.rs
+++ b/tests/test_lfm2.rs
@@ -9,19 +9,18 @@ use std::{pin::pin, time::Instant};
#[test]
fn lfm2_generate() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_lfm2 lfm2_generate -r -- --nocapture
- // test with cuda+flash-attn: RUST_BACKTRACE=1 cargo test -F cuda,flash-attn qwen3_0_6b_generate -r -- --nocapture
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
- // let model_path = format!("{}/LiquidAI/LFM2-1.2B/", save_dir);
- let model_path = format!("{}/LiquidAI/LFM2.5-1.2B-Instruct/", save_dir);
+ let model_path = format!("{}/LiquidAI/LFM2-1.2B/", save_dir);
+ // let model_path = format!("{}/LiquidAI/LFM2.5-1.2B-Instruct/", save_dir);
let message = r#"
{
"model": "lfm2",
"messages": [
{
"role": "user",
- "content": "你如何看待AI"
+ "content": "你是谁,你如何看待AI"
}
]
}
diff --git a/tests/test_lfm2vl.rs b/tests/test_lfm2vl.rs
index 9665d5d..22bbbb1 100644
--- a/tests/test_lfm2vl.rs
+++ b/tests/test_lfm2vl.rs
@@ -1,13 +1,71 @@
-use std::time::Instant;
+use std::{pin::pin, time::Instant};
-use aha::{chat::ChatCompletionParameters, models::lfm2vl::generate::Lfm2VLGenerateModel};
+use aha::{
+ chat::ChatCompletionParameters,
+ models::{GenerateModel, lfm2vl::generate::Lfm2VLGenerateModel},
+};
use anyhow::Result;
+use rocket::futures::StreamExt;
#[test]
fn lfm2vl_generate() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_lfm2vl lfm2vl_generate -r -- --nocapture
-
+
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
+ // let model_path = format!("{}/LiquidAI/LFM2.5-VL-1.6B/", save_dir);
+ let model_path = format!("{}/LiquidAI/LFM2-VL-1.6B/", save_dir);
+ let message = r#"
+ {
+ "model": "lfm2vl",
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image",
+ "image_url":
+ {
+ "url": "file://./assets/img/ocr_test1.png"
+ }
+ },
+ {
+ "type": "text",
+ "text": "图片里面是什么"
+ }
+ ]
+ }
+ ]
+ }
+ "#;
+ let mes: ChatCompletionParameters = serde_json::from_str(message)?;
+ let i_start = Instant::now();
+ let mut model = Lfm2VLGenerateModel::init(&model_path, None, None)?;
+ let i_duration = i_start.elapsed();
+ println!("Time elapsed in load model is: {:?}", i_duration);
+
+ let i_start = Instant::now();
+ let result = model.generate(mes)?;
+ let i_duration = i_start.elapsed();
+ println!("generate: \n {:?}", result);
+ if let Some(usage) = &result.usage {
+ let num_token = usage.total_tokens;
+ let duration_secs = i_duration.as_secs_f64();
+ let tps = num_token as f64 / duration_secs;
+ println!("Tokens per second (TPS): {:.2}", tps);
+ }
+ println!("Time elapsed in generate is: {:?}", i_duration);
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn lfm2vl_stream() -> Result<()> {
+ // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_lfm2vl lfm2vl_stream -r -- --nocapture
+ // test with cuda+flash-attn: RUST_BACKTRACE=1 cargo test -F cuda,flash-attn qwen3_0_6b_generate -r -- --nocapture
+
+ let save_dir =
+ aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
+ // let model_path = format!("{}/LiquidAI/LFM2-1.2B/", save_dir);
let model_path = format!("{}/LiquidAI/LFM2.5-VL-1.6B/", save_dir);
let message = r#"
{
@@ -39,16 +97,13 @@ fn lfm2vl_generate() -> Result<()> {
println!("Time elapsed in load model is: {:?}", i_duration);
let i_start = Instant::now();
- let result = model.generate(mes)?;
+ // let result = model.generate(mes)?;
+ let mut stream = pin!(model.generate_stream(mes)?);
let i_duration = i_start.elapsed();
- // println!("generate: \n {:?}", result);
- // if let Some(usage) = &result.usage {
- // let num_token = usage.total_tokens;
- // let duration_secs = i_duration.as_secs_f64();
- // let tps = num_token as f64 / duration_secs;
- // println!("Tokens per second (TPS): {:.2}", tps);
- // }
- // println!("Time elapsed in generate is: {:?}", i_duration);
+ while let Some(token) = stream.next().await {
+ println!("generate: \n {:?}", token);
+ }
+ println!("Time elapsed in generate is: {:?}", i_duration);
Ok(())
-}
\ No newline at end of file
+}
diff --git a/tests/weight_test.rs b/tests/weight_test.rs
index cd949b8..f80389d 100644
--- a/tests/weight_test.rs
+++ b/tests/weight_test.rs
@@ -287,3 +287,26 @@ fn lfm2_weight() -> Result<()> {
println!("model_list: {:?}", model_list);
Ok(())
}
+
+#[test]
+fn lfm2vl_weight() -> Result<()> {
+ // cargo test -F cuda --test weight_test lfm2vl_weight -r -- --nocapture
+ let save_dir =
+ aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
+ // let model_path = format!("{}/LiquidAI/LFM2.5-VL-1.6B/", save_dir);
+ let model_path = format!("{}/LiquidAI/LFM2-VL-1.6B/", save_dir);
+ let model_list = find_type_files(&model_path, "safetensors")?;
+
+ let device = Device::Cpu;
+ for m in &model_list {
+ let weights = safetensors::load(m, &device)?;
+ for (key, tensor) in weights.iter() {
+ // if key.contains("lm_head") {
+ // println!("=== {} === {:?}", key, tensor.shape());
+ // }
+ println!("=== {} === {:?}", key, tensor.shape());
+ }
+ }
+ println!("model_list: {:?}", model_list);
+ Ok(())
+}