Skip to main content

ohms_adaptq/
streaming_loader.rs

1use crate::{Result, WeightMatrix};
2use reqwest::Client;
3use std::io::{Read, Seek, SeekFrom, Cursor};
4use std::collections::HashMap;
5use serde_json::Value;
6use memmap2::MmapOptions;
7use std::fs::File;
8use tempfile::NamedTempFile;
9
10/// Streaming model loader that processes models directly from online sources
11/// without requiring full download to disk
12#[derive(Debug)]
13pub struct StreamingModelLoader {
14    client: Client,
15    chunk_size: usize,
16    temp_threshold: usize, // Only use temp files for models larger than this
17}
18
19impl StreamingModelLoader {
20    pub fn new() -> Self {
21        Self {
22            client: Client::builder()
23                .timeout(std::time::Duration::from_secs(300))
24                .build()
25                .expect("Failed to create HTTP client"),
26            chunk_size: 64 * 1024 * 1024, // 64MB chunks
27            temp_threshold: 512 * 1024 * 1024, // 512MB threshold
28        }
29    }
30
31    /// Load model weights with streaming compression
32    pub async fn load_and_compress_streaming(
33        &self, 
34        source_url: &str,
35        novaq_engine: &mut crate::NOVAQEngine,
36        progress_callback: impl Fn(f32, &str),
37    ) -> Result<crate::NOVAQModel> {
38        progress_callback(0.0, "Fetching model metadata...");
39        
40        // First, get model metadata without downloading the full model
41        let metadata = self.fetch_model_metadata(source_url).await?;
42        
43        match metadata.format {
44            ModelFormat::SafeTensors => {
45                self.stream_compress_safetensors(source_url, novaq_engine, progress_callback).await
46            }
47            ModelFormat::PyTorch => {
48                self.stream_compress_pytorch(source_url, novaq_engine, progress_callback).await
49            }
50            ModelFormat::GGUF => {
51                self.stream_compress_gguf(source_url, novaq_engine, progress_callback).await
52            }
53            ModelFormat::HuggingFace => {
54                self.stream_compress_huggingface(source_url, novaq_engine, progress_callback).await
55            }
56        }
57    }
58
59    /// Fetch only model metadata without downloading the full model
60    async fn fetch_model_metadata(&self, url: &str) -> Result<ModelMetadata> {
61        // For SafeTensors, we only need to read the header
62        if url.ends_with(".safetensors") {
63            return self.fetch_safetensors_metadata(url).await;
64        }
65        
66        // For HuggingFace repos, check config.json and model files
67        if url.contains("huggingface.co") || url.contains("hf:") {
68            return self.fetch_huggingface_metadata(url).await;
69        }
70        
71        // For other formats, we might need to download a small portion
72        self.fetch_generic_metadata(url).await
73    }
74
75    /// Stream compress SafeTensors format
76    async fn stream_compress_safetensors(
77        &self,
78        url: &str,
79        novaq_engine: &mut crate::NOVAQEngine,
80        progress_callback: impl Fn(f32, &str),
81    ) -> Result<crate::NOVAQModel> {
82        progress_callback(0.1, "Reading SafeTensors header...");
83        
84        // Read just the header first (first 8 bytes + header size)
85        let mut response = self.client.get(url).send().await?;
86        let total_size = response.content_length().unwrap_or(0);
87        
88        let mut header_buffer = Vec::new();
89        let mut bytes_read = 0;
90        
91        // Read header size (first 8 bytes)
92        let mut size_bytes = [0u8; 8];
93        response.chunk().await?;
94        // Read the actual header size and parse it
95        
96        progress_callback(0.2, "Parsing model structure...");
97        
98        // Parse SafeTensors header to get tensor information
99        let header: HashMap<String, Value> = serde_json::from_slice(&header_buffer)?;
100        
101        let mut weight_matrices = Vec::new();
102        let mut tensor_count = 0;
103        let total_tensors = header.len();
104        
105        progress_callback(0.3, "Starting streaming compression...");
106        
107        // Process each tensor in chunks without storing the full model
108        for (tensor_name, tensor_info) in header.iter() {
109            if tensor_name == "__metadata__" {
110                continue;
111            }
112            
113            tensor_count += 1;
114            let progress = 0.3 + (tensor_count as f32 / total_tensors as f32) * 0.6;
115            progress_callback(progress, &format!("Compressing tensor: {}", tensor_name));
116            
117            // Extract tensor data in chunks
118            let tensor_data = self.extract_tensor_streaming(url, tensor_info).await?;
119            
120            // Create weight matrix
121            let shape = tensor_info["shape"].as_array()
122                .ok_or("Invalid tensor shape")?
123                .iter()
124                .map(|v| v.as_u64().unwrap() as usize)
125                .collect();
126            
127            let weight_matrix = WeightMatrix::new(tensor_data, shape, tensor_name.clone());
128            weight_matrices.push(weight_matrix);
129        }
130        
131        progress_callback(0.9, "Finalizing NOVAQ compression...");
132        
133        // Process all weight matrices with NOVAQ
134        let novaq_model = novaq_engine.quantize_model(weight_matrices)?;
135        
136        progress_callback(1.0, "Streaming compression complete!");
137        
138        Ok(novaq_model)
139    }
140
141    /// Extract tensor data in streaming fashion
142    async fn extract_tensor_streaming(&self, url: &str, tensor_info: &Value) -> Result<Vec<f32>> {
143        let data_offsets = tensor_info["data_offsets"].as_array()
144            .ok_or("Invalid tensor data offsets")?;
145        let start_offset = data_offsets[0].as_u64().unwrap();
146        let end_offset = data_offsets[1].as_u64().unwrap();
147        let tensor_size = (end_offset - start_offset) as usize;
148        
149        // Use Range request to get only the tensor data we need
150        let response = self.client
151            .get(url)
152            .header("Range", format!("bytes={}-{}", start_offset, end_offset - 1))
153            .send()
154            .await?;
155        
156        let tensor_bytes = response.bytes().await?;
157        
158        // Convert bytes to f32 values (assuming fp32)
159        let float_count = tensor_size / 4;
160        let mut tensor_data = Vec::with_capacity(float_count);
161        
162        for chunk in tensor_bytes.chunks_exact(4) {
163            let float_val = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
164            tensor_data.push(float_val);
165        }
166        
167        Ok(tensor_data)
168    }
169
170    /// Stream compress PyTorch format
171    async fn stream_compress_pytorch(
172        &self,
173        url: &str,
174        novaq_engine: &mut crate::NOVAQEngine,
175        progress_callback: impl Fn(f32, &str),
176    ) -> Result<crate::NOVAQModel> {
177        progress_callback(0.0, "PyTorch streaming not yet implemented");
178        // TODO: Implement PyTorch streaming compression
179        Err("PyTorch streaming compression not implemented".into())
180    }
181
182    /// Stream compress GGUF format  
183    async fn stream_compress_gguf(
184        &self,
185        url: &str,
186        novaq_engine: &mut crate::NOVAQEngine,
187        progress_callback: impl Fn(f32, &str),
188    ) -> Result<crate::NOVAQModel> {
189        progress_callback(0.0, "GGUF streaming not yet implemented");
190        // TODO: Implement GGUF streaming compression
191        Err("GGUF streaming compression not implemented".into())
192    }
193
194    /// Stream compress HuggingFace repository
195    async fn stream_compress_huggingface(
196        &self,
197        url: &str,
198        novaq_engine: &mut crate::NOVAQEngine,
199        progress_callback: impl Fn(f32, &str),
200    ) -> Result<crate::NOVAQModel> {
201        progress_callback(0.0, "Detecting HuggingFace model files...");
202        
203        // Find the main model file in the HF repo
204        let model_files = self.list_huggingface_files(url).await?;
205        
206        // Prioritize SafeTensors, then PyTorch
207        let model_file = model_files.iter()
208            .find(|f| f.ends_with(".safetensors"))
209            .or_else(|| model_files.iter().find(|f| f.ends_with(".bin")))
210            .ok_or("No supported model files found in HuggingFace repo")?;
211        
212        let full_url = if url.starts_with("hf:") {
213            // Convert hf:repo/model to actual HF URL
214            let repo = url.strip_prefix("hf:").unwrap();
215            format!("https://huggingface.co/{}/resolve/main/{}", repo, model_file)
216        } else {
217            format!("{}/resolve/main/{}", url, model_file)
218        };
219        
220        // Stream compress the actual model file
221        if model_file.ends_with(".safetensors") {
222            self.stream_compress_safetensors(&full_url, novaq_engine, progress_callback).await
223        } else {
224            self.stream_compress_pytorch(&full_url, novaq_engine, progress_callback).await
225        }
226    }
227
228    /// Fetch SafeTensors metadata without downloading full file
229    async fn fetch_safetensors_metadata(&self, url: &str) -> Result<ModelMetadata> {
230        // Read just the first 1KB to get header size
231        let response = self.client
232            .get(url)
233            .header("Range", "bytes=0-1023")
234            .send()
235            .await?;
236        
237        let initial_bytes = response.bytes().await?;
238        
239        // Parse header size from first 8 bytes
240        let header_size = u64::from_le_bytes([
241            initial_bytes[0], initial_bytes[1], initial_bytes[2], initial_bytes[3],
242            initial_bytes[4], initial_bytes[5], initial_bytes[6], initial_bytes[7],
243        ]) as usize;
244        
245        Ok(ModelMetadata {
246            format: ModelFormat::SafeTensors,
247            estimated_size: 0, // Will be determined during streaming
248            tensor_count: 0,   // Will be determined from header
249        })
250    }
251
252    /// Fetch HuggingFace metadata
253    async fn fetch_huggingface_metadata(&self, url: &str) -> Result<ModelMetadata> {
254        Ok(ModelMetadata {
255            format: ModelFormat::HuggingFace,
256            estimated_size: 0,
257            tensor_count: 0,
258        })
259    }
260
261    /// Fetch generic metadata for unknown formats
262    async fn fetch_generic_metadata(&self, url: &str) -> Result<ModelMetadata> {
263        // Try to determine format from URL extension
264        let format = if url.ends_with(".safetensors") {
265            ModelFormat::SafeTensors
266        } else if url.ends_with(".bin") || url.ends_with(".pt") || url.ends_with(".pth") {
267            ModelFormat::PyTorch
268        } else if url.ends_with(".gguf") {
269            ModelFormat::GGUF
270        } else {
271            ModelFormat::SafeTensors // Default assumption
272        };
273        
274        Ok(ModelMetadata {
275            format,
276            estimated_size: 0,
277            tensor_count: 0,
278        })
279    }
280
281    /// List files in HuggingFace repository
282    async fn list_huggingface_files(&self, url: &str) -> Result<Vec<String>> {
283        // Simple implementation - in practice, would use HF API
284        Ok(vec!["model.safetensors".to_string(), "pytorch_model.bin".to_string()])
285    }
286}
287
288#[derive(Debug)]
289pub struct ModelMetadata {
290    pub format: ModelFormat,
291    pub estimated_size: u64,
292    pub tensor_count: usize,
293}
294
295#[derive(Debug)]
296pub enum ModelFormat {
297    SafeTensors,
298    PyTorch,
299    GGUF,
300    HuggingFace,
301}
302
303impl Default for StreamingModelLoader {
304    fn default() -> Self {
305        Self::new()
306    }
307}