This commit is contained in:
jason
2026-03-05 23:49:49 -05:00
parent 14ecd4676c
commit 3156818a0c
11 changed files with 3157 additions and 3 deletions
+2 -1
View File
@@ -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",
}
}
+69
View File
@@ -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(())
}
}
+1
View File
@@ -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;
+5
View File
@@ -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(())
+369
View File
@@ -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)
}
+450
View File
@@ -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
}
+4
View File
@@ -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
+344
View File
@@ -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
View File
@@ -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)
}
+99
View File
@@ -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