1use crate::gpu::GpuBackend;
20use crate::weights::{
21 WeightLoader, WeightStore, WeightTensor, check_oom_guard, estimate_has_fp8,
22 estimate_load_bytes, evict_page_cache, f16_to_bf16_bytes,
23};
24use anyhow::{Context, Result, bail};
25use std::collections::HashMap;
26use std::fs::File;
27use std::path::{Path, PathBuf};
28use std::sync::mpsc::sync_channel;
29
30mod direct_io;
31mod header;
32
33use header::{parse_header, resolve_shards};
34
35pub struct FastSafetensorsLoader {
38 pub ep_rank: usize,
39 pub ep_world_size: usize,
40 pub num_experts: usize,
41 pub peak_memory_multiplier: Option<f64>,
42 pub skip_activation_scales: bool,
53 pub skip_mtp: bool,
63 pub try_direct_io: bool,
66 pub direct_io_tensor_cap: usize,
76 pub prefetch_shards: bool,
80 pub skip_vision: bool,
89 pub defer: Option<crate::weights::DeferHook>,
97}
98
99pub fn is_vision_tensor(name: &str) -> bool {
104 name.starts_with("model.visual.")
105 || name.starts_with("model.vision")
106 || name.starts_with("visual.")
107}
108
109pub const DEFAULT_DIRECT_IO_TENSOR_CAP: usize = 5000;
113
114impl Default for FastSafetensorsLoader {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120#[path = "skip.rs"]
121mod skip;
122
123impl FastSafetensorsLoader {
124 pub fn new() -> Self {
125 Self {
126 ep_rank: 0,
127 ep_world_size: 1,
128 num_experts: 0,
129 peak_memory_multiplier: None,
130 skip_activation_scales: false,
131 skip_mtp: false,
132 try_direct_io: true,
133 direct_io_tensor_cap: DEFAULT_DIRECT_IO_TENSOR_CAP,
134 prefetch_shards: false,
135 skip_vision: false,
136 defer: None,
137 }
138 }
139
140 pub fn with_ep(ep_rank: usize, ep_world_size: usize, num_experts: usize) -> Self {
141 Self {
142 ep_rank,
143 ep_world_size,
144 num_experts,
145 peak_memory_multiplier: None,
146 skip_activation_scales: false,
147 skip_mtp: false,
148 try_direct_io: true,
149 direct_io_tensor_cap: DEFAULT_DIRECT_IO_TENSOR_CAP,
150 prefetch_shards: false,
151 skip_vision: false,
152 defer: None,
153 }
154 }
155}
156
157impl WeightLoader for FastSafetensorsLoader {
158 fn load(
159 &self,
160 model_dir: &Path,
161 gpu: &dyn GpuBackend,
162 oom_reserve_bytes: usize,
163 ) -> Result<WeightStore> {
164 let skip_fn = |name: &str| self.should_skip_tensor(name);
165 let defer_fn =
166 |name: &str, dtype: crate::weights::WeightDtype| self.is_deferred(name, dtype);
167
168 let (shard_files, tensor_to_shard): (Vec<PathBuf>, Option<HashMap<String, String>>) =
170 resolve_shards(model_dir)?;
171
172 let preflight_skip = |name: &str| skip_fn(name) || crate::weights::is_ngram_table(name);
186 {
187 let estimated = estimate_load_bytes(&shard_files, &preflight_skip, &defer_fn)?;
188 let has_fp8 = estimate_has_fp8(&shard_files, &preflight_skip, &defer_fn)?;
189 let mult = self
190 .peak_memory_multiplier
191 .unwrap_or(if has_fp8 { 1.5 } else { 1.3 });
192 let peak = (estimated as f64 * mult) as usize;
193 let free = gpu.free_memory()?;
194 let gib = |b: usize| b as f64 / (1024.0 * 1024.0 * 1024.0);
195 tracing::info!(
196 "Fast-load pre-flight: {:.2} GB on-disk, {:.1}x overhead = {:.2} GB peak, \
197 {:.2} GB free, {:.1} GB reserve (FP8: {})",
198 gib(estimated),
199 mult,
200 gib(peak),
201 gib(free),
202 gib(oom_reserve_bytes),
203 has_fp8,
204 );
205 crate::progress::preflight(gib(estimated), gib(free));
206 if peak + oom_reserve_bytes > free {
207 bail!(
208 "OOM pre-flight: peak {:.2} GB + {:.2} GB reserve exceeds {:.2} GB free. \
209 Use a smaller quantization or add more GPUs for EP.",
210 gib(peak),
211 gib(oom_reserve_bytes),
212 gib(free),
213 );
214 }
215 }
216
217 let mut weights: HashMap<String, WeightTensor> = HashMap::new();
219 let mut deferred: HashMap<String, crate::weights::DeferredTensor> = HashMap::new();
221 let total_shards = shard_files.len();
222 let initial_free = gpu.free_memory()?;
223 let mut offload_logged = false;
224
225 for (i, shard_path) in shard_files.iter().enumerate() {
226 let shard_name = shard_path
229 .file_name()
230 .and_then(|n| n.to_str())
231 .unwrap_or_default();
232 let tensor_filter: Option<Vec<String>> = tensor_to_shard.as_ref().map(|map| {
233 map.iter()
234 .filter(|(_, s)| *s == shard_name)
235 .map(|(t, _)| t.clone())
236 .collect()
237 });
238
239 tracing::info!(
240 "Fast-loading shard {}/{}: {}{}",
241 i + 1,
242 total_shards,
243 shard_name,
244 tensor_filter
245 .as_ref()
246 .map(|v| format!(" ({} tensors)", v.len()))
247 .unwrap_or_default(),
248 );
249 crate::progress::shard_start(i + 1, total_shards, shard_name);
250
251 load_shard_fast(
252 shard_path,
253 tensor_filter.as_deref(),
254 gpu,
255 &skip_fn,
256 &defer_fn,
257 self.try_direct_io,
258 self.direct_io_tensor_cap,
259 self.prefetch_shards,
260 &mut weights,
261 &mut deferred,
262 &mut offload_logged,
263 )?;
264
265 let free_now = gpu.free_memory().unwrap_or(0);
266 let used = initial_free.saturating_sub(free_now);
267 tracing::info!(
268 " Shard {}/{} done — GPU memory: {:.2} GB used, {:.2} GB free",
269 i + 1,
270 total_shards,
271 used as f64 / (1024.0 * 1024.0 * 1024.0),
272 free_now as f64 / (1024.0 * 1024.0 * 1024.0),
273 );
274 crate::progress::shard_done(
275 i + 1,
276 total_shards,
277 used as f64 / (1024.0 * 1024.0 * 1024.0),
278 free_now as f64 / (1024.0 * 1024.0 * 1024.0),
279 );
280 if !offload_logged {
281 check_oom_guard(
282 gpu,
283 oom_reserve_bytes,
284 &format!("fast weight loading (shard {}/{})", i + 1, total_shards),
285 )?;
286 }
287 }
288
289 let no_skip = |_: &str| false;
291 let no_defer = |_: &str, _: crate::weights::WeightDtype| false;
292 let extra = model_dir.join("extra_weights.safetensors");
293 if extra.exists() {
294 tracing::info!("Fast-loading extra_weights.safetensors");
295 let mut extra_offload = false;
296 load_shard_fast(
297 &extra,
298 None,
299 gpu,
300 &no_skip,
301 &no_defer,
302 self.try_direct_io,
303 self.direct_io_tensor_cap,
304 self.prefetch_shards,
305 &mut weights,
306 &mut deferred,
307 &mut extra_offload,
308 )?;
309 }
310
311 tracing::info!("Fast-loaded {} weight tensors", weights.len());
312 let mut store = WeightStore::from_map(weights);
313 for (name, d) in deferred {
314 store.defer(name, d);
315 }
316 Ok(store)
317 }
318}
319
320#[allow(clippy::too_many_arguments)]
330fn load_shard_fast(
331 shard_path: &Path,
332 tensor_filter: Option<&[String]>,
333 gpu: &dyn GpuBackend,
334 skip_fn: &dyn Fn(&str) -> bool,
335 defer_fn: &dyn Fn(&str, crate::weights::WeightDtype) -> bool,
336 try_direct_io: bool,
337 direct_io_tensor_cap: usize,
338 prefetch_shards: bool,
339 out: &mut HashMap<String, WeightTensor>,
340 deferred_out: &mut HashMap<String, crate::weights::DeferredTensor>,
341 offload_logged: &mut bool,
342) -> Result<()> {
343 let mut meta_file = File::open(shard_path)
346 .with_context(|| format!("Failed to open {}", shard_path.display()))?;
347 let mut tensors = parse_header(&mut meta_file)?;
348 let file_size = meta_file.metadata()?.len();
349
350 if let Some(allow) = tensor_filter {
352 let allow_set: std::collections::HashSet<&str> = allow.iter().map(|s| s.as_str()).collect();
353 tensors.retain(|t| allow_set.contains(t.name.as_str()));
354 }
355 let mut deferred_here: Vec<(String, crate::weights::DeferredTensor)> = Vec::new();
369 let mut ngram_count = 0usize;
370 #[allow(clippy::items_after_statements)]
371 tensors.retain(|t| {
372 let mut defer = |t: &header::TensorMeta| {
373 deferred_here.push((
374 t.name.clone(),
375 crate::weights::DeferredTensor {
376 path: shard_path.to_path_buf(),
377 offset: t.abs_offset,
378 shape: t.shape.clone(),
379 dtype: t.dtype,
380 },
381 ));
382 };
383 if crate::weights::is_ngram_table(&t.name) {
384 defer(t);
385 ngram_count += 1;
386 return false;
387 }
388 if skip_fn(&t.name) {
392 return false;
393 }
394 if !t.from_f16 && defer_fn(&t.name, t.dtype) {
398 defer(t);
399 return false;
400 }
401 true
402 });
403 if !deferred_here.is_empty() {
404 tracing::info!(
405 "Deferred {} tensor(s) in {} ({} n-gram table(s), {} claimed by the model's \
406 weight loader) — served from disk, not uploaded",
407 deferred_here.len(),
408 shard_path.display(),
409 ngram_count,
410 deferred_here.len() - ngram_count,
411 );
412 deferred_out.extend(deferred_here);
413 }
414
415 let wants_direct = try_direct_io && tensors.len() <= direct_io_tensor_cap;
420 if try_direct_io && !wants_direct {
421 tracing::info!(
422 " Shard has {} tensors (> {} cap) — using buffered+pipelined path",
423 tensors.len(),
424 direct_io_tensor_cap
425 );
426 }
427
428 let (direct_file, using_direct) = match wants_direct
430 .then(|| direct_io::open_direct(shard_path))
431 .transpose()
432 {
433 Ok(Some(f)) => (Some(f), true),
434 Ok(None) => (None, false),
435 Err(e) => {
436 tracing::warn!(
437 "O_DIRECT open failed for {} ({e}); falling back to buffered reads",
438 shard_path.display()
439 );
440 (None, false)
441 }
442 };
443 let buffered_file = File::open(shard_path)?;
444 let data_fd = direct_file.as_ref().unwrap_or(&buffered_file);
445 if prefetch_shards && !using_direct {
446 advise_prefetch_shard(&buffered_file, shard_path, file_size);
447 }
448
449 type ReadMsg = (usize, direct_io::AlignedBuffer, usize);
451 let (tx, rx) = sync_channel::<Result<ReadMsg>>(1);
452 let tensors_for_reader: Vec<(u64, usize)> =
453 tensors.iter().map(|t| (t.abs_offset, t.len)).collect();
454 let raw_fd = {
455 use std::os::unix::io::AsRawFd;
456 data_fd.as_raw_fd()
457 };
458
459 let _ = file_size; let reader_handle = std::thread::spawn(move || {
461 for (idx, (abs_offset, len)) in tensors_for_reader.iter().enumerate() {
462 let msg = direct_io::read_tensor_aligned(raw_fd, *abs_offset, *len, using_direct)
463 .map(|(buf, slice_start)| (idx, buf, slice_start));
464 if tx.send(msg).is_err() {
465 break; }
467 }
468 });
469
470 for result in rx {
472 let (idx, buf, slice_start) = result?;
473 let meta = &tensors[idx];
474 let raw = &buf.as_slice()[slice_start..slice_start + meta.len];
475 let converted: Vec<u8>;
478 let src: &[u8] = if meta.from_f16 {
479 converted = f16_to_bf16_bytes(raw);
480 &converted
481 } else {
482 raw
483 };
484
485 let ptr = match gpu.alloc(meta.len) {
486 Ok(p) => {
487 gpu.copy_h2d(src, p)?;
488 p
489 }
490 Err(_) => {
491 if !*offload_logged {
492 tracing::warn!(
493 "GPU alloc failed for {} ({} bytes) — switching to managed (UVM) memory",
494 meta.name,
495 meta.len
496 );
497 *offload_logged = true;
498 }
499 let p = gpu.alloc_managed(meta.len)?;
500 unsafe {
501 std::ptr::copy_nonoverlapping(src.as_ptr(), p.0 as *mut u8, meta.len);
502 }
503 p
504 }
505 };
506
507 out.insert(
508 meta.name.clone(),
509 WeightTensor {
510 ptr,
511 shape: meta.shape.clone(),
512 dtype: meta.dtype,
513 },
514 );
515 }
516
517 reader_handle
518 .join()
519 .map_err(|_| anyhow::anyhow!("reader thread panicked"))?;
520
521 drop(direct_file);
525 evict_page_cache(&buffered_file);
526 drop(buffered_file);
527 Ok(())
528}
529
530#[cfg(target_os = "linux")]
531fn advise_prefetch_shard(file: &File, shard_path: &Path, file_size: u64) {
532 use std::os::unix::io::AsRawFd;
533
534 let fd = file.as_raw_fd();
535 let seq_rc = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL) };
536 let willneed_rc = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_WILLNEED) };
537 if seq_rc == 0 && willneed_rc == 0 {
538 tracing::info!(
539 " NFS/shard prefetch requested for {} ({:.2} GB)",
540 shard_path.display(),
541 file_size as f64 / (1024.0 * 1024.0 * 1024.0)
542 );
543 } else {
544 tracing::warn!(
545 " NFS/shard prefetch hint failed for {}: sequential_rc={}, willneed_rc={}",
546 shard_path.display(),
547 seq_rc,
548 willneed_rc
549 );
550 }
551}
552
553#[cfg(not(target_os = "linux"))]
554fn advise_prefetch_shard(_file: &File, _shard_path: &Path, _file_size: u64) {}
555
556#[cfg(test)]
557mod skip_vision_tests {
558 use super::{FastSafetensorsLoader, is_vision_tensor};
559
560 fn loader(skip_vision: bool, ep: usize) -> FastSafetensorsLoader {
561 let mut l = FastSafetensorsLoader::with_ep(0, ep, 288);
562 l.skip_vision = skip_vision;
563 l
564 }
565
566 #[test]
567 fn vision_names_are_recognised() {
568 assert!(is_vision_tensor("model.visual.blocks.0.attn.proj.weight"));
569 assert!(is_vision_tensor(
570 "model.vision_tower.encoder.layer.0.weight"
571 ));
572 assert!(is_vision_tensor("visual.merger.proj.weight"));
573 assert!(!is_vision_tensor(
574 "model.language_model.layers.45.eh_proj.weight"
575 ));
576 assert!(!is_vision_tensor(
578 "model.language_model.layers.3.mlp.revision.weight"
579 ));
580 }
581
582 #[test]
583 fn skip_vision_drops_only_the_tower() {
584 let l = loader(true, 2);
585 assert!(l.should_skip_tensor("model.visual.blocks.0.attn.proj.weight"));
586 assert!(!l.should_skip_tensor("model.language_model.layers.45.eh_proj.weight"));
587 assert!(!l.should_skip_tensor("lm_head.weight"));
588 }
589
590 #[test]
591 fn skip_vision_applies_without_ep() {
592 let l = loader(true, 1);
594 assert!(l.should_skip_tensor("model.visual.blocks.0.attn.proj.weight"));
595 assert!(!l.should_skip_tensor("model.layers.0.self_attn.q_proj.weight"));
596 }
597
598 #[test]
599 fn default_loader_keeps_the_tower() {
600 let l = loader(false, 2);
601 assert!(!l.should_skip_tensor("model.visual.blocks.0.attn.proj.weight"));
602 assert!(!FastSafetensorsLoader::new().skip_vision);
603 }
604
605 #[test]
606 fn ep_expert_filtering_is_unchanged_by_the_vision_rule() {
607 let l = loader(true, 2); assert!(
609 !l.should_skip_tensor("model.language_model.layers.4.mlp.experts.7.up_proj.weight")
610 );
611 assert!(
612 l.should_skip_tensor("model.language_model.layers.4.mlp.experts.200.up_proj.weight")
613 );
614 }
615}