glm-ocr
This commit is contained in:
+2
-1
@@ -253,6 +253,7 @@ fn which_model_to_id(which_model: WhichModel) -> &'static str {
|
||||
WhichModel::VoxCPM1_5 => "voxcpm1.5",
|
||||
WhichModel::GlmASRNano2512 => "glm-asr-nano-2512",
|
||||
WhichModel::FunASRNano2512 => "fun-asr-nano-2512",
|
||||
WhichModel::GlmOCR => "glm-ocr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +276,7 @@ fn which_model_to_owner(which_model: WhichModel) -> &'static str {
|
||||
WhichModel::PaddleOCRVL => "PaddlePaddle",
|
||||
WhichModel::RMBG2_0 => "AI-ModelScope",
|
||||
WhichModel::VoxCPM | WhichModel::VoxCPM1_5 => "OpenBMB",
|
||||
WhichModel::GlmASRNano2512 => "ZhipuAI",
|
||||
WhichModel::GlmASRNano2512 | WhichModel::GlmOCR => "ZhipuAI",
|
||||
WhichModel::FunASRNano2512 => "FunAudioLLM",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Glm-OCR exec implementation for CLI `run` subcommand
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Ok, Result};
|
||||
|
||||
use crate::exec::ExecModel;
|
||||
use crate::models::{GenerateModel, glm_ocr::generate::GlmOcrGenerateModel};
|
||||
|
||||
pub struct GlmOcrExec;
|
||||
|
||||
impl ExecModel for GlmOcrExec {
|
||||
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
|
||||
let url = &input[0];
|
||||
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 = GlmOcrGenerateModel::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": "glm-ocr",
|
||||
"messages": [
|
||||
{{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{{
|
||||
"type": "image_url",
|
||||
"image_url": {{
|
||||
"url": "{}"
|
||||
}}
|
||||
}},
|
||||
{{
|
||||
"type": "text",
|
||||
"text": "Text Recognition:"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
],
|
||||
"max_tokens": 1024
|
||||
}}"#,
|
||||
input_url
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
pub mod deepseek_ocr;
|
||||
pub mod fun_asr_nano;
|
||||
pub mod glm_asr_nano;
|
||||
pub mod glm_ocr;
|
||||
pub mod hunyuan_ocr;
|
||||
pub mod minicpm4;
|
||||
pub mod paddleocr_vl;
|
||||
|
||||
@@ -219,6 +219,7 @@ fn run_list(args: ListArgs) -> anyhow::Result<()> {
|
||||
WhichModel::VoxCPM1_5,
|
||||
WhichModel::GlmASRNano2512,
|
||||
WhichModel::FunASRNano2512,
|
||||
WhichModel::GlmOCR,
|
||||
];
|
||||
|
||||
if args.json {
|
||||
@@ -466,6 +467,10 @@ fn run_run(args: RunArgs) -> anyhow::Result<()> {
|
||||
use aha::exec::fun_asr_nano::FunASRNanoExec;
|
||||
FunASRNanoExec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
WhichModel::GlmOCR => {
|
||||
use aha::exec::glm_ocr::GlmOcrExec;
|
||||
GlmOcrExec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
use candle_nn::Activation;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Vision encoder configuration for GLM-OCR.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrVisionConfig {
|
||||
#[serde(default)]
|
||||
pub model_type: String,
|
||||
/// Number of transformer layers (depth) in the vision encoder. Default: 24
|
||||
#[serde(default)]
|
||||
pub depth: usize,
|
||||
/// Dimensionality of the encoder layers and the pooler layer. Default: 1024
|
||||
#[serde(default = "default_hidden_size")]
|
||||
pub hidden_size: usize,
|
||||
/// Non-linear activation function in the encoder. Default: "silu"
|
||||
#[serde(default)]
|
||||
pub hidden_act: Activation,
|
||||
/// Whether to add bias to queries, keys and values. Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub attention_bias: bool,
|
||||
/// Dropout probability for attention weights. Default: 0.0
|
||||
#[serde(default)]
|
||||
pub attention_dropout: f64,
|
||||
/// Number of attention heads per layer. Default: 16
|
||||
#[serde(default = "default_num_heads")]
|
||||
pub num_heads: usize,
|
||||
/// Number of input image channels. Default: 3
|
||||
#[serde(default = "default_in_channels")]
|
||||
pub in_channels: usize,
|
||||
/// Input image resolution. Default: 336
|
||||
#[serde(default = "default_image_size")]
|
||||
pub image_size: usize,
|
||||
/// Size of each image patch. Default: 14
|
||||
#[serde(default = "default_patch_size")]
|
||||
pub patch_size: usize,
|
||||
/// Epsilon for RMS normalization layers. Default: 1e-5
|
||||
#[serde(default = "default_rms_norm_eps")]
|
||||
pub rms_norm_eps: f64,
|
||||
/// Size used for merging spatial dimensions. Default: 2
|
||||
#[serde(default = "default_spatial_merge_size")]
|
||||
pub spatial_merge_size: usize,
|
||||
/// Patch size along the temporal dimension (for video). Default: 2
|
||||
#[serde(default = "default_temporal_patch_size")]
|
||||
pub temporal_patch_size: usize,
|
||||
/// Output hidden size of the vision model. Default: 1536
|
||||
#[serde(alias = "out_hidden_size", default = "default_out_hidden_size")]
|
||||
pub out_hidden_size: usize,
|
||||
/// Dimensionality of the feed-forward layer. Default: 4096
|
||||
#[serde(default = "default_intermediate_size")]
|
||||
pub intermediate_size: usize,
|
||||
/// Std dev of truncated normal initializer for weight matrices. Default: 0.02
|
||||
#[serde(default = "default_initializer_range")]
|
||||
pub initializer_range: f64,
|
||||
/// Base frequency for RoPE in vision encoder. Default: 10000.0
|
||||
#[serde(default = "default_rope_theta")]
|
||||
pub rope_theta: f32,
|
||||
}
|
||||
|
||||
fn default_hidden_size() -> usize {
|
||||
1024
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_num_heads() -> usize {
|
||||
16
|
||||
}
|
||||
fn default_in_channels() -> usize {
|
||||
3
|
||||
}
|
||||
fn default_image_size() -> usize {
|
||||
336
|
||||
}
|
||||
fn default_patch_size() -> usize {
|
||||
14
|
||||
}
|
||||
fn default_rms_norm_eps() -> f64 {
|
||||
1e-5
|
||||
}
|
||||
fn default_spatial_merge_size() -> usize {
|
||||
2
|
||||
}
|
||||
fn default_temporal_patch_size() -> usize {
|
||||
2
|
||||
}
|
||||
fn default_out_hidden_size() -> usize {
|
||||
1536
|
||||
}
|
||||
fn default_intermediate_size() -> usize {
|
||||
4096
|
||||
}
|
||||
fn default_initializer_range() -> f64 {
|
||||
0.02
|
||||
}
|
||||
|
||||
fn default_projector_hidden_size() -> usize {
|
||||
1536
|
||||
}
|
||||
|
||||
/// Projector configuration for mapping vision features to LLM embedding space.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrProjectorConfig {
|
||||
/// Hidden size for the projector. Default: 1536
|
||||
#[serde(default = "default_projector_hidden_size")]
|
||||
pub hidden_size: usize,
|
||||
/// Activation function for the projector.
|
||||
#[serde(default)]
|
||||
pub projector_hidden_act: Activation,
|
||||
/// Number of query tokens for the projector. Default: 256
|
||||
#[serde(default = "default_num_queries")]
|
||||
pub num_queries: usize,
|
||||
}
|
||||
|
||||
fn default_num_queries() -> usize {
|
||||
256
|
||||
}
|
||||
|
||||
/// RoPE (Rotary Position Embedding) configuration parameters.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrRopeParameters {
|
||||
/// Type of RoPE scaling (e.g., "mrope" for multimodal).
|
||||
#[serde(default)]
|
||||
pub rope_type: String,
|
||||
/// Section sizes for M-RoPE (Multimodal RoPE) dimensions.
|
||||
#[serde(default)]
|
||||
pub mrope_section: Vec<usize>,
|
||||
/// Fraction of head dim to apply rotary embedding to.
|
||||
#[serde(default)]
|
||||
pub partial_rotary_factor: f32,
|
||||
/// Base frequency for RoPE. Default: 10000.0
|
||||
#[serde(default = "default_rope_theta")]
|
||||
pub rope_theta: f32,
|
||||
}
|
||||
|
||||
fn default_rope_theta() -> f32 {
|
||||
10000.0
|
||||
}
|
||||
|
||||
/// Text decoder configuration for GLM-OCR.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrTextConfig {
|
||||
/// Vocabulary size. Defines the number of different tokens. Default: 59392
|
||||
#[serde(default = "default_vocab_size")]
|
||||
pub vocab_size: usize,
|
||||
/// Dimension of the hidden representations. Default: 1024
|
||||
#[serde(default = "default_hidden_size")]
|
||||
pub hidden_size: usize,
|
||||
/// Dimension of the MLP representations. Default: 4608
|
||||
#[serde(default = "default_text_intermediate_size")]
|
||||
pub intermediate_size: usize,
|
||||
/// Number of hidden layers in the transformer decoder. Default: 16
|
||||
#[serde(default = "default_num_hidden_layers")]
|
||||
pub num_hidden_layers: usize,
|
||||
/// Number of attention heads per layer. Default: 16
|
||||
#[serde(default = "default_num_attention_heads")]
|
||||
pub num_attention_heads: usize,
|
||||
/// Number of key-value heads for Grouped Query Attention.
|
||||
/// If equal to num_attention_heads, uses MHA. If 1, uses MQA. Default: 8
|
||||
#[serde(default = "default_num_key_value_heads")]
|
||||
pub num_key_value_heads: usize,
|
||||
/// Dimension of each attention head. Default: 128
|
||||
#[serde(default = "default_head_dim")]
|
||||
pub head_dim: Option<usize>,
|
||||
/// Maximum sequence length the model can handle. Default: 131072
|
||||
#[serde(default = "default_max_position_embeddings")]
|
||||
pub max_position_embeddings: usize,
|
||||
/// Epsilon for RMS normalization layers. Default: 1e-5
|
||||
#[serde(default = "default_rms_norm_eps")]
|
||||
pub rms_norm_eps: f64,
|
||||
/// Base frequency for RoPE positional embeddings.
|
||||
#[serde(default)]
|
||||
pub rope_theta: f32,
|
||||
/// Fraction of head dim to apply rotary embedding to.
|
||||
#[serde(default)]
|
||||
pub partial_rotary_factor: f32,
|
||||
/// Non-linear activation function in the decoder. Default: "silu"
|
||||
#[serde(default)]
|
||||
pub hidden_act: Activation,
|
||||
/// Whether to return key/value caches for faster generation. Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub use_cache: bool,
|
||||
/// Dropout ratio for attention probabilities. Default: 0.0
|
||||
#[serde(default)]
|
||||
pub attention_dropout: f64,
|
||||
/// Type of RoPE scaling.
|
||||
#[serde(default)]
|
||||
pub rope_type: String,
|
||||
/// Section sizes for M-RoPE dimensions.
|
||||
#[serde(default)]
|
||||
pub mrope_section: Vec<usize>,
|
||||
/// Full RoPE configuration parameters.
|
||||
#[serde(default)]
|
||||
pub rope_parameters: Option<GlmOcrRopeParameters>,
|
||||
/// End-of-sequence token ID.
|
||||
#[serde(default)]
|
||||
pub eos_token_id: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn default_vocab_size() -> usize {
|
||||
59392
|
||||
}
|
||||
fn default_num_hidden_layers() -> usize {
|
||||
16
|
||||
}
|
||||
fn default_num_attention_heads() -> usize {
|
||||
16
|
||||
}
|
||||
fn default_num_key_value_heads() -> usize {
|
||||
8
|
||||
}
|
||||
fn default_max_position_embeddings() -> usize {
|
||||
131072
|
||||
}
|
||||
fn default_text_intermediate_size() -> usize {
|
||||
4608
|
||||
}
|
||||
fn default_head_dim() -> Option<usize> {
|
||||
Some(128)
|
||||
}
|
||||
|
||||
/// Top-level configuration for GLM-OCR multimodal model.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrConfig {
|
||||
#[serde(default)]
|
||||
pub architectures: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub model_type: String,
|
||||
/// Vision encoder configuration.
|
||||
#[serde(default)]
|
||||
pub vision_config: GlmOcrVisionConfig,
|
||||
/// Projector configuration for vision-to-text mapping.
|
||||
#[serde(default)]
|
||||
pub projector_config: GlmOcrProjectorConfig,
|
||||
/// Text decoder configuration.
|
||||
#[serde(default)]
|
||||
pub text_config: GlmOcrTextConfig,
|
||||
/// Token index to encode image prompts. Default: 59280
|
||||
#[serde(default = "default_image_token_id")]
|
||||
pub image_token_id: u32,
|
||||
/// Token index to encode video prompts. Default: 59281
|
||||
#[serde(default = "default_video_token_id")]
|
||||
pub video_token_id: u32,
|
||||
/// Token index marking start of image. Default: 59256
|
||||
#[serde(default = "default_image_start_token_id")]
|
||||
pub image_start_token_id: u32,
|
||||
/// Token index marking end of image. Default: 59257
|
||||
#[serde(default = "default_image_end_token_id")]
|
||||
pub image_end_token_id: u32,
|
||||
/// Token index marking start of video. Default: 59258
|
||||
#[serde(default = "default_video_start_token_id")]
|
||||
pub video_start_token_id: u32,
|
||||
/// Token index marking end of video. Default: 59259
|
||||
#[serde(default = "default_video_end_token_id")]
|
||||
pub video_end_token_id: u32,
|
||||
/// Beginning-of-sequence token ID.
|
||||
#[serde(default)]
|
||||
pub bos_token_id: u32,
|
||||
/// End-of-sequence token ID.
|
||||
#[serde(default)]
|
||||
pub eos_token_id: u32,
|
||||
/// Padding token ID.
|
||||
#[serde(default)]
|
||||
pub pad_token_id: u32,
|
||||
#[serde(default)]
|
||||
pub torch_dtype: String,
|
||||
}
|
||||
|
||||
fn default_image_token_id() -> u32 {
|
||||
59280
|
||||
}
|
||||
fn default_video_token_id() -> u32 {
|
||||
59281
|
||||
}
|
||||
fn default_image_start_token_id() -> u32 {
|
||||
59256
|
||||
}
|
||||
fn default_image_end_token_id() -> u32 {
|
||||
59257
|
||||
}
|
||||
fn default_video_start_token_id() -> u32 {
|
||||
59258
|
||||
}
|
||||
fn default_video_end_token_id() -> u32 {
|
||||
59259
|
||||
}
|
||||
|
||||
/// Generation configuration for controlling text output.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrGenerationConfig {
|
||||
/// Beginning-of-sequence token ID.
|
||||
#[serde(default)]
|
||||
pub bos_token_id: usize,
|
||||
/// Padding token ID.
|
||||
#[serde(default)]
|
||||
pub pad_token_id: usize,
|
||||
/// Whether to use sampling (true) or greedy decoding (false). Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub do_sample: bool,
|
||||
/// End-of-sequence token ID(s) that stop generation.
|
||||
#[serde(default)]
|
||||
pub eos_token_id: Vec<usize>,
|
||||
/// Nucleus sampling probability threshold. Default: 0.9
|
||||
#[serde(default = "default_top_p")]
|
||||
pub top_p: f32,
|
||||
/// Top-k tokens to consider for sampling. Default: 50
|
||||
#[serde(default = "default_top_k")]
|
||||
pub top_k: usize,
|
||||
/// Sampling temperature (higher = more random). Default: 0.7
|
||||
#[serde(default = "default_temperature")]
|
||||
pub temperature: f32,
|
||||
/// Penalty for repeating tokens. Default: 1.0
|
||||
#[serde(default = "default_repetition_penalty")]
|
||||
pub repetition_penalty: f32,
|
||||
}
|
||||
|
||||
fn default_top_p() -> f32 {
|
||||
0.9
|
||||
}
|
||||
fn default_top_k() -> usize {
|
||||
50
|
||||
}
|
||||
fn default_temperature() -> f32 {
|
||||
0.7
|
||||
}
|
||||
fn default_repetition_penalty() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Image preprocessor configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
|
||||
pub struct GlmOcrPreprocessorConfig {
|
||||
/// Mean values for image normalization (per channel).
|
||||
#[serde(default)]
|
||||
pub image_mean: Vec<f32>,
|
||||
/// Std dev values for image normalization (per channel).
|
||||
#[serde(default)]
|
||||
pub image_std: Vec<f32>,
|
||||
/// Shortest edge for dynamic image resizing. Default: 448
|
||||
#[serde(default)]
|
||||
pub size: Option<serde_json::Value>,
|
||||
/// Shortest edge length for resizing (min_pixels in Python).
|
||||
#[serde(default = "default_shortest_edge")]
|
||||
pub shortest_edge: usize,
|
||||
/// Longest edge for resizing (max_pixels in Python).
|
||||
#[serde(default = "default_longest_edge")]
|
||||
pub longest_edge: usize,
|
||||
/// Patch size for vision encoder.
|
||||
#[serde(default = "default_patch_size_14")]
|
||||
pub patch_size: Option<usize>,
|
||||
/// Merge size for spatial merge.
|
||||
#[serde(default = "default_merge_size")]
|
||||
pub merge_size: Option<usize>,
|
||||
}
|
||||
|
||||
fn default_shortest_edge() -> usize {
|
||||
12544 // Python's default min_pixels
|
||||
}
|
||||
|
||||
fn default_longest_edge() -> usize {
|
||||
9633792 // Python's default max_pixels
|
||||
}
|
||||
|
||||
fn default_patch_size_14() -> Option<usize> {
|
||||
Some(14)
|
||||
}
|
||||
|
||||
fn default_merge_size() -> Option<usize> {
|
||||
Some(2)
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
//! GLM-OCR Inference and Generation
|
||||
use anyhow::{Result, anyhow};
|
||||
use aha_openai_dive::v1::resources::chat::{
|
||||
ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse,
|
||||
};
|
||||
use candle_core::{DType, Device, IndexOp, Tensor};
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::utils::apply_repeat_penalty;
|
||||
use rocket::async_stream::stream;
|
||||
use rocket::futures::Stream;
|
||||
|
||||
use crate::{
|
||||
chat_template::ChatTemplate,
|
||||
models::{
|
||||
GenerateModel,
|
||||
glm_ocr::{
|
||||
config::{GlmOcrConfig, GlmOcrGenerationConfig},
|
||||
model::GlmOcrModel,
|
||||
processor::GlmOcrProcessor,
|
||||
},
|
||||
},
|
||||
tokenizer::TokenizerModel,
|
||||
utils::img_utils::extract_image_url,
|
||||
utils::{
|
||||
build_completion_chunk_response, build_completion_response, find_type_files, get_device,
|
||||
get_dtype, get_logit_processor,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct GlmOcrGenerateModel<'a> {
|
||||
chat_template: ChatTemplate<'a>,
|
||||
tokenizer: TokenizerModel,
|
||||
processor: GlmOcrProcessor,
|
||||
model: GlmOcrModel,
|
||||
device: Device,
|
||||
eos_token_ids: Vec<u32>,
|
||||
generation_config: GlmOcrGenerationConfig,
|
||||
model_name: String,
|
||||
image_token_id: u32,
|
||||
image_start_token_id: u32,
|
||||
image_end_token_id: u32,
|
||||
patch_size: usize,
|
||||
temporal_patch_size: usize,
|
||||
spatial_merge_size: usize,
|
||||
}
|
||||
|
||||
impl<'a> GlmOcrGenerateModel<'a> {
|
||||
pub fn init(path: &str, device: Option<&Device>, dtype: Option<DType>) -> Result<Self> {
|
||||
let chat_template = ChatTemplate::init(path)?;
|
||||
let tokenizer = TokenizerModel::init(path)?;
|
||||
let config_path = path.to_string() + "/config.json";
|
||||
let mut cfg: GlmOcrConfig = serde_json::from_slice(&std::fs::read(config_path)?)?;
|
||||
|
||||
if cfg.projector_config.hidden_size == 0 {
|
||||
cfg.projector_config.hidden_size = 1536;
|
||||
}
|
||||
if cfg.projector_config.num_queries == 0 {
|
||||
cfg.projector_config.num_queries = 256;
|
||||
}
|
||||
|
||||
// Apply rope_parameters if present (config.json nests these under rope_parameters)
|
||||
if let Some(ref rope_params) = cfg.text_config.rope_parameters {
|
||||
if cfg.text_config.rope_theta == 0.0 {
|
||||
cfg.text_config.rope_theta = rope_params.rope_theta;
|
||||
}
|
||||
if cfg.text_config.mrope_section.is_empty() {
|
||||
cfg.text_config.mrope_section = rope_params.mrope_section.clone();
|
||||
}
|
||||
if cfg.text_config.rope_type.is_empty() {
|
||||
cfg.text_config.rope_type = rope_params.rope_type.clone();
|
||||
}
|
||||
if cfg.text_config.partial_rotary_factor == 0.0 {
|
||||
cfg.text_config.partial_rotary_factor = rope_params.partial_rotary_factor;
|
||||
}
|
||||
}
|
||||
// Fallback rope_theta if still 0
|
||||
if cfg.text_config.rope_theta == 0.0 {
|
||||
cfg.text_config.rope_theta = 10000.0;
|
||||
}
|
||||
|
||||
// Collect all EOS token IDs (config may have one or multiple)
|
||||
let mut eos_token_ids: Vec<u32> = Vec::new();
|
||||
if cfg.eos_token_id != 0 {
|
||||
eos_token_ids.push(cfg.eos_token_id);
|
||||
}
|
||||
if let Some(ref eos_val) = cfg.text_config.eos_token_id {
|
||||
match eos_val {
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(id) = n.as_u64() {
|
||||
let id = id as u32;
|
||||
if !eos_token_ids.contains(&id) {
|
||||
eos_token_ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for v in arr {
|
||||
if let Some(id) = v.as_u64() {
|
||||
let id = id as u32;
|
||||
if !eos_token_ids.contains(&id) {
|
||||
eos_token_ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if eos_token_ids.is_empty() {
|
||||
eos_token_ids.push(59246); // GLM-OCR default
|
||||
}
|
||||
|
||||
let device = get_device(device);
|
||||
let cfg_dtype = if cfg.torch_dtype.is_empty() {
|
||||
"bfloat16"
|
||||
} else {
|
||||
&cfg.torch_dtype
|
||||
};
|
||||
let dtype = get_dtype(dtype, cfg_dtype);
|
||||
// Vision encoder has ops unsupported in F16 on CPU; use F32 for CPU
|
||||
let dtype = if matches!(device, Device::Cpu) && !matches!(dtype, DType::F32 | DType::F64) {
|
||||
DType::F32
|
||||
} else {
|
||||
dtype
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
eprintln!("GLM-OCR Config Debug:");
|
||||
eprintln!(" text_config.hidden_size: {}", cfg.text_config.hidden_size);
|
||||
eprintln!(
|
||||
" text_config.num_attention_heads: {}",
|
||||
cfg.text_config.num_attention_heads
|
||||
);
|
||||
eprintln!(
|
||||
" text_config.num_key_value_heads: {}",
|
||||
cfg.text_config.num_key_value_heads
|
||||
);
|
||||
eprintln!(
|
||||
" text_config.head_dim: {}",
|
||||
cfg.text_config.head_dim.unwrap_or_else(|| {
|
||||
// Integer division, panics if num_attention_heads is 0 (like Python)
|
||||
cfg.text_config.hidden_size / cfg.text_config.num_attention_heads
|
||||
})
|
||||
);
|
||||
eprintln!(
|
||||
" text_config.mrope_section: {:?}",
|
||||
cfg.text_config.mrope_section
|
||||
);
|
||||
eprintln!(
|
||||
" Calculated head_dim: {}",
|
||||
cfg.text_config.hidden_size / cfg.text_config.num_attention_heads
|
||||
);
|
||||
}
|
||||
|
||||
let processor = GlmOcrProcessor::new(path, &device, dtype)?;
|
||||
let model_list = find_type_files(path, "safetensors")?;
|
||||
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
|
||||
let model = GlmOcrModel::new(vb, cfg.clone())?;
|
||||
let generation_config_path = path.to_string() + "/generation_config.json";
|
||||
let generation_config: GlmOcrGenerationConfig =
|
||||
serde_json::from_slice(&std::fs::read(generation_config_path)?)?;
|
||||
|
||||
Ok(Self {
|
||||
chat_template,
|
||||
tokenizer,
|
||||
processor,
|
||||
model,
|
||||
device,
|
||||
eos_token_ids,
|
||||
generation_config,
|
||||
model_name: "glm-ocr".to_string(),
|
||||
image_token_id: cfg.image_token_id,
|
||||
image_start_token_id: cfg.image_start_token_id,
|
||||
image_end_token_id: cfg.image_end_token_id,
|
||||
patch_size: cfg.vision_config.patch_size,
|
||||
temporal_patch_size: cfg.vision_config.temporal_patch_size,
|
||||
spatial_merge_size: cfg.vision_config.spatial_merge_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GenerateModel for GlmOcrGenerateModel<'a> {
|
||||
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
|
||||
// Check if sampling is enabled - if do_sample is false, use greedy decoding (temperature = None)
|
||||
let do_sample = mes.temperature.is_some() || self.generation_config.do_sample;
|
||||
let temperature = if !do_sample {
|
||||
None // Greedy decoding
|
||||
} else {
|
||||
match mes.temperature {
|
||||
None => Some(self.generation_config.temperature),
|
||||
Some(tem) => Some(tem),
|
||||
}
|
||||
};
|
||||
let top_p = match mes.top_p {
|
||||
None => Some(self.generation_config.top_p),
|
||||
Some(top_p) => Some(top_p),
|
||||
};
|
||||
let top_k = Some(self.generation_config.top_k);
|
||||
let seed = match mes.seed {
|
||||
None => 34562u64,
|
||||
Some(s) => s as u64,
|
||||
};
|
||||
let mut logit_processor =
|
||||
get_logit_processor(temperature, top_p, top_k, seed);
|
||||
|
||||
// Extract image path and prompt from messages
|
||||
let image_urls = extract_image_url(&mes);
|
||||
let image_path = image_urls
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No image provided"))?;
|
||||
|
||||
// Get prompt text from messages
|
||||
let prompt = extract_text_from_messages(&mes).unwrap_or_else(|| "Extract all text from this image.".to_string());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
eprintln!("[GLM-OCR] ===== DEBUG START =====");
|
||||
eprintln!("[GLM-OCR] Image: {}", image_path);
|
||||
eprintln!("[GLM-OCR] Prompt: {}", prompt);
|
||||
eprintln!("[GLM-OCR] Device: {:?}", self.device);
|
||||
eprintln!("[GLM-OCR] Temperature: {:?}", temperature);
|
||||
eprintln!("[GLM-OCR] Top_p: {:?}", top_p);
|
||||
eprintln!("[GLM-OCR] Max tokens: {}", mes.max_tokens.unwrap_or(512));
|
||||
}
|
||||
|
||||
let processed = self.processor.process_info(
|
||||
image_path,
|
||||
&prompt,
|
||||
&self.tokenizer,
|
||||
self.image_token_id,
|
||||
self.image_start_token_id,
|
||||
self.image_end_token_id,
|
||||
self.patch_size,
|
||||
self.temporal_patch_size,
|
||||
self.spatial_merge_size,
|
||||
)?;
|
||||
|
||||
if std::env::var("GLM_DEBUG").is_ok() {
|
||||
eprintln!("[GLM-OCR] ===== AFTER PROCESS_INFO =====");
|
||||
eprintln!("[GLM-OCR] input_ids shape: {:?}", processed.input_ids.shape());
|
||||
let input_ids_vec = processed.input_ids.squeeze(0).unwrap().to_vec1::<u32>().unwrap();
|
||||
eprintln!("[GLM-OCR] input_ids (first 20): {:?}", &input_ids_vec[..20.min(input_ids_vec.len())]);
|
||||
eprintln!("[GLM-OCR] pixel_values shape: {:?}", processed.pixel_values.shape());
|
||||
eprintln!("[GLM-OCR] image_mask shape: {:?}", processed.image_mask.shape());
|
||||
eprintln!("[GLM-OCR] grid_thw: {:?}", processed.grid_thw);
|
||||
}
|
||||
|
||||
let mut input_ids = processed.input_ids;
|
||||
let pixel_values = Some(processed.pixel_values);
|
||||
let image_grid_thw = Some(processed.grid_thw);
|
||||
let image_mask = Some(processed.image_mask);
|
||||
let mut seqlen_offset = 0;
|
||||
let mut seq_len = input_ids.dim(1)?;
|
||||
let mut generate = Vec::new();
|
||||
let sample_len = mes.max_tokens.unwrap_or(512);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
eprintln!("[GLM-OCR] ===== START GENERATION =====");
|
||||
eprintln!("[GLM-OCR] Initial seq_len: {}", seq_len);
|
||||
eprintln!("[GLM-OCR] eos_token_ids: {:?}", self.eos_token_ids);
|
||||
}
|
||||
|
||||
for _ in 0..sample_len {
|
||||
let is_first_pass = seqlen_offset == 0;
|
||||
let logits = self.model.forward(
|
||||
&input_ids,
|
||||
if is_first_pass {
|
||||
pixel_values.as_ref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if is_first_pass {
|
||||
image_grid_thw.as_ref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if is_first_pass {
|
||||
image_mask.as_ref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
seqlen_offset,
|
||||
)?;
|
||||
let logits = logits.i((0, seq_len - 1, ..))?.to_dtype(DType::F32)?;
|
||||
let logits = if self.generation_config.repetition_penalty != 1.0 {
|
||||
apply_repeat_penalty(&logits, self.generation_config.repetition_penalty, &generate)?
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
let next_token = logit_processor.sample(&logits)?;
|
||||
|
||||
generate.push(next_token);
|
||||
if self.eos_token_ids.contains(&next_token) {
|
||||
break;
|
||||
}
|
||||
seqlen_offset += seq_len;
|
||||
seq_len = 1;
|
||||
input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?;
|
||||
}
|
||||
|
||||
self.model.clear_kv_cache();
|
||||
let num_token = generate.len() as u32;
|
||||
let res = self.tokenizer.token_decode(generate)?;
|
||||
let response = build_completion_response(res, &self.model_name, Some(num_token));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn generate_stream(
|
||||
&mut self,
|
||||
mes: ChatCompletionParameters,
|
||||
) -> Result<
|
||||
Box<
|
||||
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
|
||||
+ Send
|
||||
+ Unpin
|
||||
+ '_,
|
||||
>,
|
||||
> {
|
||||
// Check if sampling is enabled - if do_sample is false, use greedy decoding (temperature = None)
|
||||
let do_sample = mes.temperature.is_some() || self.generation_config.do_sample;
|
||||
let temperature = if !do_sample {
|
||||
None // Greedy decoding
|
||||
} else {
|
||||
match mes.temperature {
|
||||
None => Some(self.generation_config.temperature),
|
||||
Some(tem) => Some(tem),
|
||||
}
|
||||
};
|
||||
let top_p = match mes.top_p {
|
||||
None => Some(self.generation_config.top_p),
|
||||
Some(top_p) => Some(top_p),
|
||||
};
|
||||
let top_k = Some(self.generation_config.top_k);
|
||||
let seed = match mes.seed {
|
||||
None => 34562u64,
|
||||
Some(s) => s as u64,
|
||||
};
|
||||
let mut logit_processor =
|
||||
get_logit_processor(temperature, top_p, top_k, seed);
|
||||
|
||||
// Extract image path and prompt from messages
|
||||
let image_urls = extract_image_url(&mes);
|
||||
let image_path = image_urls
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No image provided"))?;
|
||||
|
||||
// Get prompt text from messages
|
||||
let prompt = extract_text_from_messages(&mes).unwrap_or_else(|| "Extract all text from this image.".to_string());
|
||||
|
||||
let processed = self.processor.process_info(
|
||||
image_path,
|
||||
&prompt,
|
||||
&self.tokenizer,
|
||||
self.image_token_id,
|
||||
self.image_start_token_id,
|
||||
self.image_end_token_id,
|
||||
self.patch_size,
|
||||
self.temporal_patch_size,
|
||||
self.spatial_merge_size,
|
||||
)?;
|
||||
|
||||
let mut input_ids = processed.input_ids;
|
||||
let pixel_values = Some(processed.pixel_values);
|
||||
let image_grid_thw = Some(processed.grid_thw);
|
||||
let image_mask = Some(processed.image_mask);
|
||||
let mut seqlen_offset = 0;
|
||||
let mut seq_len = input_ids.dim(1)?;
|
||||
let sample_len = mes.max_tokens.unwrap_or(512);
|
||||
|
||||
let stream = stream! {
|
||||
let mut generated: Vec<u32> = Vec::new();
|
||||
let mut error_tokens = Vec::new();
|
||||
for _ in 0..sample_len {
|
||||
let is_first_pass = seqlen_offset == 0;
|
||||
let logits = self.model.forward(
|
||||
&input_ids,
|
||||
if is_first_pass { pixel_values.as_ref() } else { None },
|
||||
if is_first_pass { image_grid_thw.as_ref() } else { None },
|
||||
if is_first_pass { image_mask.as_ref() } else { None },
|
||||
seqlen_offset,
|
||||
).map_err(|e| anyhow!(format!("forward error: {e}")))?;
|
||||
let logits = logits.i((0, seq_len - 1, ..)).map_err(|e| anyhow!(format!("index error: {e}")))?.to_dtype(DType::F32).map_err(|e| anyhow!(format!("dtype error: {e}")))?;
|
||||
let logits = if self.generation_config.repetition_penalty != 1.0 {
|
||||
apply_repeat_penalty(&logits, self.generation_config.repetition_penalty, &generated).map_err(|e| anyhow!(format!("repeat penalty error: {e}")))?
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
let next_token = logit_processor.sample(&logits).map_err(|e| anyhow!(format!("sample error: {e}")))?;
|
||||
generated.push(next_token);
|
||||
|
||||
let mut decode_ids = Vec::new();
|
||||
if !error_tokens.is_empty() {
|
||||
decode_ids.extend_from_slice(&error_tokens);
|
||||
}
|
||||
decode_ids.push(next_token);
|
||||
|
||||
let decoded_token = self.tokenizer.token_decode(decode_ids).map_err(|e| anyhow!(format!("decode error: {e}")))?;
|
||||
if decoded_token.contains("�") {
|
||||
error_tokens.push(next_token);
|
||||
if error_tokens.len() > 3 {
|
||||
error_tokens.clear();
|
||||
}
|
||||
seqlen_offset += seq_len;
|
||||
seq_len = 1;
|
||||
input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device).map_err(|e| anyhow!(format!("tensor error: {e}")))?;
|
||||
continue;
|
||||
}
|
||||
error_tokens.clear();
|
||||
|
||||
let chunk = build_completion_chunk_response(decoded_token, &self.model_name, None, None);
|
||||
yield Ok(chunk);
|
||||
|
||||
if self.eos_token_ids.contains(&next_token) {
|
||||
break;
|
||||
}
|
||||
seqlen_offset += seq_len;
|
||||
seq_len = 1;
|
||||
input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device).map_err(|e| anyhow!(format!("tensor error: {e}")))?;
|
||||
}
|
||||
self.model.clear_kv_cache();
|
||||
};
|
||||
|
||||
Ok(Box::new(Box::pin(stream)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract text content from chat messages
|
||||
fn extract_text_from_messages(mes: &ChatCompletionParameters) -> Option<String> {
|
||||
use aha_openai_dive::v1::resources::chat::{
|
||||
ChatMessage, ChatMessageContent, ChatMessageContentPart,
|
||||
};
|
||||
for msg in &mes.messages {
|
||||
if let ChatMessage::User { content, .. } = msg {
|
||||
match content {
|
||||
ChatMessageContent::Text(text) => return Some(text.clone()),
|
||||
ChatMessageContent::ContentPart(parts) => {
|
||||
for part in parts {
|
||||
if let ChatMessageContentPart::Text(text_part) = part {
|
||||
return Some(text_part.text.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod generate;
|
||||
pub mod model;
|
||||
pub mod processor;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,344 @@
|
||||
use anyhow::Result;
|
||||
use candle_core::{DType, Device, Tensor};
|
||||
|
||||
use super::config::GlmOcrPreprocessorConfig;
|
||||
use crate::tokenizer::TokenizerModel;
|
||||
use crate::utils::img_utils::get_image;
|
||||
|
||||
/// GLM-OCR Processor for image and text preprocessing.
|
||||
///
|
||||
/// Matches Python's Glm46VImageProcessor behavior:
|
||||
/// - Uses smart_resize to compute target dimensions
|
||||
/// - Outputs flattened patches format [num_patches, patch_dim]
|
||||
pub struct GlmOcrProcessor {
|
||||
image_mean: Vec<f32>,
|
||||
image_std: Vec<f32>,
|
||||
shortest_edge: usize, // min_pixels in Python
|
||||
longest_edge: usize, // max_pixels in Python
|
||||
patch_size: usize,
|
||||
merge_size: usize,
|
||||
temporal_patch_size: usize,
|
||||
device: Device,
|
||||
dtype: DType,
|
||||
}
|
||||
|
||||
pub struct ProcessedImage {
|
||||
pub pixel_values: Tensor, // Shape: [num_patches, patch_dim]
|
||||
pub grid_h: usize,
|
||||
pub grid_w: usize,
|
||||
}
|
||||
|
||||
pub struct ProcessedInput {
|
||||
pub input_ids: Tensor,
|
||||
pub pixel_values: Tensor, // Shape: [num_patches, patch_dim]
|
||||
pub image_mask: Tensor,
|
||||
pub grid_thw: Tensor,
|
||||
}
|
||||
|
||||
impl GlmOcrProcessor {
|
||||
pub fn new(path: &str, device: &Device, dtype: DType) -> Result<Self> {
|
||||
let config_path = format!("{}/preprocessor_config.json", path);
|
||||
|
||||
// Load preprocessor config
|
||||
let (image_mean, image_std, shortest_edge, longest_edge, patch_size, merge_size) =
|
||||
if std::path::Path::new(&config_path).exists() {
|
||||
let config: GlmOcrPreprocessorConfig = serde_json::from_slice(&std::fs::read(&config_path)?)?;
|
||||
// Parse size object - Python uses shortest_edge: 12544, longest_edge: 9633792
|
||||
let (shortest, longest) = if let Some(size_val) = &config.size {
|
||||
if let Some(obj) = size_val.as_object() {
|
||||
let s = obj.get("shortest_edge")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(12544);
|
||||
let l = obj.get("longest_edge")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(9_633_792);
|
||||
(s, l)
|
||||
} else {
|
||||
(12544, 9_633_792)
|
||||
}
|
||||
} else {
|
||||
(12544, 9_633_792)
|
||||
};
|
||||
let patch_size = config.patch_size.unwrap_or(14);
|
||||
let merge_size = config.merge_size.unwrap_or(2);
|
||||
(config.image_mean, config.image_std, shortest, longest, patch_size, merge_size)
|
||||
} else {
|
||||
(
|
||||
vec![0.48145466, 0.4578275, 0.40821073],
|
||||
vec![0.26862954, 0.26130258, 0.27577711],
|
||||
12544, // Python's min_pixels
|
||||
9_633_792, // Python's max_pixels
|
||||
14, // patch_size
|
||||
2, // merge_size
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
image_mean,
|
||||
image_std,
|
||||
shortest_edge,
|
||||
longest_edge,
|
||||
patch_size,
|
||||
merge_size,
|
||||
temporal_patch_size: 2, // Fixed for images
|
||||
device: device.clone(),
|
||||
dtype,
|
||||
})
|
||||
}
|
||||
|
||||
/// Python's smart_resize implementation
|
||||
/// Returns (resized_height, resized_width)
|
||||
fn smart_resize(&self, height: usize, width: usize) -> (usize, usize) {
|
||||
let factor = self.patch_size * self.merge_size; // 28
|
||||
let temporal_factor = self.temporal_patch_size; // 2
|
||||
|
||||
// Ensure minimum size
|
||||
let mut h = height;
|
||||
let mut w = width;
|
||||
if h < factor || w < factor {
|
||||
let scale = (factor as f32 / h.min(w) as f32).max(1.0);
|
||||
h = (h as f32 * scale).round() as usize;
|
||||
w = (w as f32 * scale).round() as usize;
|
||||
}
|
||||
|
||||
// Check aspect ratio constraint
|
||||
if w.max(h) as f32 / w.min(h) as f32 > 200.0 {
|
||||
// Would raise error in Python
|
||||
// For now, just proceed
|
||||
}
|
||||
|
||||
// Round to nearest multiple of factor
|
||||
let h_bar = ((h + factor / 2) / factor) * factor;
|
||||
let w_bar = ((w + factor / 2) / factor) * factor;
|
||||
let t_bar = ((temporal_factor + temporal_factor / 2) / temporal_factor) * temporal_factor;
|
||||
|
||||
// Check max_pixels constraint
|
||||
let max_pixels = self.longest_edge;
|
||||
let min_pixels = self.shortest_edge;
|
||||
|
||||
let mut final_h = h_bar;
|
||||
let mut final_w = w_bar;
|
||||
|
||||
if t_bar * h_bar * w_bar > max_pixels {
|
||||
let beta = ((h * w) as f32 / max_pixels as f32).sqrt();
|
||||
final_h = (factor.max((h as f32 / beta / factor as f32).floor() as usize * factor)) as usize;
|
||||
final_w = (factor.max((w as f32 / beta / factor as f32).floor() as usize * factor)) as usize;
|
||||
} else if t_bar * h_bar * w_bar < min_pixels {
|
||||
let beta = (min_pixels as f32 / (h * w) as f32).sqrt();
|
||||
final_h = ((h as f32 * beta / factor as f32).ceil() as usize * factor) as usize;
|
||||
final_w = ((w as f32 * beta / factor as f32).ceil() as usize * factor) as usize;
|
||||
}
|
||||
|
||||
(final_h, final_w)
|
||||
}
|
||||
|
||||
/// Process image for vision encoder.
|
||||
///
|
||||
/// Matches Python's Glm46VImageProcessor._preprocess():
|
||||
/// 1. Resize using smart_resize
|
||||
/// 2. Normalize
|
||||
/// 3. Reshape into flattened patches [num_patches, patch_dim]
|
||||
///
|
||||
/// Output format: [grid_t * grid_h * grid_w, channels * temporal_patch_size * patch_size * patch_size]
|
||||
/// For images: grid_t = 1, so [grid_h * grid_w, 3 * 2 * 14 * 14] = [num_patches, 1176]
|
||||
pub fn process_image(&self, image_path: &str) -> Result<ProcessedImage> {
|
||||
let img = get_image(image_path)?;
|
||||
let (orig_w, orig_h) = (img.width() as usize, img.height() as usize);
|
||||
|
||||
// Use smart_resize to compute target dimensions
|
||||
let (target_h, target_w) = self.smart_resize(orig_h, orig_w);
|
||||
if std::env::var("GLM_DEBUG").is_ok() {
|
||||
let grid_h = target_h / self.patch_size;
|
||||
let grid_w = target_w / self.patch_size;
|
||||
let n_patches = grid_h * grid_w;
|
||||
let pv_elems = n_patches * 3 * self.temporal_patch_size * self.patch_size * self.patch_size;
|
||||
let pv_mb = (pv_elems * 2) as f64 / 1_048_576.0; // bf16
|
||||
let attn_mb = (16 * n_patches * n_patches * 4) as f64 / 1_048_576.0; // 16 heads, f32
|
||||
eprintln!(
|
||||
"[GLM-OCR OOM-DBG] Image: orig={orig_w}x{orig_h} -> target={target_w}x{target_h} \
|
||||
grid={grid_w}x{grid_h} n_patches={n_patches} \
|
||||
pixel_values={pv_mb:.1}MB vision_attn_per_layer={attn_mb:.1}MB"
|
||||
);
|
||||
}
|
||||
|
||||
// Resize image
|
||||
let img = img.resize_exact(
|
||||
target_w as u32,
|
||||
target_h as u32,
|
||||
image::imageops::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
// Convert to RGB and normalize
|
||||
let img = img.to_rgb8();
|
||||
let pixels: Vec<f32> = img
|
||||
.pixels()
|
||||
.flat_map(|p| {
|
||||
vec![
|
||||
p[0] as f32 / 255.0,
|
||||
p[1] as f32 / 255.0,
|
||||
p[2] as f32 / 255.0,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Reshape to [H, W, 3]
|
||||
let tensor = Tensor::from_vec(pixels, (target_h, target_w, 3), &self.device)?;
|
||||
|
||||
// Normalize
|
||||
let mean = Tensor::new(self.image_mean.clone(), &self.device)?.reshape((1, 1, 3))?;
|
||||
let std = Tensor::new(self.image_std.clone(), &self.device)?.reshape((1, 1, 3))?;
|
||||
let tensor = tensor.broadcast_sub(&mean)?.broadcast_div(&std)?;
|
||||
|
||||
// Now reshape into flattened patches like Python
|
||||
let grid_h = target_h / self.patch_size;
|
||||
let grid_w = target_w / self.patch_size;
|
||||
let patch_size = self.patch_size;
|
||||
let channels = 3;
|
||||
let temporal_patch_size = 2; // Python uses temporal_patch_size=2 even for images
|
||||
|
||||
// Reshape: [H, W, 3] -> [grid_h, patch_size, grid_w, patch_size, 3]
|
||||
let tensor = tensor.reshape((
|
||||
grid_h, patch_size,
|
||||
grid_w, patch_size,
|
||||
channels,
|
||||
))?;
|
||||
|
||||
// Permute to: [grid_h, grid_w, patch_size, patch_size, channels]
|
||||
let tensor = tensor.permute((0, 2, 1, 3, 4))?;
|
||||
|
||||
// Permute to put channels first: [grid_h, grid_w, channels, patch_size, patch_size]
|
||||
// Python's patch_embed.forward does: view(-1, C, T, P, P) so we need (C, T, P_h, P_w) order
|
||||
let tensor = tensor.permute((0, 1, 4, 2, 3))?;
|
||||
|
||||
// Reshape to: [num_patches, channels, patch_size, patch_size]
|
||||
let num_patches = grid_h * grid_w;
|
||||
let tensor = tensor.reshape((num_patches, channels, patch_size, patch_size))?;
|
||||
|
||||
// Add temporal dimension after C: [num_patches, channels, 1, patch_size, patch_size]
|
||||
let tensor = tensor.unsqueeze(2)?;
|
||||
// Repeat T times along temporal dim 2: [num_patches, channels, temporal_patch_size, patch_size, patch_size]
|
||||
let tensor = tensor.repeat((1, 1, temporal_patch_size, 1, 1))?;
|
||||
|
||||
// Flatten to: [num_patches, channels * temporal_patch_size * patch_size * patch_size]
|
||||
// This gives (C, T, P_h, P_w) order matching Python's patch_embed input
|
||||
let patch_dim = channels * temporal_patch_size * patch_size * patch_size;
|
||||
let tensor = tensor.reshape((num_patches, patch_dim))?;
|
||||
|
||||
// Convert to model dtype
|
||||
let tensor = tensor.to_dtype(self.dtype)?;
|
||||
|
||||
Ok(ProcessedImage {
|
||||
pixel_values: tensor,
|
||||
grid_h,
|
||||
grid_w,
|
||||
})
|
||||
}
|
||||
|
||||
/// Process image and text for multimodal input.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_path` - Path to input image
|
||||
/// * `prompt` - Text prompt/question about the image
|
||||
/// * `tokenizer` - Tokenizer for text encoding
|
||||
/// * `image_token_id` - Token ID for image content placeholders
|
||||
/// * `image_start_token_id` - Token ID marking start of image region
|
||||
/// * `image_end_token_id` - Token ID marking end of image region
|
||||
/// * `patch_size` - Vision encoder patch size (default: 14)
|
||||
/// * `temporal_patch_size` - Temporal patch size (default: 2, unused for images)
|
||||
/// * `spatial_merge_size` - Spatial merge factor (default: 2)
|
||||
///
|
||||
/// # Returns
|
||||
/// ProcessedInput containing:
|
||||
/// - input_ids: Combined image placeholder + text token IDs
|
||||
/// - pixel_values: Flattened patches tensor [num_patches, patch_dim]
|
||||
/// - image_mask: Boolean mask for image token positions
|
||||
/// - grid_thw: (temporal, height, width) grid dimensions for RoPE
|
||||
pub fn process_info(
|
||||
&self,
|
||||
image_path: &str,
|
||||
prompt: &str,
|
||||
tokenizer: &TokenizerModel,
|
||||
image_token_id: u32,
|
||||
image_start_token_id: u32,
|
||||
image_end_token_id: u32,
|
||||
_patch_size: usize,
|
||||
_temporal_patch_size: usize,
|
||||
spatial_merge_size: usize,
|
||||
) -> Result<ProcessedInput> {
|
||||
let processed_image = self.process_image(image_path)?;
|
||||
let pixel_values = processed_image.pixel_values;
|
||||
let grid_h = processed_image.grid_h;
|
||||
let grid_w = processed_image.grid_w;
|
||||
|
||||
// After spatial merge, each spatial_merge_size x spatial_merge_size block becomes 1 token
|
||||
let merged_h = grid_h / spatial_merge_size;
|
||||
let merged_w = grid_w / spatial_merge_size;
|
||||
let num_image_tokens = merged_h * merged_w;
|
||||
|
||||
// GLM-OCR format: [gMASK] <sop> <|user|> \n <|begin_of_image|> <|image|>*N <|end_of_image|> text <|assistant|> \n
|
||||
// Special token IDs:
|
||||
// 59248 = [gMASK]
|
||||
// 59250 = <sop>
|
||||
// 59253 = <|user|>
|
||||
// 59256 = <|begin_of_image|>
|
||||
// 59280 = <|image|>
|
||||
// 59257 = <|end_of_image|>
|
||||
// 59254 = <|assistant|>
|
||||
|
||||
// Build input_ids following Python format
|
||||
let mut input_ids_vec = Vec::new();
|
||||
|
||||
// Header: [gMASK] <sop> <|user|> \n
|
||||
input_ids_vec.push(59248); // [gMASK]
|
||||
input_ids_vec.push(59250); // <sop>
|
||||
input_ids_vec.push(59253); // <|user|>
|
||||
input_ids_vec.push(10); // newline
|
||||
|
||||
// Image tokens: <|begin_of_image|> <|image|>*N <|end_of_image|>
|
||||
input_ids_vec.push(image_start_token_id); // <|begin_of_image|>
|
||||
for _ in 0..num_image_tokens {
|
||||
input_ids_vec.push(image_token_id); // <|image|>
|
||||
}
|
||||
input_ids_vec.push(image_end_token_id); // <|end_of_image|>
|
||||
|
||||
// Text prompt (without special tokens - they're already added)
|
||||
let text_ids = tokenizer.text_encode_vec(prompt.to_string(), false)?;
|
||||
input_ids_vec.extend(text_ids);
|
||||
|
||||
// Generation prompt: <|assistant|> \n
|
||||
input_ids_vec.push(59254); // <|assistant|>
|
||||
input_ids_vec.push(10); // newline
|
||||
|
||||
let input_ids = Tensor::from_vec(
|
||||
input_ids_vec.clone(),
|
||||
(1, input_ids_vec.len()),
|
||||
&self.device,
|
||||
)?;
|
||||
|
||||
// Create image mask (1s at image token positions, 0 elsewhere)
|
||||
// Image tokens start after header (4 tokens) + start token (1 token) = index 5
|
||||
let mut image_mask_vec = vec![0u32; input_ids_vec.len()];
|
||||
let image_start_idx = 5; // After [gMASK, sop, user, newline, begin_image]
|
||||
for i in 0..num_image_tokens {
|
||||
image_mask_vec[image_start_idx + i] = 1;
|
||||
}
|
||||
let image_mask = Tensor::from_vec(image_mask_vec, (1, input_ids_vec.len()), &self.device)?;
|
||||
|
||||
// Compute grid_thw for RoPE
|
||||
// For images: grid_t = 1
|
||||
let grid_thw = Tensor::from_vec(
|
||||
vec![1u32, grid_h as u32, grid_w as u32],
|
||||
(3,),
|
||||
&self.device,
|
||||
)?;
|
||||
|
||||
Ok(ProcessedInput {
|
||||
input_ids,
|
||||
pixel_values,
|
||||
image_mask,
|
||||
grid_thw,
|
||||
})
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -5,6 +5,7 @@ pub mod deepseek_ocr;
|
||||
pub mod feature_extractor;
|
||||
pub mod fun_asr_nano;
|
||||
pub mod glm_asr_nano;
|
||||
pub mod glm_ocr;
|
||||
pub mod hunyuan_ocr;
|
||||
pub mod mask_gct;
|
||||
pub mod minicpm4;
|
||||
@@ -27,7 +28,7 @@ use rocket::futures::Stream;
|
||||
use crate::models::{
|
||||
deepseek_ocr::generate::DeepseekOCRGenerateModel,
|
||||
fun_asr_nano::generate::FunAsrNanoGenerateModel,
|
||||
glm_asr_nano::generate::GlmAsrNanoGenerateModel,
|
||||
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,
|
||||
@@ -81,6 +82,8 @@ pub enum WhichModel {
|
||||
GlmASRNano2512,
|
||||
#[value(name = "fun-asr-nano-2512", hide = true)]
|
||||
FunASRNano2512,
|
||||
#[value(name = "glm-ocr", hide = true)]
|
||||
GlmOCR,
|
||||
}
|
||||
|
||||
impl WhichModel {
|
||||
@@ -109,6 +112,7 @@ impl WhichModel {
|
||||
WhichModel::VoxCPM1_5 => "OpenBMB/VoxCPM1.5",
|
||||
WhichModel::GlmASRNano2512 => "ZhipuAI/GLM-ASR-Nano-2512",
|
||||
WhichModel::FunASRNano2512 => "FunAudioLLM/Fun-ASR-Nano-2512",
|
||||
WhichModel::GlmOCR => "ZhipuAI/GLM-OCR",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,7 @@ impl WhichModel {
|
||||
| WhichModel::Qwen3_5_4B
|
||||
| WhichModel::Qwen3_5_9B => "vlm",
|
||||
// OCR models
|
||||
WhichModel::DeepSeekOCR | WhichModel::HunyuanOCR | WhichModel::PaddleOCRVL => "ocr",
|
||||
WhichModel::DeepSeekOCR | WhichModel::HunyuanOCR | WhichModel::GlmOCR | WhichModel::PaddleOCRVL => "ocr",
|
||||
// ASR models
|
||||
WhichModel::Qwen3ASR0_6B
|
||||
| WhichModel::Qwen3ASR1_7B
|
||||
@@ -169,6 +173,7 @@ pub enum ModelInstance<'a> {
|
||||
VoxCPM(Box<VoxCPMGenerate>),
|
||||
GlmASRNano(GlmAsrNanoGenerateModel<'a>),
|
||||
FunASRNano(FunAsrNanoGenerateModel),
|
||||
GlmOCR(GlmOcrGenerateModel<'a>),
|
||||
}
|
||||
|
||||
impl<'a> GenerateModel for ModelInstance<'a> {
|
||||
@@ -187,6 +192,7 @@ impl<'a> GenerateModel for ModelInstance<'a> {
|
||||
ModelInstance::VoxCPM(model) => model.generate(mes),
|
||||
ModelInstance::GlmASRNano(model) => model.generate(mes),
|
||||
ModelInstance::FunASRNano(model) => model.generate(mes),
|
||||
ModelInstance::GlmOCR(model) => model.generate(mes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +221,7 @@ impl<'a> GenerateModel for ModelInstance<'a> {
|
||||
ModelInstance::VoxCPM(model) => model.generate_stream(mes),
|
||||
ModelInstance::GlmASRNano(model) => model.generate_stream(mes),
|
||||
ModelInstance::FunASRNano(model) => model.generate_stream(mes),
|
||||
ModelInstance::GlmOCR(model) => model.generate_stream(mes),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,6 +316,10 @@ pub fn load_model(model_type: WhichModel, path: &str) -> Result<ModelInstance<'_
|
||||
let model = FunAsrNanoGenerateModel::init(path, None, None)?;
|
||||
ModelInstance::FunASRNano(model)
|
||||
}
|
||||
WhichModel::GlmOCR => {
|
||||
let model = GlmOcrGenerateModel::init(path, None, None)?;
|
||||
ModelInstance::GlmOCR(model)
|
||||
}
|
||||
};
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
@@ -159,6 +159,105 @@ pub fn glm_asr_apply_rotary_pos_emb(
|
||||
Ok((q_embed, k_embed))
|
||||
}
|
||||
|
||||
/// Interleaved rotation used by GLM-OCR text decoder.
|
||||
///
|
||||
/// Python `rotate_half_llm`:
|
||||
/// x1 = x[..., 0::2] # even indices
|
||||
/// x2 = x[..., 1::2] # odd indices
|
||||
/// return stack((-x2, x1), dim=-1).flatten(-2)
|
||||
/// # e.g. [q0,q1,q2,q3] → [-q1, q0, -q3, q2]
|
||||
///
|
||||
/// Each adjacent pair (x_{2i}, x_{2i+1}) is rotated to (-x_{2i+1}, x_{2i}).
|
||||
/// This is the correct counterpart to `repeat_interleave(2)` style cos/sin.
|
||||
fn rotate_half_llm(x: &Tensor) -> Result<Tensor> {
|
||||
let last_dim = x.dim(D::Minus1)?;
|
||||
let half = last_dim / 2;
|
||||
// Reshape (..., D) → (..., D/2, 2) so each row is one adjacent pair
|
||||
let mut pair_shape = x.dims().to_vec();
|
||||
let rank = pair_shape.len();
|
||||
pair_shape[rank - 1] = half;
|
||||
pair_shape.push(2);
|
||||
let x_pairs = x.reshape(pair_shape)?; // (..., half, 2)
|
||||
// col 0 = even elements [x0, x2, ...], col 1 = odd elements [x1, x3, ...]
|
||||
let x_even = x_pairs.narrow(D::Minus1, 0, 1)?; // (..., half, 1)
|
||||
let x_odd = x_pairs.narrow(D::Minus1, 1, 1)?; // (..., half, 1)
|
||||
let neg_x_odd = x_odd.affine(-1.0, 0.0)?;
|
||||
// Concatenate [-x_odd, x_even] → [[-x1,x0], [-x3,x2], ...]
|
||||
let result_pairs = Tensor::cat(&[&neg_x_odd, &x_even], D::Minus1)?; // (..., half, 2)
|
||||
// Flatten last two dims back to D: [-x1, x0, -x3, x2, ...]
|
||||
Ok(result_pairs.reshape(x.dims().to_vec())?)
|
||||
}
|
||||
|
||||
pub fn glm_ocr_apply_rotary_pos_emb(
|
||||
q: &Tensor,
|
||||
k: &Tensor,
|
||||
cos: &Tensor,
|
||||
sin: &Tensor,
|
||||
) -> Result<(Tensor, Tensor)> {
|
||||
// GLM-OCR applies rotary to only the first rotary_dim of head_dim
|
||||
// cos/sin: (bs, seq_len, head_dim) - already doubled via cat(freqs, freqs)
|
||||
// q/k: (bs, n_head, seq_len, head_dim)
|
||||
// Python: unsqueeze_dim=1
|
||||
// - rank 2 (seq_len, head_dim) -> unsqueeze(1) -> (seq_len, 1, head_dim)
|
||||
// - rank 3 (bs, seq_len, head_dim) -> unsqueeze(1) -> (bs, 1, seq_len, head_dim)
|
||||
let mut cos = cos.clone();
|
||||
let mut sin = sin.clone();
|
||||
|
||||
cos = cos.unsqueeze(1)?; // (seq_len, head_dim) -> (seq_len, 1, head_dim)
|
||||
sin = sin.unsqueeze(1)?;
|
||||
|
||||
// Python: cos = cos[..., :cos.shape[-1]//2].repeat_interleave(2, dim=-1)
|
||||
// Take first half and interleave each element
|
||||
let full_dim = cos.dim(D::Minus1)?;
|
||||
let half_dim = full_dim / 2;
|
||||
let cos_half = cos.narrow(D::Minus1, 0, half_dim)?;
|
||||
let sin_half = sin.narrow(D::Minus1, 0, half_dim)?;
|
||||
// repeat_interleave(2, dim=-1): [a,b,c] -> [a,a,b,b,c,c]
|
||||
let cos_interleaved = cos_half
|
||||
.unsqueeze(D::Minus1)?
|
||||
.broadcast_mul(&Tensor::ones(
|
||||
&[1, 1, 1, 1, 2],
|
||||
cos_half.dtype(),
|
||||
cos_half.device(),
|
||||
)?)?
|
||||
.reshape(cos.shape())?;
|
||||
let sin_interleaved = sin_half
|
||||
.unsqueeze(D::Minus1)?
|
||||
.broadcast_mul(&Tensor::ones(
|
||||
&[1, 1, 1, 1, 2],
|
||||
sin_half.dtype(),
|
||||
sin_half.device(),
|
||||
)?)?
|
||||
.reshape(sin.shape())?;
|
||||
|
||||
let cos = cos_interleaved.to_dtype(q.dtype())?;
|
||||
let sin = sin_interleaved.to_dtype(q.dtype())?;
|
||||
|
||||
let rotary_dim = cos.dim(D::Minus1)?;
|
||||
// Split q/k into rotary and pass-through portions
|
||||
let q_rot = q.narrow(D::Minus1, 0, rotary_dim)?;
|
||||
let q_pass = q.narrow(D::Minus1, rotary_dim, q.dim(D::Minus1)? - rotary_dim)?;
|
||||
let k_rot = k.narrow(D::Minus1, 0, rotary_dim)?;
|
||||
let k_pass = k.narrow(D::Minus1, rotary_dim, k.dim(D::Minus1)? - rotary_dim)?;
|
||||
|
||||
// Apply rotary: q_rot * cos + rotate_half_llm(q_rot) * sin
|
||||
// Must use interleaved rotate_half_llm (not split-half rotate_half) because
|
||||
// cos/sin use repeat_interleave(2) format: [c0,c0,c1,c1,...].
|
||||
// rotate_half_llm rotates adjacent pairs (q_{2i},q_{2i+1}) → (-q_{2i+1}, q_{2i}),
|
||||
// which is the correct counterpart for this cos/sin format.
|
||||
let q_embed = q_rot
|
||||
.broadcast_mul(&cos)?
|
||||
.add(&rotate_half_llm(&q_rot)?.broadcast_mul(&sin)?)?;
|
||||
let k_embed = k_rot
|
||||
.broadcast_mul(&cos)?
|
||||
.add(&rotate_half_llm(&k_rot)?.broadcast_mul(&sin)?)?;
|
||||
|
||||
// Concatenate rotary and pass-through portions
|
||||
let q_embed = Tensor::cat(&[&q_embed, &q_pass], D::Minus1)?;
|
||||
let k_embed = Tensor::cat(&[&k_embed, &k_pass], D::Minus1)?;
|
||||
Ok((q_embed, k_embed))
|
||||
}
|
||||
|
||||
pub fn roformer_rotate(x: &Tensor) -> Result<Tensor> {
|
||||
let dims = x.dims();
|
||||
let last_dim = dims
|
||||
|
||||
Reference in New Issue
Block a user