use crate::error::{Error as MlmfError, Result};
use crate::multimodal::*;
use candle_core::{DType, Device, Module, Tensor};
use candle_nn::{Linear, VarBuilder, VarMap};
use std::collections::HashMap;
use std::sync::Arc;
pub struct BasicMultiModalProcessor {
config: MultiModalConfig,
device: Device,
dtype: DType,
_text_encoder_placeholder: bool,
_image_encoder_placeholder: bool,
_audio_encoder_placeholder: bool,
_video_encoder_placeholder: bool,
cross_attention_layers: HashMap<(Modality, Modality), CrossModalAttention>,
fusion_layer: Option<FusionLayer>,
stats: MultiModalStats,
}
impl BasicMultiModalProcessor {
pub fn new(config: MultiModalConfig, device: Device, dtype: DType) -> Result<Self> {
let mut processor = Self {
config: config.clone(),
device: device.clone(),
dtype,
_text_encoder_placeholder: false,
_image_encoder_placeholder: false,
_audio_encoder_placeholder: false,
_video_encoder_placeholder: false,
cross_attention_layers: HashMap::new(),
fusion_layer: None,
stats: MultiModalStats {
processing_times: HashMap::new(),
memory_usage: HashMap::new(),
attention_stats: HashMap::new(),
total_time_ms: 0.0,
samples_processed: 0,
},
};
processor.initialize_attention_layers()?;
processor.initialize_fusion_layer()?;
Ok(processor)
}
fn initialize_attention_layers(&mut self) -> Result<()> {
let modalities: Vec<Modality> = self.config.modalities.keys().cloned().collect();
for &query_mod in &modalities {
for &key_mod in &modalities {
if query_mod != key_mod {
let query_config = &self.config.modalities[&query_mod];
let key_config = &self.config.modalities[&key_mod];
let attention_layer = CrossModalAttention::new(
query_config.embedding_dim,
key_config.embedding_dim,
&self.config.cross_modal_attention,
&self.device,
self.dtype,
)?;
self.cross_attention_layers
.insert((query_mod, key_mod), attention_layer);
}
}
}
Ok(())
}
fn initialize_fusion_layer(&mut self) -> Result<()> {
let total_dim: usize = self
.config
.modalities
.values()
.map(|config| config.embedding_dim)
.sum();
self.fusion_layer = Some(FusionLayer::new(
total_dim,
&self.config.fusion_strategy,
&self.device,
self.dtype,
)?);
Ok(())
}
}
impl MultiModalProcessor for BasicMultiModalProcessor {
fn process(&self, input: MultiModalInput) -> Result<MultiModalOutput> {
let start_time = std::time::Instant::now();
let mut modality_embeddings = HashMap::new();
let mut processing_times = HashMap::new();
for (modality, modality_input) in &input.modality_inputs {
let modality_start = std::time::Instant::now();
let preprocessed = self.preprocess_modality(*modality, modality_input.tensor())?;
let embeddings = self.encode_modality(*modality, &preprocessed)?;
modality_embeddings.insert(*modality, embeddings);
processing_times.insert(*modality, modality_start.elapsed().as_secs_f64() * 1000.0);
}
let mut attention_weights = HashMap::new();
let mut attended_embeddings = modality_embeddings.clone();
for ((query_mod, key_mod), attention_layer) in &self.cross_attention_layers {
if let (Some(query_emb), Some(key_emb)) = (
modality_embeddings.get(query_mod),
modality_embeddings.get(key_mod),
) {
let (attended, weights) = attention_layer.forward(query_emb, key_emb, key_emb)?;
attended_embeddings.insert(*query_mod, attended);
attention_weights.insert((*query_mod, *key_mod), weights);
}
}
let fused_embeddings = if let Some(fusion_layer) = &self.fusion_layer {
let embeddings_vec: Vec<&Tensor> = attended_embeddings.values().collect();
fusion_layer.fuse(&embeddings_vec)?
} else {
self.simple_concatenation(&attended_embeddings)?
};
let total_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(MultiModalOutput {
fused_embeddings,
modality_embeddings: attended_embeddings,
attention_weights,
metadata: HashMap::new(),
})
}
fn supported_modalities(&self) -> Vec<Modality> {
self.config.modalities.keys().cloned().collect()
}
fn config(&self) -> &MultiModalConfig {
&self.config
}
fn preprocess_modality(&self, modality: Modality, input: &Tensor) -> Result<Tensor> {
let config = self.config.modalities.get(&modality).ok_or_else(|| {
MlmfError::invalid_config(format!("No configuration for modality: {:?}", modality))
})?;
match &config.preprocessing {
PreprocessingConfig::Text {
max_length,
padding,
truncation,
..
} => self.preprocess_text(input, *max_length, *padding, *truncation),
PreprocessingConfig::Image {
resize,
normalize,
patch_size,
} => self.preprocess_image(input, *resize, *normalize, *patch_size),
PreprocessingConfig::Audio {
sample_rate,
frame_length,
hop_length,
n_mels,
} => self.preprocess_audio(input, *sample_rate, *frame_length, *hop_length, *n_mels),
PreprocessingConfig::Video {
frame_rate,
frame_size,
temporal_window,
} => self.preprocess_video(input, *frame_rate, *frame_size, *temporal_window),
PreprocessingConfig::Custom(_) => {
Ok(input.clone())
}
}
}
fn cross_modal_attention(
&self,
query_modality: Modality,
key_modality: Modality,
query: &Tensor,
key: &Tensor,
value: &Tensor,
) -> Result<(Tensor, Tensor)> {
if let Some(attention_layer) = self
.cross_attention_layers
.get(&(query_modality, key_modality))
{
attention_layer.forward(query, key, value)
} else {
Err(MlmfError::invalid_config(format!(
"No cross-modal attention layer for {:?} -> {:?}",
query_modality, key_modality
)))
}
}
}
impl BasicMultiModalProcessor {
fn encode_modality(&self, modality: Modality, input: &Tensor) -> Result<Tensor> {
match modality {
Modality::Text => {
self.simple_projection(input, 768)
}
Modality::Image => self.simple_projection(input, 2048),
Modality::Audio => self.simple_projection(input, 512),
Modality::Video => self.simple_projection(input, 1024),
Modality::Custom(_) => {
Ok(input.clone())
}
}
}
fn preprocess_text(
&self,
input: &Tensor,
max_length: usize,
padding: bool,
truncation: bool,
) -> Result<Tensor> {
let shape = input.shape();
if shape.dims().len() < 2 {
return Err(MlmfError::invalid_format(
"Text input must have at least 2 dimensions".to_string(),
));
}
let seq_len = shape.dims()[1];
if truncation && seq_len > max_length {
let indices = Tensor::arange(0, max_length as i64, &self.device)?;
Ok(input.index_select(&indices, 1)?)
} else if padding && seq_len < max_length {
let batch_size = shape.dims()[0];
let pad_length = max_length - seq_len;
let padding_tensor =
Tensor::zeros((batch_size, pad_length), input.dtype(), &self.device)?;
Ok(Tensor::cat(&[input, &padding_tensor], 1)?)
} else {
Ok(input.clone())
}
}
fn preprocess_image(
&self,
input: &Tensor,
_resize: Option<(u32, u32)>,
normalize: bool,
_patch_size: u32,
) -> Result<Tensor> {
let mut processed = input.clone();
if normalize {
processed = ((processed - 0.5)? / 0.5)?;
}
Ok(processed)
}
fn preprocess_audio(
&self,
input: &Tensor,
_sample_rate: u32,
_frame_length: usize,
_hop_length: usize,
_n_mels: usize,
) -> Result<Tensor> {
Ok(input.clone())
}
fn preprocess_video(
&self,
input: &Tensor,
_frame_rate: f32,
_frame_size: (u32, u32),
_temporal_window: usize,
) -> Result<Tensor> {
Ok(input.clone())
}
fn simple_projection(&self, input: &Tensor, output_dim: usize) -> Result<Tensor> {
let input_shape = input.shape();
let input_dim = input_shape.dims()[input_shape.dims().len() - 1];
let weights = Tensor::randn(0f32, 1f32, (input_dim, output_dim), &self.device)?;
Ok(input.matmul(&weights)?)
}
fn simple_concatenation(&self, embeddings: &HashMap<Modality, Tensor>) -> Result<Tensor> {
let tensors: Vec<&Tensor> = embeddings.values().collect();
if tensors.is_empty() {
return Err(MlmfError::invalid_format(
"No embeddings to concatenate".to_string(),
));
}
let last_dim = tensors[0].shape().dims().len() - 1;
Ok(Tensor::cat(&tensors, last_dim)?)
}
}
pub struct CrossModalAttention {
query_proj: Linear,
key_proj: Linear,
value_proj: Linear,
output_proj: Linear,
num_heads: usize,
head_dim: usize,
temperature: f32,
}
impl CrossModalAttention {
pub fn new(
query_dim: usize,
key_dim: usize,
config: &CrossModalAttentionConfig,
device: &Device,
dtype: DType,
) -> Result<Self> {
let head_dim = query_dim / config.num_heads;
let attention_dim = config.num_heads * head_dim;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, dtype, device);
let query_proj = Linear::new(
Tensor::randn(0f32, 1f32, (query_dim, attention_dim), device)?,
None,
);
let key_proj = Linear::new(
Tensor::randn(0f32, 1f32, (key_dim, attention_dim), device)?,
None,
);
let value_proj = Linear::new(
Tensor::randn(0f32, 1f32, (key_dim, attention_dim), device)?,
None,
);
let output_proj = Linear::new(
Tensor::randn(0f32, 1f32, (attention_dim, query_dim), device)?,
None,
);
Ok(Self {
query_proj,
key_proj,
value_proj,
output_proj,
num_heads: config.num_heads,
head_dim,
temperature: config.temperature,
})
}
pub fn forward(
&self,
query: &Tensor,
key: &Tensor,
value: &Tensor,
) -> Result<(Tensor, Tensor)> {
let batch_size = query.shape().dims()[0];
let seq_len_q = query.shape().dims()[1];
let seq_len_k = key.shape().dims()[1];
let q = self.query_proj.forward(query)?;
let k = self.key_proj.forward(key)?;
let v = self.value_proj.forward(value)?;
let q = q
.reshape((batch_size, seq_len_q, self.num_heads, self.head_dim))?
.transpose(1, 2)?; let k = k
.reshape((batch_size, seq_len_k, self.num_heads, self.head_dim))?
.transpose(1, 2)?; let v = v
.reshape((batch_size, seq_len_k, self.num_heads, self.head_dim))?
.transpose(1, 2)?;
let scores = q.matmul(&k.transpose(2, 3)?)?; let scale_factor = (self.head_dim as f64).sqrt() as f32;
let scale_tensor = Tensor::full(scale_factor, scores.shape(), scores.device())?;
let scaled_scores = scores.div(&scale_tensor)?;
let attention_weights = candle_nn::ops::softmax(&scaled_scores, 3)?;
let attended = attention_weights.matmul(&v)?;
let attended = attended.transpose(1, 2)?.reshape((
batch_size,
seq_len_q,
self.num_heads * self.head_dim,
))?;
let output = self.output_proj.forward(&attended)?;
let avg_attention = attention_weights.mean(1)?;
Ok((output, avg_attention))
}
}
pub struct FusionLayer {
strategy: FusionStrategy,
projection: Option<Linear>,
attention_weights: Option<Linear>,
}
impl FusionLayer {
pub fn new(
input_dim: usize,
strategy: &FusionStrategy,
device: &Device,
dtype: DType,
) -> Result<Self> {
let (projection, attention_weights) = match strategy {
FusionStrategy::EarlyFusion => (None, None),
FusionStrategy::LateFusion => {
let proj = Linear::new(
Tensor::randn(0f32, 1f32, (input_dim, input_dim), device)?,
Some(Tensor::zeros(input_dim, dtype, device)?),
);
(Some(proj), None)
}
FusionStrategy::AttentionFusion { attention_dim } => {
let proj = Linear::new(
Tensor::randn(0f32, 1f32, (input_dim, *attention_dim), device)?,
None,
);
let attn = Linear::new(
Tensor::randn(0f32, 1f32, (*attention_dim, 1), device)?,
None,
);
(Some(proj), Some(attn))
}
_ => (None, None),
};
Ok(Self {
strategy: strategy.clone(),
projection,
attention_weights,
})
}
pub fn fuse(&self, embeddings: &[&Tensor]) -> Result<Tensor> {
if embeddings.is_empty() {
return Err(MlmfError::invalid_format(
"No embeddings to fuse".to_string(),
));
}
match &self.strategy {
FusionStrategy::EarlyFusion => {
let last_dim = embeddings[0].shape().dims().len() - 1;
Ok(Tensor::cat(embeddings, last_dim)?)
}
FusionStrategy::LateFusion => {
let stacked = Tensor::stack(embeddings, 0)?;
let mean_embedding = stacked.mean(0)?;
if let Some(proj) = &self.projection {
Ok(proj.forward(&mean_embedding)?)
} else {
Ok(mean_embedding)
}
}
FusionStrategy::AttentionFusion { .. } => {
if let (Some(proj), Some(attn_weights)) =
(&self.projection, &self.attention_weights)
{
let stacked = Tensor::stack(embeddings, 1)?; let projected = proj.forward(&stacked)?;
let attention_scores = attn_weights.forward(&projected)?;
let attention_weights = candle_nn::ops::softmax(&attention_scores, 1)?;
Ok((stacked * attention_weights)?.sum(1)?)
} else {
let stacked = Tensor::stack(embeddings, 0)?;
Ok(stacked.mean(0)?)
}
}
_ => {
let last_dim = embeddings[0].shape().dims().len() - 1;
Ok(Tensor::cat(embeddings, last_dim)?)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use candle_core::Device;
#[test]
fn test_basic_processor_creation() -> Result<()> {
let config = MultiModalConfig::default();
let device = Device::Cpu;
let processor = BasicMultiModalProcessor::new(config, device, DType::F32)?;
assert_eq!(processor.supported_modalities().len(), 2);
assert!(processor.supported_modalities().contains(&Modality::Text));
assert!(processor.supported_modalities().contains(&Modality::Image));
Ok(())
}
}