From 9224593b78723f568937487c7ea194b033d3fdc3 Mon Sep 17 00:00:00 2001 From: jhqxxx <18280426169@163.com> Date: Mon, 23 Mar 2026 22:07:41 +0800 Subject: [PATCH] add LFM2-1.2B, LFM2.5-1.2B-Instruct --- Cargo.lock | 2 +- Cargo.toml | 4 +- README.md | 13 +- README.zh-CN.md | 8 +- docs/changelog.md | 4 + docs/changelog.zh-CN.md | 4 + docs/supported-models.md | 2 + docs/supported-models.zh-CN.md | 2 + src/api/mod.rs | 3 + src/exec/lfm2.rs | 61 +++++++ src/exec/mod.rs | 1 + src/main.rs | 10 ++ src/models/common/mod.rs | 139 +++++++++++++++ src/models/lfm2/config.rs | 88 +++++++++ src/models/lfm2/generate.rs | 163 +++++++++++++++++ src/models/lfm2/mod.rs | 3 + src/models/lfm2/model.rs | 313 +++++++++++++++++++++++++++++++++ src/models/mod.rs | 31 +++- src/models/qwen3/model.rs | 22 ++- src/models/qwen3_5/generate.rs | 7 +- src/tokenizer/mod.rs | 8 + tests/config_tests.rs | 18 +- tests/messy_test.rs | 20 ++- tests/test_lfm2.rs | 91 ++++++++++ tests/test_qwen3.rs | 2 +- tests/weight_test.rs | 22 +++ 26 files changed, 1001 insertions(+), 40 deletions(-) create mode 100644 src/exec/lfm2.rs create mode 100644 src/models/lfm2/config.rs create mode 100644 src/models/lfm2/generate.rs create mode 100644 src/models/lfm2/mod.rs create mode 100644 src/models/lfm2/model.rs create mode 100644 tests/test_lfm2.rs diff --git a/Cargo.lock b/Cargo.lock index 3c56c66..58f18ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "aha" -version = "0.2.3" +version = "0.2.4" dependencies = [ "aha_openai_dive", "ahash", diff --git a/Cargo.toml b/Cargo.toml index f3b6db0..ab4596c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "aha" -version = "0.2.3" +version = "0.2.4" edition = "2024" repository = "https://github.com/jhqxxx/aha" license = "Apache-2.0" -description = "aha model inference library, now supports Qwen(2.5VL/3/3VL/3.5/ASR), MiniCPM4, VoxCPM/1.5, DeepSeek-OCR/2, Hunyuan-OCR, PaddleOCR-VL/1.5, RMBG2.0, GLM(ASR-Nano-2512/OCR), Fun-ASR-Nano-2512" +description = "aha model inference library, now supports Qwen(2.5VL/3/3VL/3.5/ASR), MiniCPM4, VoxCPM/1.5, DeepSeek-OCR/2, Hunyuan-OCR, PaddleOCR-VL/1.5, RMBG2.0, GLM(ASR-Nano-2512/OCR), Fun-ASR-Nano-2512, LFM(2/2.5)" [dependencies] candle-core = { version = "0.9.2" } diff --git a/README.md b/README.md index 5bf72ac..5ab8250 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ aha is a high-performance, cross-platform AI inference engine built with Rust and the Candle framework. It brings state-of-the-art AI models to your local machine—no API keys, no cloud dependencies, just pure, fast AI running directly on your hardware. ## Changelog +### v0.2.4 (2026-03-23) +- add LFM2.5-1.2B-Instruct +- add LFM2-1.2B + ### v0.2.3 (2026-03-18) - add DeepSeek-OCR-2 @@ -49,9 +53,6 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an ### v0.2.1 (2026-03-05) - Added Qwen3.5 model -### 2026-03-01 -- update interpolate.rs - **[View full changelog](docs/changelog.md)** → @@ -125,9 +126,9 @@ curl http://localhost:10100/chat/completions \ | Category | Models | |----------|--------| -| **Text** | Qwen3, MiniCPM4 | -| **Vision** | Qwen2.5-VL, Qwen3-VL | -| **OCR** | DeepSeek-OCR, Hunyuan-OCR, PaddleOCR-VL | +| **Text** | Qwen3, MiniCPM4,
LFM2-1.2B, LFM2.5-1.2B-Instruct | +| **Vision** | Qwen2.5-VL, Qwen3-VL, Qwen3.5 | +| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2 ,
Hunyuan-OCR, PaddleOCR-VL, PaddleOCR-VL1.5 | | **ASR** | GLM-ASR-Nano, Fun-ASR-Nano, Qwen3-ASR | | **Audio** | VoxCPM, VoxCPM1.5 | | **Image** | RMBG-2.0 (background removal) | diff --git a/README.zh-CN.md b/README.zh-CN.md index b7b0f8d..00bd0fb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -25,6 +25,10 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理引擎。将最先进的 AI 模型带到您的本地机器——无需 API 密钥,无需云依赖,纯粹、快速的 AI,直接在您的硬件上运行。 ## 更新日志 +### v0.2.4 (2026-03-23) +- 新增 LFM2.5-1.2B-Instruct +- 新增 LFM2-1.2B + ### v0.2.3 (2026-03-18) - 新增 DeepSeek-OCR-2 @@ -124,9 +128,9 @@ curl http://localhost:10100/chat/completions \ | 类别 | 模型 | |------|------| -| **文本** | Qwen3, MiniCPM4 | +| **文本** | Qwen3, MiniCPM4,
LFM2-1.2B, LFM2.5-1.2B-Instruct | | **视觉** | Qwen2.5-VL, Qwen3-VL | -| **OCR** | DeepSeek-OCR, Hunyuan-OCR, PaddleOCR-VL | +| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2 ,
Hunyuan-OCR, PaddleOCR-VL, PaddleOCR-VL1.5 | | **ASR** | GLM-ASR-Nano, Fun-ASR-Nano,Qwen3-ASR | | **音频** | VoxCPM, VoxCPM1.5 | | **图像** | RMBG-2.0 (背景移除) | diff --git a/docs/changelog.md b/docs/changelog.md index 4c47692..f3ea4e3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,10 @@ All notable changes to aha will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### v0.2.4 (2026-03-23) +- add LFM2.5-1.2B-Instruct +- add LFM2-1.2B + ### v0.2.3 (2026-03-18) - add DeepSeek-OCR-2 diff --git a/docs/changelog.zh-CN.md b/docs/changelog.zh-CN.md index 5f5a4e3..5355518 100644 --- a/docs/changelog.zh-CN.md +++ b/docs/changelog.zh-CN.md @@ -5,6 +5,10 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。 +### v0.2.4 (2026-03-23) +- 新增 LFM2.5-1.2B-Instruct +- 新增 LFM2-1.2B + ### v0.2.3 (2026-03-18) - 新增 DeepSeek-OCR-2 diff --git a/docs/supported-models.md b/docs/supported-models.md index 436ba73..606b772 100644 --- a/docs/supported-models.md +++ b/docs/supported-models.md @@ -8,6 +8,8 @@ aha supports a growing collection of state-of-the-art AI models across multiple |-------|-----------|-------------|----------|---------| | **Qwen3-0.6B** | 0.6B | Latest generation | Advanced reasoning | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) | | **MiniCPM4-0.5B** | 0.5B | Efficient lightweight | Edge deployment | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) | +| **LFM2-1.2B** | 1.2B | Efficient lightweight | Edge deployment | [lfm1.0](https://huggingface.co/LiquidAI/LFM2-1.2B/blob/main/LICENSE) | +| **LFM2.5-1.2B-Instruct** | 1.2B | Efficient lightweight | Edge deployment | [lfm1.0](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct/blob/main/LICENSE) | ## Vision & Multimodal diff --git a/docs/supported-models.zh-CN.md b/docs/supported-models.zh-CN.md index f24305d..a1b26de 100644 --- a/docs/supported-models.zh-CN.md +++ b/docs/supported-models.zh-CN.md @@ -8,6 +8,8 @@ aha 支持多个领域的最先进 AI 模型集合。 |------|--------|------|----------|---------| | **Qwen3-0.6B** | 0.6B | 最新一代 | 高级推理 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) | | **MiniCPM4-0.5B** | 0.5B | 高效轻量级 | 边缘部署 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) | +| **LFM2-1.2B** | 1.2B | 高效轻量级 | 边缘部署 | [lfm1.0](https://huggingface.co/LiquidAI/LFM2-1.2B/blob/main/LICENSE) | +| **LFM2.5-1.2B-Instruct** | 1.2B | 高效轻量级 | 边缘部署 | [lfm1.0](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct/blob/main/LICENSE) | ## 视觉与多模态 diff --git a/src/api/mod.rs b/src/api/mod.rs index 73074b9..4ade969 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -247,6 +247,8 @@ struct ErrorResponse { fn which_model_to_id(which_model: WhichModel) -> &'static str { match which_model { 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::Qwen3_0_6B => "qwen3-0.6b", @@ -297,6 +299,7 @@ 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", } } diff --git a/src/exec/lfm2.rs b/src/exec/lfm2.rs new file mode 100644 index 0000000..41986ff --- /dev/null +++ b/src/exec/lfm2.rs @@ -0,0 +1,61 @@ +//! MiniCPM4-0.5B 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::lfm2::generate::Lfm2GenerateModel; +use crate::utils::get_file_path; + +pub struct Lfm2Exec; + +impl ExecModel for Lfm2Exec { + 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 = &input[7..]; + let path = get_file_path(input_text)?; + std::fs::read_to_string(path)? + } else { + input_text.to_string() + }; + + let i_start = Instant::now(); + let mut model = Lfm2GenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "temperature": 0.3, + "top_p": 0.8, + "model": "lfm2", + "messages": [ + {{ + "role": "user", + "content": "{}" + }} + ] + }}"#, + target_text.replace('"', "\\\"") + ); + 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); + + // Print result + 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 1e2c08e..191024a 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -18,6 +18,7 @@ pub mod qwen3vl; pub mod rmbg2_0; pub mod voxcpm; pub mod voxcpm1_5; +pub mod lfm2; use anyhow::Result; diff --git a/src/main.rs b/src/main.rs index 99ae64e..299d77a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -244,6 +244,8 @@ struct ModelInfo { fn run_list(args: ListArgs) -> anyhow::Result<()> { let models = [ WhichModel::MiniCPM4_0_5B, + WhichModel::LFM2_1_2B, + WhichModel::LFM2_5_1_2BInstruct, WhichModel::Qwen2_5vl3B, WhichModel::Qwen2_5vl7B, WhichModel::Qwen3_0_6B, @@ -461,6 +463,14 @@ fn run_run(args: RunArgs) -> anyhow::Result<()> { use aha::exec::minicpm4::MiniCPM4Exec; MiniCPM4Exec::run(&input, output.as_deref(), &weight_path)?; } + WhichModel::LFM2_1_2B => { + use aha::exec::lfm2::Lfm2Exec; + Lfm2Exec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::LFM2_5_1_2BInstruct => { + 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)?; diff --git a/src/models/common/mod.rs b/src/models/common/mod.rs index 8d871c2..d859dc5 100644 --- a/src/models/common/mod.rs +++ b/src/models/common/mod.rs @@ -411,6 +411,145 @@ impl QKVCatAttention { } } +pub struct QKNormAttention { + q_proj: Linear, + k_proj: Linear, + v_proj: Linear, + o_proj: Linear, + q_norm: RmsNorm, + k_norm: RmsNorm, + num_attention_heads: usize, + num_key_value_heads: usize, + num_kv_groups: usize, + head_dim: usize, + scaling: f64, + kv_cache: Option<(Tensor, Tensor)>, +} + +impl QKNormAttention { + pub fn new( + vb: VarBuilder, + hidden_size: usize, + num_attention_heads: usize, + head_dim: Option, + num_key_value_heads: Option, + attention_bias: bool, + rms_norm_eps: f64, + q_proj_pp_name: Option<&str>, + k_proj_pp_name: Option<&str>, + v_proj_pp_name: Option<&str>, + o_proj_pp_name: Option<&str>, + q_norm_pp_name: Option<&str>, + k_norm_pp_name: Option<&str>, + ) -> Result { + let head_dim = head_dim.unwrap_or(hidden_size / num_attention_heads); + let num_key_value_heads = num_key_value_heads.unwrap_or(num_attention_heads); + let num_kv_groups = num_attention_heads / num_key_value_heads; + let scaling = 1f64 / f64::sqrt(head_dim as f64); + let q_proj_pp_name = q_proj_pp_name.unwrap_or("q_proj"); + let k_proj_pp_name = k_proj_pp_name.unwrap_or("k_proj"); + let v_proj_pp_name = v_proj_pp_name.unwrap_or("v_proj"); + let o_proj_pp_name = o_proj_pp_name.unwrap_or("o_proj"); + let q_norm_pp_name = q_norm_pp_name.unwrap_or("q_norm"); + let k_norm_pp_name = k_norm_pp_name.unwrap_or("k_norm"); + let q_proj = linear_b( + hidden_size, + num_attention_heads * head_dim, + attention_bias, + vb.pp(q_proj_pp_name), + )?; + let k_proj = linear_b( + hidden_size, + num_key_value_heads * head_dim, + attention_bias, + vb.pp(k_proj_pp_name), + )?; + let v_proj = linear_b( + hidden_size, + num_key_value_heads * head_dim, + attention_bias, + vb.pp(v_proj_pp_name), + )?; + let o_proj = linear_b( + num_attention_heads * head_dim, + hidden_size, + attention_bias, + vb.pp(o_proj_pp_name), + )?; + let q_norm = rms_norm(head_dim, rms_norm_eps, vb.pp(q_norm_pp_name))?; + let k_norm = rms_norm(head_dim, rms_norm_eps, vb.pp(k_norm_pp_name))?; + Ok(Self { + q_proj, + k_proj, + v_proj, + o_proj, + q_norm, + k_norm, + num_attention_heads, + num_key_value_heads, + num_kv_groups, + head_dim, + scaling, + kv_cache: None, + }) + } + + pub fn forward( + &mut self, + xs: &Tensor, + cos: &Tensor, + sin: &Tensor, + attention_mask: Option<&Tensor>, + ) -> Result { + let (b_sz, q_len, _) = xs.dims3()?; + let query_states = self.q_proj.forward(xs)?.reshape(( + b_sz, + q_len, + self.num_attention_heads, + self.head_dim, + ))?; + let query_states = self.q_norm.forward(&query_states)?.transpose(1, 2)?; + let key_states = self.k_proj.forward(xs)?.reshape(( + b_sz, + q_len, + self.num_key_value_heads, + self.head_dim, + ))?; + let key_states = self.k_norm.forward(&key_states)?.transpose(1, 2)?; + let value_states = self.v_proj.forward(xs)?; + let value_states = value_states + .reshape((b_sz, q_len, self.num_key_value_heads, self.head_dim))? + .transpose(1, 2)?; + let (query_states, key_states) = + apply_rotary_pos_emb(&query_states, &key_states, cos, sin, false)?; + let (key_states, value_states) = match &self.kv_cache { + None => (key_states, value_states), + Some((prev_k, prev_v)) => { + let key_states = Tensor::cat(&[prev_k, &key_states], 2)?; + let value_states = Tensor::cat(&[prev_v, &value_states], 2)?; + (key_states, value_states) + } + }; + self.kv_cache = Some((key_states.clone(), value_states.clone())); + let attn_output = eager_attention_forward( + &query_states, + &key_states, + &value_states, + Some(self.num_kv_groups), + attention_mask, + self.scaling, + )?; + let attn_output = + attn_output.reshape((b_sz, q_len, self.num_attention_heads * self.head_dim))?; + let attn_output = attn_output.apply(&self.o_proj)?; + Ok(attn_output) + } + + pub fn clear_kv_cache(&mut self) { + self.kv_cache = None + } +} + pub struct NaiveAttnTwoLinearMLPBlock { self_attn: NaiveAttention, mlp: TwoLinearMLP, diff --git a/src/models/lfm2/config.rs b/src/models/lfm2/config.rs new file mode 100644 index 0000000..8ec24c1 --- /dev/null +++ b/src/models/lfm2/config.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; +use anyhow::{anyhow, Result}; +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct Lfm2Config { + pub architectures: Vec, + pub block_auto_adjust_ff_dim: bool, + pub block_dim: usize, + pub block_ff_dim: usize, + pub block_ffn_dim_multiplier: f64, + pub block_mlp_init_scale: f64, + pub block_multiple_of: usize, + pub block_norm_eps: f64, + pub block_out_init_scale: f64, + pub block_use_swiglu: bool, + pub block_use_xavier_init: bool, + pub bos_token_id: u32, + #[serde[rename="conv_L_cache"]] + pub conv_l_cache: usize, + pub conv_bias: bool, + pub conv_dim: usize, + pub conv_dim_out: Option, + pub conv_use_xavier_init: bool, + pub eos_token_id: u32, + pub full_attn_idxs: Option>, + pub layer_types: Option>, + pub hidden_size: usize, + pub initializer_range: f64, + pub intermediate_size: Option, + pub max_position_embeddings: usize, + pub model_type: String, + pub norm_eps: f64, + pub num_attention_heads: usize, + pub num_heads: usize, + pub num_hidden_layers: usize, + pub num_key_value_heads: usize, + pub pad_token_id: u32, + pub rope_theta: f32, + pub torch_dtype: Option, + pub dtype: Option, + pub use_cache: bool, + pub use_pos_enc: bool, + pub vocab_size: usize, + pub tie_embedding: Option, +} + + +impl Lfm2Config { + pub fn full_attn_idx2layer_type(&mut self) { + if self.layer_types.is_none() + && let Some(idx) = &self.full_attn_idxs + { + let mut layer_types = vec![]; + for i in 0..self.num_hidden_layers { + if idx.contains(&i) { + layer_types.push("full_attention".to_string()); + } else { + layer_types.push("conv".to_string()); + } + } + self.layer_types = Some(layer_types); + } + } + + pub fn get_layer_types(&self) -> Result> { + if let Some(types) = &self.layer_types { + Ok(types.clone()) + } else if let Some(idx) = &self.full_attn_idxs { + let mut layer_types = vec![]; + for i in 0..self.num_hidden_layers { + if idx.contains(&i) { + layer_types.push("full_attention".to_string()); + } else { + layer_types.push("conv".to_string()); + } + } + Ok(layer_types) + } else { + Err(anyhow!("layer_types full_attn_idxs cannot be none at the same time")) + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct Lfm2GenerateConfig { + pub bos_token_id: u32, + pub eos_token_id: u32, + pub pad_token_id: u32 +} \ No newline at end of file diff --git a/src/models/lfm2/generate.rs b/src/models/lfm2/generate.rs new file mode 100644 index 0000000..1c58aff --- /dev/null +++ b/src/models/lfm2/generate.rs @@ -0,0 +1,163 @@ +use crate::{ + chat_template::ChatTemplate, + models::{GenerateModel, lfm2::{ + config::{Lfm2Config, Lfm2GenerateConfig}, + model::Lfm2Model, + }}, + tokenizer::TokenizerModel, + utils::{ + build_completion_response, find_type_files, get_device, get_dtype, get_logit_processor, + }, +}; +use crate::utils::build_completion_chunk_response; +use aha_openai_dive::v1::resources::chat::{ChatCompletionParameters, ChatCompletionResponse}; +use anyhow::Result; +use candle_core::{DType, Device, Tensor}; +use candle_nn::VarBuilder; +use rocket::async_stream::stream; + +pub struct Lfm2GenerateModel<'a> { + chat_template: ChatTemplate<'a>, + tokenizer: TokenizerModel, + device: Device, + model: Lfm2Model, + eos_token_id: u32, + model_name: String, +} +impl<'a> Lfm2GenerateModel<'a> { + pub fn init(path: &str, device: Option<&Device>, dtype: Option) -> Result { + let chat_template = ChatTemplate::init(path)?; + let tokenizer = TokenizerModel::init(path)?; + let device = get_device(device); + let gen_cfg_path = path.to_string() + "/generation_config.json"; + let gen_cfg: Lfm2GenerateConfig = serde_json::from_slice(&std::fs::read(gen_cfg_path)?)?; + let cfg_path = path.to_string() + "/config.json"; + let cfg: Lfm2Config = serde_json::from_slice(&std::fs::read(cfg_path)?)?; + let model_path = find_type_files(path, "safetensors")?; + let cfg_dtype = if let Some(dtype) = &cfg.dtype { + dtype.clone() + } else if let Some(dtype) = &cfg.torch_dtype { + dtype.clone() + } else { + "bfloat16".to_string() + }; + 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 eos_token_id = gen_cfg.eos_token_id; + let model_name = std::path::Path::new(path) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("lfm2") + .to_string(); + Ok(Self { + chat_template, + tokenizer, + device, + model, + eos_token_id, + model_name, + }) + } + + +} + +impl<'a> GenerateModel for Lfm2GenerateModel<'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, + mes.top_p, + None, + mes.seed.unwrap_or(34562) as u64, + ); + let mut input_ids = self.tokenizer.text_encode(mes_render, &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); + for _ in 0..sample_len { + let logit = self.model.forward(&input_ids, 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; + } + 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> + + 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 mut input_ids = self.tokenizer.text_encode(mes_render, &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 stream = stream! { + let mut err_tokens = vec![]; + for _ in 0..sample_len { + let logit = self.model.forward(&input_ids, 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; + 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; + } + self.model.clear_cache(); + }; + Ok(Box::new(Box::pin(stream))) + } +} \ No newline at end of file diff --git a/src/models/lfm2/mod.rs b/src/models/lfm2/mod.rs new file mode 100644 index 0000000..de0a4df --- /dev/null +++ b/src/models/lfm2/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod model; +pub mod generate; \ No newline at end of file diff --git a/src/models/lfm2/model.rs b/src/models/lfm2/model.rs new file mode 100644 index 0000000..db7d4d1 --- /dev/null +++ b/src/models/lfm2/model.rs @@ -0,0 +1,313 @@ +use crate::{ + models::{ + common::{GateUpDownMLP, QKNormAttention, conv1d_depthwise, get_conv1d}, + lfm2::config::Lfm2Config, + }, + position_embed::rope::RoPE, + utils::tensor_utils::prepare_causal_attention_mask, +}; +use anyhow::Result; +use candle_core::{D, Tensor}; +use candle_nn::{ + Conv1d, Embedding, Linear, Module, RmsNorm, VarBuilder, embedding, linear_b, rms_norm, +}; + +pub struct Lfm2ShortConv { + l_cache: usize, + conv: Conv1d, + in_proj: Linear, + out_proj: Linear, + cache: Option, +} + +impl Lfm2ShortConv { + pub fn new(vb: VarBuilder, config: &Lfm2Config) -> Result { + let l_cache = config.conv_l_cache; + let bias = config.conv_bias; + let conv = get_conv1d( + vb.pp("conv"), + config.hidden_size, + config.hidden_size, + l_cache, + l_cache - 1, + 1, + 1, + config.hidden_size, + bias, + )?; + let in_proj = linear_b( + config.hidden_size, + config.hidden_size * 3, + bias, + vb.pp("in_proj"), + )?; + let out_proj = linear_b( + config.hidden_size, + config.hidden_size, + bias, + vb.pp("out_proj"), + )?; + Ok(Self { + l_cache, + conv, + in_proj, + out_proj, + cache: None, + }) + } + + pub fn forward(&mut self, xs: &Tensor) -> Result { + let seq_len = xs.dim(1)?; + let bc_x = self.in_proj.forward(xs)?.transpose(D::Minus1, D::Minus2)?; + let chunk = bc_x.chunk(3, D::Minus2)?; + let bx = chunk[0].mul(&chunk[2])?; + let c: &Tensor = &chunk[1]; + let conv_out = if self.cache.is_none() && seq_len > 1 { + let pad_num = self.l_cache as isize - seq_len as isize; + let conv_state = if pad_num > 0 { + bx.pad_with_zeros(D::Minus1, pad_num as usize, 0)? + } else { + bx.narrow(D::Minus1, pad_num.unsigned_abs(), self.l_cache)? + }; + self.cache = Some(conv_state); + let bx = bx.pad_with_zeros(D::Minus1, self.l_cache-1, self.l_cache-1)?; + let bx = conv1d_depthwise(&bx, self.conv.weight(), self.conv.bias())?; + bx.narrow(D::Minus1, 0, seq_len)? + } else { + let conv_state = self.cache.as_ref().unwrap(); + let conv_state = Tensor::cat(&[conv_state, &bx], D::Minus1)?; + let conv_state = conv_state.narrow(D::Minus1, 1, self.l_cache)?; + let conv_out = conv1d_depthwise(&conv_state, self.conv.weight(), self.conv.bias())?; + self.cache = Some(conv_state); + conv_out + }; + let y = c.mul(&conv_out)?; + let y = y.transpose(D::Minus1, D::Minus2)?.contiguous()?; + let y = self.out_proj.forward(&y)?; + Ok(y) + } + + pub fn clear_cache(&mut self) { + self.cache = None; + } +} + +enum LayerKind { + SelfAttn(QKNormAttention), + Conv(Lfm2ShortConv), +} + +impl LayerKind { + pub fn forward( + &mut self, + xs: &Tensor, + cos: &Tensor, + sin: &Tensor, + attention_mask: Option<&Tensor>, + ) -> Result { + match self { + LayerKind::SelfAttn(attn) => attn.forward(xs, cos, sin, attention_mask), + LayerKind::Conv(conv) => conv.forward(xs), + } + } +} + +pub struct Lfm2DecoderLayer { + layer: LayerKind, + feed_forward: GateUpDownMLP, + operator_norm: RmsNorm, + ffn_norm: RmsNorm, +} + +impl Lfm2DecoderLayer { + pub fn new(vb: VarBuilder, config: &Lfm2Config, layer_type: &str) -> Result { + let layer = if layer_type.eq("full_attention") { + let attn = QKNormAttention::new( + vb.pp("self_attn"), + config.hidden_size, + config.num_attention_heads, + None, + Some(config.num_key_value_heads), + false, + config.block_norm_eps, + Some("q_proj"), + Some("k_proj"), + Some("v_proj"), + Some("out_proj"), + Some("q_layernorm"), + Some("k_layernorm"), + )?; + LayerKind::SelfAttn(attn) + } else { + let conv = Lfm2ShortConv::new(vb.pp("conv"), config)?; + LayerKind::Conv(conv) + }; + let intermediate_size = if config.block_auto_adjust_ff_dim { + let inter_size = 2 * config.block_ff_dim / 3; + let inter_size = (config.block_ffn_dim_multiplier * inter_size as f64) as usize; + let inter_size = config.block_multiple_of + * ((inter_size + config.block_multiple_of - 1) / config.block_multiple_of); + inter_size + } else { + config.block_ff_dim + }; + let feed_forward = GateUpDownMLP::new( + vb.pp("feed_forward"), + config.hidden_size, + intermediate_size, + candle_nn::Activation::Silu, + false, + Some("w1"), + Some("w3"), + Some("w2"), + )?; + + let operator_norm = rms_norm(config.hidden_size, config.norm_eps, vb.pp("operator_norm"))?; + let ffn_norm = rms_norm(config.hidden_size, config.norm_eps, vb.pp("ffn_norm"))?; + Ok(Self { + layer, + feed_forward, + operator_norm, + ffn_norm, + }) + } + + pub fn forward( + &mut self, + xs: &Tensor, + cos: &Tensor, + sin: &Tensor, + attention_mask: Option<&Tensor>, + ) -> Result { + let res = xs.clone(); + let xs = self.operator_norm.forward(xs)?; + let xs = self.layer.forward(&xs, cos, sin, attention_mask)?; + let res = xs.add(&res)?; + let xs = self.ffn_norm.forward(&res)?; + let xs = self.feed_forward.forward(&xs)?; + let xs = xs.add(&res)?; + Ok(xs) + } + + pub fn clear_cache(&mut self) { + match &mut self.layer { + LayerKind::SelfAttn(attn) => attn.clear_kv_cache(), + LayerKind::Conv(conv) => conv.clear_cache(), + } + } +} + +pub struct Lfm2Decoder { + embed_tokens: Embedding, + layers: Vec, + // rotary_emb: RoPE, + pos_emb: RoPE, + embedding_norm: RmsNorm, +} + +impl Lfm2Decoder { + pub fn new(vb: VarBuilder, config: &Lfm2Config) -> Result { + let embed_tokens = embedding(config.vocab_size, config.hidden_size, vb.pp("embed_tokens"))?; + let mut layers = vec![]; + let vb_layers = vb.pp("layers"); + // let layer_types = config.layer_types.as_ref().unwrap(); + let layer_types = config.get_layer_types()?; + for i in 0..config.num_hidden_layers { + let layer_type = layer_types.get(i).unwrap(); + let layer = Lfm2DecoderLayer::new(vb_layers.pp(i), config, layer_type)?; + layers.push(layer); + } + let dim = config.hidden_size / config.num_attention_heads; + let pos_emb = RoPE::new(dim, config.rope_theta, vb.device())?; + let embedding_norm = + rms_norm(config.hidden_size, config.norm_eps, vb.pp("embedding_norm"))?; + Ok(Self { + embed_tokens, + layers, + pos_emb, + embedding_norm, + }) + } + + pub fn forward( + &mut self, + input_ids: &Tensor, + inputs_embeds: Option<&Tensor>, + seqlen_offset: usize, + ) -> Result { + let inputs_embeds = if let Some(embed) = inputs_embeds { + embed.clone() + } else { + self.embed_tokens.forward(input_ids)? + }; + + let (bs, seq_len, _) = inputs_embeds.dims3()?; + let attention_mask = if seq_len > 1 { + Some(prepare_causal_attention_mask( + bs, + seq_len, + seqlen_offset, + inputs_embeds.device(), + )?) + } else { + None + }; + let (cos, sin) = self + .pos_emb + .forward(seqlen_offset, seq_len, inputs_embeds.device())?; + let mut xs = inputs_embeds; + for layer in &mut self.layers { + xs = layer.forward(&xs, &cos, &sin, attention_mask.as_ref())?; + } + let xs = self.embedding_norm.forward(&xs)?; + Ok(xs) + } + + pub fn clear_cache(&mut self) { + for layer in &mut self.layers { + layer.clear_cache() + } + } +} + +pub struct Lfm2Model { + model: Lfm2Decoder, + lm_head: Linear, +} + +impl Lfm2Model { + pub fn new(vb: VarBuilder, config: &Lfm2Config) -> Result { + let model = Lfm2Decoder::new(vb.pp("model"), config)?; + let lm_head = if let Some(flag) = config.tie_embedding + && flag + { + Linear::new(model.embed_tokens.embeddings().clone(), None) + } else { + let linear = linear_b( + config.hidden_size, + config.vocab_size, + false, + vb.pp("lm_head"), + ); + match linear { + Ok(linear) => linear, + Err(_) => { + Linear::new(model.embed_tokens.embeddings().clone(), None) + } + } + }; + Ok(Self { model, lm_head }) + } + + pub fn forward(&mut self, input_ids: &Tensor, seqlen_offset: usize) -> Result { + let xs = self.model.forward(input_ids, None, seqlen_offset)?; + let seq_len = xs.dim(1)?; + let last_xs = xs.narrow(1, seq_len - 1, 1)?; + let xs = self.lm_head.forward(&last_xs)?; + Ok(xs) + } + + pub fn clear_cache(&mut self) { + self.model.clear_cache(); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index dc3fe87..c9ce259 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -19,6 +19,7 @@ pub mod qwen3vl; pub mod rmbg2_0; pub mod voxcpm; pub mod w2v_bert_2_0; +pub mod lfm2; use aha_openai_dive::v1::resources::chat::{ ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse, @@ -27,18 +28,15 @@ use anyhow::{Result, anyhow}; use rocket::futures::Stream; use crate::models::{ - deepseek_ocr::generate::DeepseekOCRGenerateModel, - fun_asr_nano::generate::FunAsrNanoGenerateModel, - glm_asr_nano::generate::GlmAsrNanoGenerateModel, glm_ocr::generate::GlmOcrGenerateModel, - hunyuan_ocr::generate::HunyuanOCRGenerateModel, 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, + deepseek_ocr::generate::DeepseekOCRGenerateModel, 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 }; #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum WhichModel { + #[value(name = "lfm2-1.2b", hide = true)] + LFM2_1_2B, + #[value(name = "lfm2.5-1.2b-instruct", hide = true)] + LFM2_5_1_2BInstruct, #[value(name = "minicpm4-0.5b", hide = true)] MiniCPM4_0_5B, #[value(name = "qwen2.5vl-3b", hide = true)] @@ -97,6 +95,8 @@ impl WhichModel { /// Get the ModelScope model ID for this model variant pub fn model_id(self) -> &'static str { match self { + WhichModel::LFM2_1_2B => "LiquidAI/LFM2-1.2B", + WhichModel::LFM2_5_1_2BInstruct => "LiquidAI/LFM2.5-1.2B-Instruct", 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", @@ -130,7 +130,9 @@ impl WhichModel { pub fn model_type(self) -> &'static str { match self { // LLM models - WhichModel::MiniCPM4_0_5B | WhichModel::Qwen3_0_6B => "llm", + WhichModel::MiniCPM4_0_5B | WhichModel::Qwen3_0_6B + | WhichModel::LFM2_1_2B + | WhichModel::LFM2_5_1_2BInstruct => "llm", WhichModel::Qwen2_5vl3B | WhichModel::Qwen2_5vl7B | WhichModel::Qwen3vl2B @@ -177,6 +179,7 @@ pub trait GenerateModel { pub enum ModelInstance<'a> { MiniCPM4(MiniCPMGenerateModel<'a>), + Lfm2(Lfm2GenerateModel<'a>), Qwen2_5VL(Qwen2_5VLGenerateModel<'a>), Qwen3(Qwen3GenerateModel<'a>), Qwen3_5(Qwen3_5GenerateModel<'a>), @@ -196,6 +199,7 @@ impl<'a> GenerateModel for ModelInstance<'a> { fn generate(&mut self, mes: ChatCompletionParameters) -> Result { match self { ModelInstance::MiniCPM4(model) => model.generate(mes), + ModelInstance::Lfm2(model) => model.generate(mes), ModelInstance::Qwen2_5VL(model) => model.generate(mes), ModelInstance::Qwen3(model) => model.generate(mes), ModelInstance::Qwen3_5(model) => model.generate(mes), @@ -225,6 +229,7 @@ impl<'a> GenerateModel for ModelInstance<'a> { > { match self { ModelInstance::MiniCPM4(model) => model.generate_stream(mes), + ModelInstance::Lfm2(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), @@ -253,6 +258,14 @@ pub fn load_model<'a>( let model = MiniCPMGenerateModel::init(path, None, None)?; ModelInstance::MiniCPM4(model) } + WhichModel::LFM2_1_2B => { + let model = Lfm2GenerateModel::init(path, None, None)?; + ModelInstance::Lfm2(model) + } + WhichModel::LFM2_5_1_2BInstruct => { + let model = Lfm2GenerateModel::init(path, None, None)?; + ModelInstance::Lfm2(model) + } WhichModel::Qwen2_5vl3B => { let model = Qwen2_5VLGenerateModel::init(path, None, None)?; ModelInstance::Qwen2_5VL(model) diff --git a/src/models/qwen3/model.rs b/src/models/qwen3/model.rs index 9543ad0..bd67077 100644 --- a/src/models/qwen3/model.rs +++ b/src/models/qwen3/model.rs @@ -6,7 +6,7 @@ use candle_nn::{ use crate::{ models::{ - common::{GateUpDownMLP, eager_attention_forward}, + common::{GateUpDownMLP, QKNormAttention, eager_attention_forward}, qwen3::config::Qwen3Config, }, position_embed::rope::{RoPE, apply_rotary_pos_emb}, @@ -135,7 +135,8 @@ impl Qwen3Attention { } pub struct Qwen3DecoderLayer { - self_attn: Qwen3Attention, + // self_attn: Qwen3Attention, + self_attn: QKNormAttention, mlp: GateUpDownMLP, input_layernorm: RmsNorm, post_attention_layernorm: RmsNorm, @@ -143,7 +144,22 @@ pub struct Qwen3DecoderLayer { impl Qwen3DecoderLayer { pub fn new(config: &Qwen3Config, vb: VarBuilder) -> Result { - let self_attn = Qwen3Attention::new(config, vb.pp("self_attn"))?; + // let self_attn = Qwen3Attention::new(config, vb.pp("self_attn"))?; + let self_attn = QKNormAttention::new( + vb.pp("self_attn"), + config.hidden_size, + config.num_attention_heads, + Some(config.head_dim), + Some(config.num_key_value_heads), + config.attention_bias, + config.rms_norm_eps, + None, + None, + None, + None, + None, + None, + )?; let mlp = GateUpDownMLP::new( vb.pp("mlp"), config.hidden_size, diff --git a/src/models/qwen3_5/generate.rs b/src/models/qwen3_5/generate.rs index 992b450..1c4cfae 100644 --- a/src/models/qwen3_5/generate.rs +++ b/src/models/qwen3_5/generate.rs @@ -36,10 +36,9 @@ pub struct Qwen3_5GenerateModel<'a> { impl<'a> Qwen3_5GenerateModel<'a> { pub fn init(path: &str, device: Option<&Device>, dtype: Option) -> Result { - let model_name = path - .split("/") - .collect::>() - .pop() + let model_name = std::path::Path::new(path) + .file_name() + .and_then(|s| s.to_str()) .unwrap_or("qwen3.5"); let chat_template = ChatTemplate::init(path)?; let tokenizer = TokenizerModel::init(path)?; diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index 1e1d8e3..7d4c165 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -110,6 +110,14 @@ impl TokenizerModel { .map_err(|e| anyhow!(format!("tokenizer encode error{}", e)))?; Ok(decode) } + + pub fn token_decode_with_special(&self, tokens: Vec) -> Result { + let decode = self + .tokenizer + .decode(&tokens, false) + .map_err(|e| anyhow!(format!("tokenizer encode error{}", e)))?; + Ok(decode) + } } pub fn sentencepiece_encode( diff --git a/tests/config_tests.rs b/tests/config_tests.rs index 7b1dfcb..94409c8 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -1,8 +1,5 @@ use aha::models::{ - deepseek_ocr::config::DeepseekOCRConfig, hunyuan_ocr::config::HunYuanVLConfig, - 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, minicpm4::config::MiniCPM4Config, paddleocr_vl::config::PaddleOCRVLConfig, qwen2_5vl::config::Qwen2_5VLConfig, qwen3vl::config::Qwen3VLConfig, voxcpm::config::VoxCPMConfig }; use anyhow::Result; @@ -85,3 +82,16 @@ fn paddleocr_vl_config() -> Result<()> { println!("{:?}", config); Ok(()) } + +#[test] +fn lfm2_config() -> Result<()> { + // cargo test -F cuda --test config_tests lfm2_config -r -- --nocapture + let model_path = "/home/jhq/.aha/LiquidAI/LFM2-1.2B/"; + // let model_path = "/home/jhq/.aha/LiquidAI/LFM2.5-1.2B-Instruct/"; + let config_path = model_path.to_string() + "/config.json"; + let mut config: Lfm2Config = serde_json::from_slice(&std::fs::read(config_path)?)?; + println!("{:?}", config); + config.full_attn_idx2layer_type(); + println!("{:?}", config); + Ok(()) +} diff --git a/tests/messy_test.rs b/tests/messy_test.rs index 43ab670..e80cccc 100644 --- a/tests/messy_test.rs +++ b/tests/messy_test.rs @@ -35,14 +35,18 @@ async fn download_test() -> Result<()> { #[test] fn messy_test() -> Result<()> { // RUST_BACKTRACE=1 cargo test -F cuda --test messy_test messy_test -r -- --nocapture - let save_dir = - aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?; - let model_path = format!("{}/deepseek-ai/DeepSeek-OCR-2/", save_dir); - let stem = std::path::Path::new(&model_path) - .file_stem() // 获取文件名主干(不含扩展名) - .and_then(|s| s.to_str()) - .unwrap_or("qwen3.5"); - println!("stem: {:?}", stem); + // let t1 = Tensor::randn(0.0, 1.0, (1, 2, 6), device)?; + // println!(" t1: {}", t1); + // let t2 = t1.pad_with_zeros(D::Minus1, -3, 0)?; + // println!(" t2: {}", t); + // let save_dir = + // aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?; + // let model_path = format!("{}/deepseek-ai/DeepSeek-OCR-2/", save_dir); + // let stem = std::path::Path::new(&model_path) + // .file_name() + // .and_then(|s| s.to_str()) + // .unwrap_or("qwen3.5"); + // println!("stem: {:?}", stem); // let device = &candle_core::Device::Cpu; // let t1 = Tensor::randn(0.0, 1.0, (16, 9, 64, 128), device)?; // let t2 = Tensor::randn(0.0, 1.0, (16, 9, 128, 64), device)?; diff --git a/tests/test_lfm2.rs b/tests/test_lfm2.rs new file mode 100644 index 0000000..60e2c9d --- /dev/null +++ b/tests/test_lfm2.rs @@ -0,0 +1,91 @@ +use std::{pin::pin, time::Instant}; +use anyhow::Result; +use aha::{chat::ChatCompletionParameters, models::{GenerateModel, lfm2::generate::Lfm2GenerateModel}}; +use rocket::futures::StreamExt; + +#[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 message = r#" + { + "model": "lfm2", + "messages": [ + { + "role": "user", + "content": "你如何看待AI" + } + ] + } + "#; + let mes: ChatCompletionParameters = serde_json::from_str(message)?; + let i_start = Instant::now(); + let mut model = Lfm2GenerateModel::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 lfm2_stream() -> Result<()> { + // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_lfm2 lfm2_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-1.2B-Instruct/", save_dir); + let message = r#" + { + "model": "lfm2", + "messages": [ + { + "role": "user", + "content": "你如何看待AI" + } + ] + } + "#; + let mes: ChatCompletionParameters = serde_json::from_str(message)?; + let i_start = Instant::now(); + let mut model = Lfm2GenerateModel::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 mut stream = pin!(model.generate_stream(mes)?); + let i_duration = i_start.elapsed(); + while let Some(token) = stream.next().await { + println!("generate: \n {:?}", token); + } + // 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(()) +} \ No newline at end of file diff --git a/tests/test_qwen3.rs b/tests/test_qwen3.rs index cc1b944..61bd73a 100644 --- a/tests/test_qwen3.rs +++ b/tests/test_qwen3.rs @@ -7,7 +7,7 @@ use rocket::futures::StreamExt; #[test] fn qwen3_0_6b_generate() -> Result<()> { - // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda qwen3_0_6b_generate -r -- --nocapture + // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_qwen3 qwen3_0_6b_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 = diff --git a/tests/weight_test.rs b/tests/weight_test.rs index 643b01d..cd949b8 100644 --- a/tests/weight_test.rs +++ b/tests/weight_test.rs @@ -265,3 +265,25 @@ fn deepseekocrv2_weight() -> Result<()> { println!("model_list: {:?}", model_list); Ok(()) } + +#[test] +fn lfm2_weight() -> Result<()> { + // cargo test -F cuda --test weight_test lfm2_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-1.2B/", 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(()) +}