1mod allocator;
4mod buffer;
5mod capture;
6mod command;
7mod event;
8pub mod profile;
9mod residency;
10
11use std::collections::{HashMap, HashSet};
12use std::ptr::NonNull;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
15
16use allocator::Pool;
17pub use buffer::Buffer;
18pub use command::{Arg, CommandBuffer, Deferred, Pending, Submitted, Timeline};
19pub use event::Event;
20use nuvec_kernels::{MetallibMode, NaxMode};
21use objc2::rc::{Retained, autoreleasepool};
22use objc2::runtime::ProtocolObject;
23use objc2_foundation::NSString;
24use objc2_metal::{
25 MTLCommandQueue, MTLComputePipelineState, MTLCreateSystemDefaultDevice, MTLDataType, MTLDevice,
26 MTLFunctionConstantValues, MTLLibrary,
27};
28
29use crate::info::{NaxOverride, NaxReason, NaxStatus, nax_status, parse_architecture};
30use crate::{DeviceInfo, GpuError, MemoryStats, Result};
31
32type Library = Retained<ProtocolObject<dyn MTLLibrary>>;
33
34struct Libraries {
37 libraries: Vec<Library>,
38 by_name: Vec<(Box<str>, usize)>,
43}
44
45impl Libraries {
46 fn new(libraries: Vec<Library>) -> Self {
47 let mut by_name: Vec<(Box<str>, usize)> = Vec::new();
48 for (i, library) in libraries.iter().enumerate() {
49 for name in library.functionNames().iter() {
50 by_name.push((name.to_string().into_boxed_str(), i));
51 }
52 }
53 by_name.sort_by(|a, b| a.0.cmp(&b.0));
55 by_name.dedup_by(|later, first| later.0 == first.0);
56 Libraries { libraries, by_name }
57 }
58
59 fn get(&self, name: &str) -> Result<&Library> {
61 self.by_name
62 .binary_search_by(|(n, _)| n.as_ref().cmp(name))
63 .map(|at| &self.libraries[self.by_name[at].1])
64 .map_err(|_| GpuError::MissingKernel(name.to_owned()))
65 }
66}
67
68type Slot = Arc<Mutex<Option<Pipeline>>>;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum Constant {
74 Bool(bool),
76 Int(i32),
78}
79
80#[derive(Clone)]
82pub struct Device {
83 inner: Arc<Inner>,
84}
85
86struct Inner {
87 device: Retained<ProtocolObject<dyn MTLDevice>>,
88 queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
89 info: DeviceInfo,
90 library: OnceLock<Result<Libraries>>,
91 nax_library: OnceLock<Result<Libraries>>,
93 nax: OnceLock<NaxStatus>,
95 nax_failed: AtomicBool,
97 pipelines: Mutex<HashMap<String, Slot>>,
102 pool: Arc<Pool>,
103 in_flight: InFlight,
104}
105
106impl Drop for Inner {
107 fn drop(&mut self) {
110 autoreleasepool(|_| self.pool.clear_cache());
111 }
112}
113
114#[derive(Default)]
116pub(crate) struct InFlight {
117 count: Mutex<usize>,
118 done: Condvar,
119}
120
121impl InFlight {
122 pub(crate) fn begin(&self) {
123 *self.count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
124 }
125
126 pub(crate) fn end(&self) {
127 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
128 *count = count.saturating_sub(1);
129 self.done.notify_all();
130 }
131
132 fn get(&self) -> usize {
133 *self.count.lock().unwrap_or_else(PoisonError::into_inner)
134 }
135
136 fn wait_below(&self, max: usize) {
137 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
138 while *count >= max.max(1) {
139 count = self
140 .done
141 .wait(count)
142 .unwrap_or_else(PoisonError::into_inner);
143 }
144 }
145}
146
147impl Device {
148 pub fn system_default() -> Option<Device> {
151 autoreleasepool(|_| Self::create())
152 }
153
154 fn create() -> Option<Device> {
155 let device = MTLCreateSystemDefaultDevice()?;
156 let queue = device.newCommandQueue()?;
157 let architecture = device.architecture().name().to_string();
158 let (generation, class) = parse_architecture(&architecture);
159 let info = DeviceInfo {
160 name: device.name().to_string(),
161 architecture,
162 generation,
163 class,
164 recommended_working_set: device.recommendedMaxWorkingSetSize(),
165 max_threads_per_threadgroup: device.maxThreadsPerThreadgroup().width,
166 physical_memory: objc2_foundation::NSProcessInfo::processInfo().physicalMemory(),
167 };
168 let ram = info.physical_memory;
169 let options = objc2_metal::MTLResourceOptions::StorageModeShared;
170 let placeholder = device.newBufferWithLength_options(16, options);
171 let residency = residency::Residency::new(
172 residency::metal(&device, &queue),
173 info.recommended_working_set,
174 );
175 let pool = Arc::new(Pool::new(
176 info.recommended_working_set,
177 ram,
178 placeholder,
179 residency,
180 ));
181 let interval = std::env::var("MLX_METAL_RESIDENCY_REFRESH_INTERVAL_MS")
183 .ok()
184 .and_then(|v| v.trim().parse::<u64>().ok())
185 .filter(|&ms| ms > 0);
186 if let Some(ms) = interval {
187 let pool = Arc::downgrade(&pool);
188 let refresh = move || {
189 while let Some(pool) = {
190 std::thread::sleep(std::time::Duration::from_millis(ms));
191 pool.upgrade()
192 } {
193 pool.refresh_residency();
194 }
195 };
196 let _ = std::thread::Builder::new()
197 .name("nuvec-residency".into())
198 .spawn(refresh);
199 }
200 Some(Device {
201 inner: Arc::new(Inner {
202 device,
203 queue,
204 info,
205 library: OnceLock::new(),
206 nax_library: OnceLock::new(),
207 nax: OnceLock::new(),
208 nax_failed: AtomicBool::new(false),
209 pipelines: Mutex::new(HashMap::new()),
210 pool,
211 in_flight: InFlight::default(),
212 }),
213 })
214 }
215
216 pub fn shared() -> Option<&'static Device> {
219 static SHARED: OnceLock<Option<Device>> = OnceLock::new();
220 SHARED.get_or_init(Device::system_default).as_ref()
221 }
222
223 pub fn memory(&self) -> MemoryStats {
225 self.inner.pool.stats()
226 }
227
228 pub fn system_allocations(&self) -> u64 {
231 self.inner.pool.system_allocations()
232 }
233
234 pub fn set_memory_limit(&self, bytes: usize) -> usize {
245 autoreleasepool(|_| self.inner.pool.set_memory_limit(bytes))
246 }
247
248 pub fn set_cache_limit(&self, bytes: usize) -> usize {
250 autoreleasepool(|_| self.inner.pool.set_cache_limit(bytes))
251 }
252
253 pub fn set_wired_limit(&self, bytes: usize) -> Result<usize> {
260 if bytes as u64 > self.inner.info.recommended_working_set {
261 return Err(GpuError::InvalidArgument(format!(
262 "wired limit {bytes} exceeds the recommended working set ({} bytes)",
263 self.inner.info.recommended_working_set
264 )));
265 }
266 Ok(autoreleasepool(|_| self.inner.pool.set_wired_limit(bytes)))
267 }
268
269 #[doc(hidden)]
272 pub fn __set_residency_set_cap(&self, bytes: usize) {
273 self.inner.pool.set_residency_cap(bytes);
274 }
275
276 pub fn can_wire(&self) -> bool {
278 self.inner.pool.can_wire()
279 }
280
281 pub fn clear_cache(&self) {
283 autoreleasepool(|_| self.inner.pool.clear_cache());
284 }
285
286 pub fn in_flight(&self) -> usize {
288 self.inner.in_flight.get()
289 }
290
291 pub fn wait_for_capacity(&self, max: usize) {
294 self.inner.in_flight.wait_below(max);
295 }
296
297 pub(crate) fn flights(&self) -> &InFlight {
298 &self.inner.in_flight
299 }
300
301 pub fn reset_peak(&self) {
304 self.inner.pool.reset_peak();
305 }
306
307 pub(crate) fn pool(&self) -> &Arc<Pool> {
308 &self.inner.pool
309 }
310
311 pub fn info(&self) -> &DeviceInfo {
313 &self.inner.info
314 }
315
316 pub fn kernel_names(&self) -> Result<Vec<String>> {
319 Ok(self
321 .library()?
322 .by_name
323 .iter()
324 .map(|(n, _)| n.to_string())
325 .collect())
326 }
327
328 pub fn pipeline(&self, name: &str) -> Result<Pipeline> {
334 self.pipeline_with(name, &[])
335 }
336
337 pub fn pipeline_with(&self, name: &str, constants: &[(usize, bool)]) -> Result<Pipeline> {
340 let constants: Vec<(usize, Constant)> = constants
341 .iter()
342 .map(|&(i, v)| (i, Constant::Bool(v)))
343 .collect();
344 self.pipeline_with_constants(name, &constants)
345 }
346
347 pub fn pipeline_with_constants(
350 &self,
351 name: &str,
352 constants: &[(usize, Constant)],
353 ) -> Result<Pipeline> {
354 thread_local! {
355 static KEY: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
357 }
358 let hit = KEY.with(|k| {
359 use std::fmt::Write;
360 let mut k = k.borrow_mut();
361 k.clear();
362 k.push_str(name);
363 for (i, v) in constants {
364 let _ = match v {
365 Constant::Bool(b) => write!(k, "#{i}={}", u8::from(*b)),
366 Constant::Int(n) => write!(k, "#{i}=i{n}"),
367 };
368 }
369 let slot = {
370 let cache = self
371 .inner
372 .pipelines
373 .lock()
374 .unwrap_or_else(PoisonError::into_inner);
375 cache.get(k.as_str()).cloned()
376 };
377 match slot.and_then(|slot| slot.lock().unwrap_or_else(PoisonError::into_inner).clone())
378 {
379 Some(pipeline) => Ok(pipeline),
380 None => Err(k.clone()),
381 }
382 });
383 let key = match hit {
384 Ok(pipeline) => return Ok(pipeline),
385 Err(key) => key,
386 };
387 let nax = name.starts_with("nuvec_nax_");
391 let built = match nax {
392 true => self.nax_library().and_then(|l| l.get(name)),
393 false => self.library().and_then(|l| l.get(name)),
394 }
395 .and_then(|library| self.build_pipeline(name, &key, library, constants));
396 if nax && built.is_err() && self.nax().is_active() {
397 self.inner.nax_failed.store(true, Ordering::Relaxed);
398 }
399 built
400 }
401
402 fn build_pipeline(
403 &self,
404 name: &str,
405 key: &str,
406 library: &Library,
407 constants: &[(usize, Constant)],
408 ) -> Result<Pipeline> {
409 let key = key.to_owned();
410 self.cached(key.clone(), || {
411 let ns_name = NSString::from_str(name);
412 let function = if constants.is_empty() {
413 library
414 .newFunctionWithName(&ns_name)
415 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?
416 } else {
417 let values = MTLFunctionConstantValues::new();
418 for &(index, value) in constants {
419 match value {
420 Constant::Bool(b) => unsafe {
423 values.setConstantValue_type_atIndex(
424 NonNull::from(&b).cast(),
425 MTLDataType::Bool,
426 index,
427 );
428 },
429 Constant::Int(n) => unsafe {
431 values.setConstantValue_type_atIndex(
432 NonNull::from(&n).cast(),
433 MTLDataType::Int,
434 index,
435 );
436 },
437 }
438 }
439 library
440 .newFunctionWithName_constantValues_error(&ns_name, &values)
441 .map_err(|e| GpuError::Pipeline {
442 name: key.clone(),
443 message: e.localizedDescription().to_string(),
444 })?
445 };
446 let state = self
447 .inner
448 .device
449 .newComputePipelineStateWithFunction_error(&function)
450 .map_err(|e| GpuError::Pipeline {
451 name: key.clone(),
452 message: e.localizedDescription().to_string(),
453 })?;
454 Ok(Pipeline {
455 state,
456 name: key.as_str().into(),
457 })
458 })
459 }
460
461 pub fn pipeline_from_source(
473 &self,
474 name: &str,
475 source: impl FnOnce() -> String,
476 ) -> Result<Pipeline> {
477 self.cached(format!("src:{name}"), || {
478 let failed = |message: String| GpuError::Pipeline {
479 name: name.to_owned(),
480 message,
481 };
482 let source = inline_sources(&[("generated.metal", &source())]);
483 let options = objc2_metal::MTLCompileOptions::new();
487 options.setMathMode(objc2_metal::MTLMathMode::Safe);
488 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
489 let library = self
490 .inner
491 .device
492 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
493 .map_err(|e| failed(e.localizedDescription().to_string()))?;
494 let function = library
495 .newFunctionWithName(&NSString::from_str(name))
496 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?;
497 let state = self
498 .inner
499 .device
500 .newComputePipelineStateWithFunction_error(&function)
501 .map_err(|e| failed(e.localizedDescription().to_string()))?;
502 Ok(Pipeline {
503 state,
504 name: name.into(),
505 })
506 })
507 }
508
509 fn cached(&self, key: String, compile: impl FnOnce() -> Result<Pipeline>) -> Result<Pipeline> {
514 let slot = {
515 let mut cache = self
516 .inner
517 .pipelines
518 .lock()
519 .unwrap_or_else(PoisonError::into_inner);
520 cache.entry(key).or_default().clone()
521 };
522 let mut slot = slot.lock().unwrap_or_else(PoisonError::into_inner);
523 if let Some(p) = &*slot {
524 return Ok(p.clone());
525 }
526 let pipeline = autoreleasepool(|_| compile())?;
527 *slot = Some(pipeline.clone());
528 Ok(pipeline)
529 }
530
531 fn library(&self) -> Result<&Libraries> {
533 self.inner
534 .library
535 .get_or_init(|| autoreleasepool(|_| load_library(&self.inner.device)))
536 .as_ref()
537 .map_err(Clone::clone)
538 }
539
540 fn nax_library(&self) -> Result<&Libraries> {
541 self.inner
542 .nax_library
543 .get_or_init(|| autoreleasepool(|_| load_nax_library(&self.inner.device)))
544 .as_ref()
545 .map_err(Clone::clone)
546 }
547
548 pub fn nax(&self) -> NaxStatus {
555 if self.inner.nax_failed.load(Ordering::Relaxed) {
556 return NaxStatus::Off(NaxReason::PipelineError);
557 }
558 *self.inner.nax.get_or_init(|| {
559 let v = objc2_foundation::NSProcessInfo::processInfo().operatingSystemVersion();
560 let os = (v.majorVersion as u32, v.minorVersion as u32);
561 let info = &self.inner.info;
562 nax_status(
563 info.generation,
564 info.class,
565 os,
566 NaxOverride::from_env(),
567 nuvec_kernels::NAX_MODE != NaxMode::Absent,
568 )
569 })
570 }
571
572 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLDevice> {
573 &self.inner.device
574 }
575
576 pub(crate) fn queue(&self) -> &ProtocolObject<dyn MTLCommandQueue> {
577 &self.inner.queue
578 }
579}
580
581impl std::fmt::Debug for Device {
582 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
583 f.debug_struct("Device")
584 .field("info", &self.inner.info)
585 .finish_non_exhaustive()
586 }
587}
588
589fn load_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
595 let error = |e: Retained<objc2_foundation::NSError>| {
596 GpuError::LibraryLoad(e.localizedDescription().to_string())
597 };
598 match nuvec_kernels::MODE {
599 MetallibMode::Precompiled => {
600 let bytes = nuvec_kernels::metallib().ok_or(GpuError::NoKernels)?;
601 let library = device
602 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
603 .map_err(error)?;
604 Ok(Libraries::new(vec![library]))
605 }
606 MetallibMode::RuntimeSource => {
607 let files: Vec<(&str, &str)> = nuvec_kernels::sources()
608 .iter()
609 .copied()
610 .filter(|(p, _)| p.ends_with(".metal"))
611 .collect();
612 let compile = |&(path, text): &(&str, &str)| -> Result<Library> {
613 let mut source = String::new();
614 if !nuvec_kernels::contracted(path) {
615 source.push_str("#pragma clang fp contract(off)\n");
617 }
618 source.push_str(&inline_sources(&[(path, text)]));
619 let options = objc2_metal::MTLCompileOptions::new();
620 options.setMathMode(objc2_metal::MTLMathMode::Safe);
621 options.setMathFloatingPointFunctions(
624 objc2_metal::MTLMathFloatingPointFunctions::Precise,
625 );
626 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
627 device
628 .newLibraryWithSource_options_error(
629 &NSString::from_str(&source),
630 Some(&options),
631 )
632 .map_err(|e| {
633 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
634 })
635 };
636 let libraries = std::thread::scope(|scope| {
637 let handles: Vec<_> = files
638 .iter()
639 .map(|file| scope.spawn(move || compile(file)))
640 .collect();
641 handles
642 .into_iter()
643 .map(|h| h.join().expect("shader compilation does not panic"))
644 .collect::<Result<Vec<_>>>()
645 })?;
646 Ok(Libraries::new(libraries))
647 }
648 _ => Err(GpuError::NoKernels),
649 }
650}
651
652fn load_nax_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
655 match nuvec_kernels::NAX_MODE {
656 NaxMode::Precompiled => {
657 let bytes = nuvec_kernels::nax_metallib().ok_or(GpuError::NoKernels)?;
658 let library = device
659 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
660 .map_err(|e| GpuError::LibraryLoad(e.localizedDescription().to_string()))?;
661 Ok(Libraries::new(vec![library]))
662 }
663 NaxMode::RuntimeSource => {
664 let libraries = nuvec_kernels::nax_sources()
665 .iter()
666 .filter(|(p, _)| p.ends_with(".metal"))
667 .map(|(path, text)| {
668 let options = objc2_metal::MTLCompileOptions::new();
669 options.setMathMode(objc2_metal::MTLMathMode::Safe);
670 options.setMathFloatingPointFunctions(
671 objc2_metal::MTLMathFloatingPointFunctions::Precise,
672 );
673 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version4_0);
674 device
675 .newLibraryWithSource_options_error(
676 &NSString::from_str(text),
677 Some(&options),
678 )
679 .map_err(|e| {
680 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
681 })
682 })
683 .collect::<Result<Vec<_>>>()?;
684 Ok(Libraries::new(libraries))
685 }
686 _ => Err(GpuError::NoKernels),
687 }
688}
689
690fn inline_sources(sources: &[(&str, &str)]) -> String {
694 let headers: HashMap<&str, &str> = nuvec_kernels::sources()
695 .iter()
696 .chain(sources)
697 .copied()
698 .chain([("common.h", nuvec_kernels::COMMON_H)])
699 .filter(|(p, _)| p.ends_with(".h"))
700 .collect();
701 let mut included = HashSet::new();
702 let mut out = String::new();
703 for (_, source) in sources.iter().filter(|(p, _)| p.ends_with(".metal")) {
704 inline_into(source, &headers, &mut included, &mut out);
705 }
706 out
707}
708
709fn inline_into<'a>(
710 source: &str,
711 headers: &HashMap<&'a str, &'a str>,
712 included: &mut HashSet<&'a str>,
713 out: &mut String,
714) {
715 for line in source.lines() {
716 let header = line
717 .trim()
718 .strip_prefix("#include \"")
719 .and_then(|r| r.strip_suffix('"'));
720 match header.and_then(|h| headers.get_key_value(h)) {
721 Some((&h, &text)) => {
722 if included.insert(h) {
723 inline_into(text, headers, included, out);
724 out.push('\n');
725 }
726 }
727 None => {
728 out.push_str(line);
729 out.push('\n');
730 }
731 }
732 }
733}
734
735#[derive(Clone)]
737pub struct Pipeline {
738 state: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
739 name: Arc<str>,
740}
741
742impl Pipeline {
743 pub fn name(&self) -> &str {
745 &self.name
746 }
747
748 pub fn max_threads_per_threadgroup(&self) -> usize {
750 self.state.maxTotalThreadsPerThreadgroup()
751 }
752
753 pub fn execution_width(&self) -> usize {
755 self.state.threadExecutionWidth()
756 }
757
758 pub fn same_as(&self, other: &Pipeline) -> bool {
760 Retained::as_ptr(&self.state) == Retained::as_ptr(&other.state)
761 }
762
763 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLComputePipelineState> {
764 &self.state
765 }
766}
767
768impl std::fmt::Debug for Pipeline {
769 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770 f.debug_struct("Pipeline")
771 .field("name", &self.name)
772 .finish_non_exhaustive()
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::inline_sources;
779
780 #[test]
781 fn inlines_each_header_once() {
782 let sources = [
783 ("a.metal", "#include \"h.h\"\nkernel A;"),
784 ("b.metal", " #include \"h.h\"\nkernel B;"),
785 ("h.h", "#pragma once\ninline int f();"),
786 ];
787 let out = inline_sources(&sources);
788 assert_eq!(out.matches("inline int f();").count(), 1);
789 assert!(out.contains("kernel A;") && out.contains("kernel B;"));
790 assert!(!out.contains("#include \"h.h\""));
791 }
792
793 #[test]
794 fn inlines_headers_included_by_headers() {
795 let sources = [
796 (
797 "a.metal",
798 "#include \"outer.h\"\n#include \"inner.h\"\nkernel A;",
799 ),
800 ("outer.h", "#include \"inner.h\"\ninline int g();"),
801 ("inner.h", "inline int f();"),
802 ];
803 let out = inline_sources(&sources);
804 assert_eq!(out.matches("inline int f();").count(), 1);
805 assert!(out.find("inline int f();") < out.find("inline int g();"));
806 assert!(!out.contains("#include"));
807 }
808}