use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use crate::error::{EcosystemError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PyTorchDType {
Float32,
Float64,
Float16,
BFloat16,
Int32,
Int64,
Int8,
UInt8,
Bool,
}
impl PyTorchDType {
pub fn size(&self) -> usize {
match self {
Self::Float32 | Self::Int32 => 4,
Self::Float64 | Self::Int64 => 8,
Self::Float16 | Self::BFloat16 => 2,
Self::Int8 | Self::UInt8 | Self::Bool => 1,
}
}
pub fn to_torch_str(&self) -> &'static str {
match self {
Self::Float32 => "torch.float32",
Self::Float64 => "torch.float64",
Self::Float16 => "torch.float16",
Self::BFloat16 => "torch.bfloat16",
Self::Int32 => "torch.int32",
Self::Int64 => "torch.int64",
Self::Int8 => "torch.int8",
Self::UInt8 => "torch.uint8",
Self::Bool => "torch.bool",
}
}
}
#[derive(Debug, Clone)]
pub struct PyTorchTensor {
pub data: Vec<u8>,
pub shape: Vec<usize>,
pub dtype: PyTorchDType,
pub requires_grad: bool,
pub device: String,
}
impl PyTorchTensor {
pub fn new(data: Vec<u8>, shape: Vec<usize>, dtype: PyTorchDType) -> Self {
Self {
data,
shape,
dtype,
requires_grad: false,
device: "cpu".to_string(),
}
}
pub fn with_grad(mut self) -> Self {
self.requires_grad = true;
self
}
pub fn to_device(mut self, device: &str) -> Self {
self.device = device.to_string();
self
}
pub fn numel(&self) -> usize {
self.shape.iter().product()
}
pub fn size_bytes(&self) -> usize {
self.numel() * self.dtype.size()
}
}
#[derive(Debug, Clone)]
pub struct PyTorchConfig {
pub default_device: String,
pub enable_cuda: bool,
pub memory_pool_size: usize,
pub use_pinned_memory: bool,
}
impl Default for PyTorchConfig {
fn default() -> Self {
Self {
default_device: "cpu".to_string(),
enable_cuda: true,
memory_pool_size: 1024 * 1024 * 1024, use_pinned_memory: false,
}
}
}
pub struct PyTorchBridge {
config: PyTorchConfig,
tensor_cache: HashMap<String, PyTorchTensor>,
}
impl PyTorchBridge {
pub fn new() -> Self {
Self {
config: PyTorchConfig::default(),
tensor_cache: HashMap::new(),
}
}
pub fn with_config(config: PyTorchConfig) -> Self {
Self {
config,
tensor_cache: HashMap::new(),
}
}
pub fn to_pytorch(
&self,
data: &[u8],
shape: &[usize],
dtype: PyTorchDType,
) -> Result<PyTorchTensor> {
let expected_size = shape.iter().product::<usize>() * dtype.size();
if data.len() != expected_size {
return Err(EcosystemError::DataConversion(format!(
"Data size {} doesn't match expected {}",
data.len(),
expected_size
)));
}
Ok(PyTorchTensor {
data: data.to_vec(),
shape: shape.to_vec(),
dtype,
requires_grad: false,
device: self.config.default_device.clone(),
})
}
pub fn from_pytorch(&self, tensor: &PyTorchTensor) -> Result<(Vec<u8>, Vec<usize>)> {
Ok((tensor.data.clone(), tensor.shape.clone()))
}
pub fn cache_tensor(&mut self, name: &str, tensor: PyTorchTensor) {
self.tensor_cache.insert(name.to_string(), tensor);
}
pub fn get_cached(&self, name: &str) -> Option<&PyTorchTensor> {
self.tensor_cache.get(name)
}
pub fn clear_cache(&mut self) {
self.tensor_cache.clear();
}
pub fn config(&self) -> &PyTorchConfig {
&self.config
}
pub fn export_metadata(&self, tensor: &PyTorchTensor) -> HashMap<String, String> {
let mut metadata = HashMap::new();
metadata.insert("dtype".to_string(), tensor.dtype.to_torch_str().to_string());
metadata.insert("shape".to_string(), format!("{:?}", tensor.shape));
metadata.insert("device".to_string(), tensor.device.clone());
metadata.insert(
"requires_grad".to_string(),
tensor.requires_grad.to_string(),
);
metadata.insert("numel".to_string(), tensor.numel().to_string());
metadata
}
}
impl Default for PyTorchBridge {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct OnnxInputSpec {
pub name: String,
pub shape: Vec<Option<usize>>,
pub dtype: OnnxDType,
}
#[derive(Debug, Clone)]
pub struct OnnxOutputSpec {
pub name: String,
pub shape: Vec<Option<usize>>,
pub dtype: OnnxDType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnnxDType {
Float,
Double,
Int32,
Int64,
Int8,
UInt8,
Float16,
String,
Bool,
}
#[derive(Debug, Clone)]
pub struct OnnxModelMetadata {
pub name: String,
pub version: u64,
pub producer: String,
pub inputs: Vec<OnnxInputSpec>,
pub outputs: Vec<OnnxOutputSpec>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct OnnxOutput {
pub name: String,
pub data: Vec<u8>,
pub shape: Vec<usize>,
pub dtype: OnnxDType,
}
#[derive(Debug, Clone)]
pub struct OnnxConfig {
pub execution_provider: OnnxExecutionProvider,
pub optimization_level: OnnxOptLevel,
pub intra_op_threads: usize,
pub inter_op_threads: usize,
pub enable_mem_pattern: bool,
pub enable_mem_arena: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnnxExecutionProvider {
Cpu,
Cuda,
TensorRT,
ROCm,
DirectML,
CoreML,
OpenVINO,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnnxOptLevel {
None,
Basic,
Extended,
All,
}
impl Default for OnnxConfig {
fn default() -> Self {
Self {
execution_provider: OnnxExecutionProvider::Cpu,
optimization_level: OnnxOptLevel::All,
intra_op_threads: 0, inter_op_threads: 0, enable_mem_pattern: true,
enable_mem_arena: true,
}
}
}
#[async_trait::async_trait]
pub trait OnnxRuntime: Send + Sync + 'static {
async fn load_model(&self, path: &Path) -> Result<String>;
async fn get_metadata(&self, model_id: &str) -> Result<OnnxModelMetadata>;
async fn run_inference(
&self,
model_id: &str,
inputs: HashMap<String, Vec<u8>>,
) -> Result<Vec<OnnxOutput>>;
async fn unload_model(&self, model_id: &str) -> Result<()>;
}
pub struct OnnxExecutor<R> {
runtime: Arc<R>,
config: OnnxConfig,
loaded_models: HashMap<String, OnnxModelMetadata>,
}
impl<R: OnnxRuntime> OnnxExecutor<R> {
pub fn new(runtime: Arc<R>) -> Self {
Self {
runtime,
config: OnnxConfig::default(),
loaded_models: HashMap::new(),
}
}
pub fn with_config(runtime: Arc<R>, config: OnnxConfig) -> Self {
Self {
runtime,
config,
loaded_models: HashMap::new(),
}
}
pub async fn load(&mut self, path: impl AsRef<Path>) -> Result<String> {
let model_id = self.runtime.load_model(path.as_ref()).await?;
let metadata = self.runtime.get_metadata(&model_id).await?;
self.loaded_models.insert(model_id.clone(), metadata);
Ok(model_id)
}
pub async fn run(
&self,
model_id: &str,
inputs: HashMap<String, Vec<u8>>,
) -> Result<Vec<OnnxOutput>> {
if !self.loaded_models.contains_key(model_id) {
return Err(EcosystemError::Configuration(format!(
"Model {} not loaded",
model_id
)));
}
self.runtime.run_inference(model_id, inputs).await
}
pub fn metadata(&self, model_id: &str) -> Option<&OnnxModelMetadata> {
self.loaded_models.get(model_id)
}
pub async fn unload(&mut self, model_id: &str) -> Result<()> {
self.runtime.unload_model(model_id).await?;
self.loaded_models.remove(model_id);
Ok(())
}
pub fn loaded_models(&self) -> Vec<&str> {
self.loaded_models.keys().map(|s| s.as_str()).collect()
}
pub fn config(&self) -> &OnnxConfig {
&self.config
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HuggingFaceTask {
TextClassification,
TokenClassification,
QuestionAnswering,
TextGeneration,
Summarization,
Translation,
FillMask,
SentenceSimilarity,
FeatureExtraction,
ImageClassification,
ObjectDetection,
ImageSegmentation,
ZeroShotClassification,
Conversational,
}
impl HuggingFaceTask {
pub fn task_name(&self) -> &'static str {
match self {
Self::TextClassification => "text-classification",
Self::TokenClassification => "token-classification",
Self::QuestionAnswering => "question-answering",
Self::TextGeneration => "text-generation",
Self::Summarization => "summarization",
Self::Translation => "translation",
Self::FillMask => "fill-mask",
Self::SentenceSimilarity => "sentence-similarity",
Self::FeatureExtraction => "feature-extraction",
Self::ImageClassification => "image-classification",
Self::ObjectDetection => "object-detection",
Self::ImageSegmentation => "image-segmentation",
Self::ZeroShotClassification => "zero-shot-classification",
Self::Conversational => "conversational",
}
}
}
#[derive(Debug, Clone)]
pub struct HuggingFaceModel {
pub model_id: String,
pub revision: Option<String>,
pub task: HuggingFaceTask,
pub use_gpu: bool,
pub config: HashMap<String, String>,
}
impl HuggingFaceModel {
pub fn new(model_id: &str, task: HuggingFaceTask) -> Self {
Self {
model_id: model_id.to_string(),
revision: None,
task,
use_gpu: true,
config: HashMap::new(),
}
}
pub fn revision(mut self, rev: &str) -> Self {
self.revision = Some(rev.to_string());
self
}
pub fn cpu_only(mut self) -> Self {
self.use_gpu = false;
self
}
pub fn with_config(mut self, key: &str, value: &str) -> Self {
self.config.insert(key.to_string(), value.to_string());
self
}
}
#[derive(Debug, Clone)]
pub struct HuggingFaceConfig {
pub cache_dir: Option<PathBuf>,
pub max_length: usize,
pub batch_size: usize,
pub timeout: Duration,
pub use_fp16: bool,
pub quantization: Option<HuggingFaceQuantization>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HuggingFaceQuantization {
Int8,
Int4,
Fp8,
Gptq,
Awq,
}
impl Default for HuggingFaceConfig {
fn default() -> Self {
Self {
cache_dir: None,
max_length: 512,
batch_size: 1,
timeout: Duration::from_secs(60),
use_fp16: true,
quantization: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TextClassificationResult {
pub label: String,
pub score: f32,
}
#[derive(Debug, Clone)]
pub struct TokenClassificationResult {
pub word: String,
pub entity: String,
pub score: f32,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone)]
pub struct QuestionAnsweringResult {
pub answer: String,
pub score: f32,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone)]
pub struct TextGenerationResult {
pub generated_text: String,
}
#[derive(Debug, Clone)]
pub struct FeatureExtractionResult {
pub embeddings: Vec<f32>,
pub shape: Vec<usize>,
}
#[async_trait::async_trait]
pub trait HuggingFaceRuntime: Send + Sync + 'static {
async fn load_model(&self, model: &HuggingFaceModel) -> Result<String>;
async fn text_classification(
&self,
model_id: &str,
texts: &[&str],
) -> Result<Vec<TextClassificationResult>>;
async fn token_classification(
&self,
model_id: &str,
text: &str,
) -> Result<Vec<TokenClassificationResult>>;
async fn question_answering(
&self,
model_id: &str,
question: &str,
context: &str,
) -> Result<QuestionAnsweringResult>;
async fn text_generation(
&self,
model_id: &str,
prompt: &str,
max_tokens: usize,
) -> Result<TextGenerationResult>;
async fn feature_extraction(
&self,
model_id: &str,
texts: &[&str],
) -> Result<Vec<FeatureExtractionResult>>;
async fn unload_model(&self, model_id: &str) -> Result<()>;
}
pub struct HuggingFacePipeline<R> {
runtime: Arc<R>,
config: HuggingFaceConfig,
model: HuggingFaceModel,
model_handle: Option<String>,
}
impl<R: HuggingFaceRuntime> HuggingFacePipeline<R> {
pub fn new(runtime: Arc<R>, model: HuggingFaceModel) -> Self {
Self {
runtime,
config: HuggingFaceConfig::default(),
model,
model_handle: None,
}
}
pub fn with_config(mut self, config: HuggingFaceConfig) -> Self {
self.config = config;
self
}
pub fn text_classification(runtime: Arc<R>, model_id: &str) -> Self {
Self::new(
runtime,
HuggingFaceModel::new(model_id, HuggingFaceTask::TextClassification),
)
}
pub fn text_generation(runtime: Arc<R>, model_id: &str) -> Self {
Self::new(
runtime,
HuggingFaceModel::new(model_id, HuggingFaceTask::TextGeneration),
)
}
pub fn feature_extraction(runtime: Arc<R>, model_id: &str) -> Self {
Self::new(
runtime,
HuggingFaceModel::new(model_id, HuggingFaceTask::FeatureExtraction),
)
}
pub fn question_answering(runtime: Arc<R>, model_id: &str) -> Self {
Self::new(
runtime,
HuggingFaceModel::new(model_id, HuggingFaceTask::QuestionAnswering),
)
}
pub async fn load(&mut self) -> Result<()> {
let handle = self.runtime.load_model(&self.model).await?;
self.model_handle = Some(handle);
Ok(())
}
async fn ensure_loaded(&mut self) -> Result<&str> {
if self.model_handle.is_none() {
self.load().await?;
}
Ok(self
.model_handle
.as_ref()
.expect("model_handle is Some after load()"))
}
pub async fn classify(&mut self, texts: &[&str]) -> Result<Vec<TextClassificationResult>> {
let handle = self.ensure_loaded().await?.to_string();
self.runtime.text_classification(&handle, texts).await
}
pub async fn generate(
&mut self,
prompt: &str,
max_tokens: usize,
) -> Result<TextGenerationResult> {
let handle = self.ensure_loaded().await?.to_string();
self.runtime
.text_generation(&handle, prompt, max_tokens)
.await
}
pub async fn extract_features(
&mut self,
texts: &[&str],
) -> Result<Vec<FeatureExtractionResult>> {
let handle = self.ensure_loaded().await?.to_string();
self.runtime.feature_extraction(&handle, texts).await
}
pub async fn answer(
&mut self,
question: &str,
context: &str,
) -> Result<QuestionAnsweringResult> {
let handle = self.ensure_loaded().await?.to_string();
self.runtime
.question_answering(&handle, question, context)
.await
}
pub fn model(&self) -> &HuggingFaceModel {
&self.model
}
pub fn config(&self) -> &HuggingFaceConfig {
&self.config
}
pub async fn unload(&mut self) -> Result<()> {
if let Some(handle) = self.model_handle.take() {
self.runtime.unload_model(&handle).await?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TokenizerConfig {
pub vocab_size: usize,
pub max_length: usize,
pub pad_token_id: u32,
pub bos_token_id: u32,
pub eos_token_id: u32,
pub unk_token_id: u32,
pub add_special_tokens: bool,
}
impl Default for TokenizerConfig {
fn default() -> Self {
Self {
vocab_size: 30522, max_length: 512,
pad_token_id: 0,
bos_token_id: 101, eos_token_id: 102, unk_token_id: 100, add_special_tokens: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ModelLoadStats {
pub load_time_ms: u64,
pub model_size_bytes: u64,
pub num_parameters: u64,
pub device_memory_bytes: u64,
}
#[derive(Debug, Clone, Default)]
pub struct InferenceStats {
pub total_time_ms: u64,
pub tokens_processed: u64,
pub throughput: f64,
pub latency_per_token_ms: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pytorch_dtype_size() {
assert_eq!(PyTorchDType::Float32.size(), 4);
assert_eq!(PyTorchDType::Float64.size(), 8);
assert_eq!(PyTorchDType::Float16.size(), 2);
assert_eq!(PyTorchDType::Int8.size(), 1);
}
#[test]
fn test_pytorch_tensor() {
let tensor = PyTorchTensor::new(vec![0; 16], vec![2, 2], PyTorchDType::Float32);
assert_eq!(tensor.numel(), 4);
assert_eq!(tensor.size_bytes(), 16);
}
#[test]
fn test_pytorch_bridge() {
let bridge = PyTorchBridge::new();
let data = vec![0u8; 16];
let tensor = bridge
.to_pytorch(&data, &[2, 2], PyTorchDType::Float32)
.unwrap();
assert_eq!(tensor.shape, vec![2, 2]);
}
#[test]
fn test_huggingface_model() {
let model = HuggingFaceModel::new("bert-base-uncased", HuggingFaceTask::TextClassification)
.revision("main")
.cpu_only();
assert_eq!(model.model_id, "bert-base-uncased");
assert!(!model.use_gpu);
}
#[test]
fn test_huggingface_task() {
assert_eq!(
HuggingFaceTask::TextClassification.task_name(),
"text-classification"
);
assert_eq!(
HuggingFaceTask::TextGeneration.task_name(),
"text-generation"
);
}
#[test]
fn test_onnx_config_default() {
let config = OnnxConfig::default();
assert_eq!(config.execution_provider, OnnxExecutionProvider::Cpu);
assert_eq!(config.optimization_level, OnnxOptLevel::All);
}
#[test]
fn test_tokenizer_config_default() {
let config = TokenizerConfig::default();
assert_eq!(config.vocab_size, 30522);
assert_eq!(config.max_length, 512);
}
}