1mod config;
26mod container;
27mod dequant_cpu;
28mod dequant_gpu;
29mod names;
30mod sidecar;
31mod value_transform;
32
33pub use config::config_from_gguf_dir;
34
35use anyhow::{Context, Result, bail};
36use std::collections::HashMap;
37use std::path::{Path, PathBuf};
38
39use super::{WeightDtype, WeightStore, WeightTensor, check_oom_guard, evict_page_cache};
40use crate::gpu::{DevicePtr, GpuBackend};
41
42pub fn find_gguf(dir: &Path) -> Option<PathBuf> {
49 let mut candidates: Vec<PathBuf> = std::fs::read_dir(dir)
50 .ok()?
51 .filter_map(|e| e.ok())
52 .map(|e| e.path())
53 .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("gguf"))
54 .collect();
55 candidates.sort();
56 candidates
57 .iter()
58 .find(|p| !sidecar::is_mmproj(p))
59 .cloned()
60 .or_else(|| candidates.into_iter().next())
61}
62
63fn native_q2_enabled() -> bool {
70 std::env::var("ATLAS_GGUF_NATIVE_Q2").ok().as_deref() == Some("1")
71}
72
73fn q2_group_usize() -> usize {
75 match std::env::var("ATLAS_GGUF_Q2_GROUP").ok().as_deref() {
76 Some("64") => 64,
77 _ => 128,
78 }
79}
80
81fn q2_group_variant(g: usize) -> container::Q2Group {
83 if g == 64 {
84 container::Q2Group::G64
85 } else {
86 container::Q2Group::G128
87 }
88}
89
90pub struct GgufLoader {
96 pub ep_rank: usize,
98 pub ep_world_size: usize,
100 pub num_experts: usize,
102 pub peak_memory_multiplier: Option<f64>,
104}
105
106impl Default for GgufLoader {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl GgufLoader {
113 pub fn new() -> Self {
115 Self {
116 ep_rank: 0,
117 ep_world_size: 1,
118 num_experts: 0,
119 peak_memory_multiplier: None,
120 }
121 }
122
123 pub fn with_ep(ep_rank: usize, ep_world_size: usize, num_experts: usize) -> Self {
125 Self {
126 ep_rank,
127 ep_world_size,
128 num_experts,
129 peak_memory_multiplier: None,
130 }
131 }
132
133 fn should_skip_expert(&self, idx: usize) -> bool {
135 if self.ep_world_size <= 1 || self.num_experts == 0 {
136 return false;
137 }
138 let per_rank = self.num_experts / self.ep_world_size;
139 let local_start = self.ep_rank * per_rank;
140 let local_end = if self.ep_rank == self.ep_world_size - 1 {
141 self.num_experts
142 } else {
143 local_start + per_rank
144 };
145 idx < local_start || idx >= local_end
146 }
147
148 fn emit_experts(
152 &self,
153 weights: &mut HashMap<String, WeightTensor>,
154 base_ptr: DevicePtr,
155 shape: &[usize],
156 layer: usize,
157 proj: &str,
158 skipped: &mut usize,
159 ) -> Result<()> {
160 let count = *shape
161 .first()
162 .context("stacked expert tensor has no leading expert dimension")?;
163 let per_elems: usize = shape[1..].iter().product();
164 let per_bytes = per_elems * WeightDtype::BF16.byte_size();
165 let expert_shape: Vec<usize> = shape[1..].to_vec();
166 for e in 0..count {
167 if self.should_skip_expert(e) {
168 *skipped += 1;
169 continue;
170 }
171 let ptr = base_ptr.offset(e * per_bytes);
172 let name = names::expert_name(layer, proj, e);
173 weights.insert(
174 name,
175 WeightTensor {
176 ptr,
177 shape: expert_shape.clone(),
178 dtype: WeightDtype::BF16,
179 },
180 );
181 }
182 Ok(())
183 }
184}
185
186fn dequant_to_device(
190 gpu: &dyn GpuBackend,
191 id: u32,
192 raw: &[u8],
193 num_elements: usize,
194 q2_group: usize,
195 force_cpu: bool,
196) -> Result<DevicePtr> {
197 if !force_cpu && dequant_gpu::supports(id) {
198 let q_ptr = gpu.alloc(raw.len())?;
199 gpu.copy_h2d(raw, q_ptr)?;
200 let bf16_ptr = dequant_gpu::to_bf16(gpu, id, q_ptr, num_elements, q2_group)
201 .with_context(|| format!("GPU dequant failed for ggml type {id}"))?;
202 gpu.free(q_ptr)?;
203 Ok(bf16_ptr)
204 } else if dequant_cpu::supports(id) {
205 let host = dequant_cpu::to_bf16_bytes(id, q2_group, raw, num_elements)
206 .with_context(|| format!("CPU dequant failed for ggml type {id}"))?;
207 debug_assert_eq!(host.len(), num_elements * WeightDtype::BF16.byte_size());
208 let bf16_ptr = gpu.alloc(host.len())?;
209 gpu.copy_h2d(&host, bf16_ptr)?;
210 Ok(bf16_ptr)
211 } else {
212 bail!("No GPU or CPU dequant available for ggml type {id}");
213 }
214}
215
216fn preflight_oom(
219 gpu: &dyn GpuBackend,
220 est_bf16_bytes: usize,
221 reserve_bytes: usize,
222 multiplier: Option<f64>,
223) -> Result<()> {
224 let overhead = multiplier.unwrap_or(1.1);
227 let peak = (est_bf16_bytes as f64 * overhead) as usize;
228 let free = gpu.free_memory()?;
229 let gb = |b: usize| b as f64 / (1024.0 * 1024.0 * 1024.0);
230 tracing::info!(
231 "GGUF pre-flight: ~{:.2} GB BF16 after dequant, {:.1}x overhead = {:.2} GB peak, \
232 {:.2} GB free, {:.1} GB reserve",
233 gb(est_bf16_bytes),
234 overhead,
235 gb(peak),
236 gb(free),
237 gb(reserve_bytes),
238 );
239 if peak + reserve_bytes > free {
240 bail!(
241 "Pre-flight OOM: GGUF dequant to BF16 needs ~{:.2} GB peak + {:.1} GB reserve, \
242 only {:.2} GB free. Use a smaller model or lower --oom-guard-mb.",
243 gb(peak),
244 gb(reserve_bytes),
245 gb(free),
246 );
247 }
248 Ok(())
249}
250
251impl super::WeightLoader for GgufLoader {
252 fn load(
253 &self,
254 model_dir: &Path,
255 gpu: &dyn GpuBackend,
256 oom_reserve_bytes: usize,
257 ) -> Result<WeightStore> {
258 let path = find_gguf(model_dir)
259 .with_context(|| format!("No .gguf file found in {}", model_dir.display()))?;
260 tracing::info!("Loading GGUF weights from {}", path.display());
261
262 let force_cpu = std::env::var("ATLAS_GGUF_FORCE_CPU").ok().as_deref() == Some("1");
263 let native_q2 = native_q2_enabled();
264 let q2_group = q2_group_usize();
265 let q2_variant = q2_group_variant(q2_group);
266 if native_q2 {
267 tracing::info!(
268 "ATLAS_GGUF_NATIVE_Q2=1: keeping id-42 FFN projections packed (group {q2_group})"
269 );
270 }
271
272 let (bb_file, bb_mmap, bb_gguf) = sidecar::open_gguf(&path)?;
274 let arch = bb_gguf
275 .get_str("general.architecture")
276 .unwrap_or("llama")
277 .to_lowercase();
278
279 let is_qwen35 = value_transform::is_qwen35(&arch);
285 let gdn = if is_qwen35 {
286 value_transform::gdn_dims(&bb_gguf, &arch)
287 } else {
288 None
289 };
290 if is_qwen35 && gdn.is_none() {
291 bail!(
292 "GGUF arch '{arch}' is qwen35-family but the SSM metadata keys \
293 ({arch}.ssm.*) are missing; cannot apply GDN value transforms"
294 );
295 }
296
297 let mmproj_path = sidecar::find_mmproj(model_dir, &path);
301 let mmproj = match &mmproj_path {
302 Some(mp) => {
303 tracing::info!("Found mmproj vision sidecar {}", mp.display());
304 Some(sidecar::open_gguf(mp)?)
305 }
306 None => None,
307 };
308 let mmproj_arch = mmproj.as_ref().map(|(_, _, g)| {
309 g.get_str("general.architecture")
310 .unwrap_or("clip")
311 .to_lowercase()
312 });
313
314 let mut est = sidecar::est_bf16(&bb_gguf, &arch);
316 if let (Some((_, _, mm_gguf)), Some(mm_arch)) = (mmproj.as_ref(), mmproj_arch.as_ref()) {
317 est += sidecar::est_bf16(mm_gguf, mm_arch);
318 }
319 preflight_oom(gpu, est, oom_reserve_bytes, self.peak_memory_multiplier)?;
320
321 let mut weights: HashMap<String, WeightTensor> = HashMap::new();
322 let mut skipped = 0usize;
323
324 sidecar::load_pass(
326 self,
327 gpu,
328 &bb_gguf,
329 &bb_mmap,
330 &arch,
331 gdn,
332 force_cpu,
333 native_q2,
334 q2_group,
335 q2_variant,
336 &mut weights,
337 &mut skipped,
338 )?;
339 drop(bb_gguf);
340 drop(bb_mmap);
341 evict_page_cache(&bb_file);
342
343 if let (Some((mm_file, mm_mmap, mm_gguf)), Some(mm_arch)) = (mmproj, mmproj_arch) {
347 let before = weights.len();
348 sidecar::load_pass(
351 self,
352 gpu,
353 &mm_gguf,
354 &mm_mmap,
355 &mm_arch,
356 None,
357 force_cpu,
358 false,
359 q2_group,
360 q2_variant,
361 &mut weights,
362 &mut skipped,
363 )?;
364 tracing::info!(
365 "Merged {} mmproj tensors (arch '{}') into the weight store",
366 weights.len() - before,
367 mm_arch,
368 );
369 drop(mm_gguf);
370 drop(mm_mmap);
371 evict_page_cache(&mm_file);
372 }
373
374 if skipped > 0 {
375 tracing::info!("EP: skipped {} remote expert slices", skipped);
376 }
377 check_oom_guard(gpu, oom_reserve_bytes, "weight loading (GGUF)")?;
378 tracing::info!("Loaded {} weight tensors (GGUF → BF16)", weights.len());
379 Ok(WeightStore::from_map(weights))
380 }
381}
382
383#[cfg(test)]
384mod real_file_test;
385
386#[cfg(all(test, feature = "cuda"))]
387mod gpu_validate_test;
388
389#[cfg(test)]
390mod tests;