Files
aha/src/models/glm_ocr/generate.rs
T
2026-03-06 11:25:44 -05:00

394 lines
15 KiB
Rust

//! 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
};
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());
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 mut generate = Vec::new();
let sample_len = mes.max_tokens.unwrap_or(512);
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
}