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#[derive(Debug)]
13pub struct StreamingModelLoader {
14 client: Client,
15 chunk_size: usize,
16 temp_threshold: usize, }
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, temp_threshold: 512 * 1024 * 1024, }
29 }
30
31 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 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 async fn fetch_model_metadata(&self, url: &str) -> Result<ModelMetadata> {
61 if url.ends_with(".safetensors") {
63 return self.fetch_safetensors_metadata(url).await;
64 }
65
66 if url.contains("huggingface.co") || url.contains("hf:") {
68 return self.fetch_huggingface_metadata(url).await;
69 }
70
71 self.fetch_generic_metadata(url).await
73 }
74
75 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 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 let mut size_bytes = [0u8; 8];
93 response.chunk().await?;
94 progress_callback(0.2, "Parsing model structure...");
97
98 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 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 let tensor_data = self.extract_tensor_streaming(url, tensor_info).await?;
119
120 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 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 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 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 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 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 Err("PyTorch streaming compression not implemented".into())
180 }
181
182 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 Err("GGUF streaming compression not implemented".into())
192 }
193
194 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 let model_files = self.list_huggingface_files(url).await?;
205
206 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 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 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 async fn fetch_safetensors_metadata(&self, url: &str) -> Result<ModelMetadata> {
230 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 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, tensor_count: 0, })
250 }
251
252 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 async fn fetch_generic_metadata(&self, url: &str) -> Result<ModelMetadata> {
263 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 };
273
274 Ok(ModelMetadata {
275 format,
276 estimated_size: 0,
277 tensor_count: 0,
278 })
279 }
280
281 async fn list_huggingface_files(&self, url: &str) -> Result<Vec<String>> {
283 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}