1#![deny(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
41#![allow(unexpected_cfgs)]
45
46#[macro_use]
48mod error;
49mod buffer;
50mod buffer_pool;
51mod device;
52mod dtypes;
53mod encoder;
54mod encoder_session;
55pub mod encoder_worker;
56mod env_flags;
57mod kernel_registry;
58mod mem_ranges;
59mod residency;
60pub mod gguf;
61pub mod kernel_profile;
62pub mod graph;
63pub mod metal_capture;
64pub mod ops;
65pub mod turboquant;
66pub mod tq_oracle;
67pub mod weight;
68
69pub use buffer::MlxBuffer;
71pub use buffer_pool::MlxBufferPool;
72pub use device::MlxDevice;
73pub use dtypes::DType;
74pub use encoder::{
75 auto_barrier_concurrent_count, auto_barrier_count, barrier_count, barrier_total_ns,
76 cmd_buf_count, dispatch_count, gpu_busy_ns, pipeline_dispatch_buckets, reset_counters,
77 reset_pipeline_dispatch_buckets, set_encode_trace, sync_count, CapturedNode, CapturedOpKind,
78 CommandEncoder, DispatchKind, DispatchRecord, KernelArg, RecordedBinding,
79};
80pub use encoder_session::EncoderSession;
81pub use mem_ranges::{BufferRange, MemRangeRole, MemRanges};
82pub use error::{MlxError, Result};
83pub use graph::{
84 barrier_ns, barrier_ns_reset, ComputeGraph, GraphExecutor, GraphSession, OpKind,
85};
86pub use kernel_registry::KernelRegistry;
87#[doc(hidden)]
93pub use residency::{
94 macos_15_or_newer_for_test, reset_residency_env_cache_for_test,
95 reset_residency_test_counters, residency_allocation_count_for_test,
96 residency_commit_call_count_for_test,
97};
98
99pub use gguf::{GgufFile, MetadataValue, TensorInfo};
101
102pub use ops::dense_mm_bf16::{dense_matmul_bf16_f32_tensor, DenseMmBf16F32Params};
104pub use ops::dense_mm_f16::{dense_matmul_f16_f32_tensor, DenseMmF16F32Params};
105pub use ops::dense_mm_f32_f32::{dense_matmul_f32_f32_tensor, DenseMmF32F32Params};
106pub use ops::quantized_matmul::{quantized_matmul, quantized_matmul_simd, QuantizedMatmulParams};
107pub use ops::quantized_matmul_ggml::{
108 dispatch_mm_for_test, dispatch_mv_q6k_mn, dispatch_mv_q6k_mn_adaptive,
109 quantized_matmul_ggml, quantized_matmul_mm_tensor_perm021,
110 quantized_matmul_mm_tensor_perm021_f16,
111 GgmlQuantizedMatmulParams, GgmlQuantizedMatmulPerm021Params, GgmlType,
112 MM_ROUTING_THRESHOLD,
113};
114pub use ops::mul_mv_ext::{mul_mv_ext_dispatch, MulMvExtParams};
115pub use ops::quantized_matmul_id::{
116 quantized_matmul_id, quantized_matmul_id_into, QuantizedMatmulIdParams,
117};
118pub use ops::quantized_matmul_id_ggml::{
119 dispatch_id_mm_for_test, quantized_matmul_id_ggml, quantized_matmul_id_ggml_pooled,
120 quantized_matmul_id_swiglu_q4_0,
121 GgmlIdMmDispatchParams, GgmlQuantizedMatmulIdParams, IdMmScratch,
122 MM_ID_ROUTING_THRESHOLD,
123};
124
125pub use weight::{
127 load_quantized_weights, safetensors_to_metal_buffer, QuantizationConfig, QuantizedWeight,
128 SafetensorsFile, TensorQuantConfig,
129};
130
131pub use metal::MTLSize;
133pub use metal;
134
135#[cfg(test)]
136#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
137mod tests {
138 use super::*;
139
140 fn _assert_send<T: Send>() {}
142 fn _assert_sync<T: Sync>() {}
143
144 #[allow(dead_code)]
145 fn assert_send_sync() {
146 _assert_send::<MlxDevice>();
147 _assert_sync::<MlxDevice>();
148 _assert_send::<MlxBuffer>();
149 _assert_sync::<MlxBuffer>();
150 _assert_send::<MlxError>();
151 _assert_sync::<MlxError>();
152 }
153
154 #[test]
156 fn test_device_init() {
157 let device = MlxDevice::new().expect("MlxDevice::new() should succeed on Apple Silicon");
158 let name = device.name();
159 assert!(!name.is_empty(), "Device name should not be empty");
160 println!("Metal device: {name}");
161 }
162
163 #[test]
165 fn test_buffer_alloc() {
166 let device = MlxDevice::new().expect("device");
167 let shape = vec![2, 3, 4];
168 let byte_len = 2 * 3 * 4 * DType::F32.size_of(); let buf = device
170 .alloc_buffer(byte_len, DType::F32, shape.clone())
171 .expect("alloc_buffer");
172
173 assert_eq!(buf.dtype(), DType::F32);
174 assert_eq!(buf.shape(), &shape);
175 assert_eq!(buf.byte_len(), byte_len);
176 assert_eq!(buf.element_count(), 24);
177 }
178
179 #[test]
181 fn test_buffer_readwrite() {
182 let device = MlxDevice::new().expect("device");
183 let n = 64;
184 let byte_len = n * std::mem::size_of::<f32>();
185 let mut buf = device
186 .alloc_buffer(byte_len, DType::F32, vec![n])
187 .expect("alloc_buffer");
188
189 {
191 let slice: &mut [f32] = buf.as_mut_slice().expect("as_mut_slice");
192 assert_eq!(slice.len(), n);
193 for (i, val) in slice.iter_mut().enumerate() {
194 *val = i as f32 * 1.5;
195 }
196 }
197
198 {
200 let slice: &[f32] = buf.as_slice().expect("as_slice");
201 for (i, &val) in slice.iter().enumerate() {
202 let expected = i as f32 * 1.5;
203 assert!(
204 (val - expected).abs() < f32::EPSILON,
205 "Mismatch at index {i}: got {val}, expected {expected}"
206 );
207 }
208 }
209 }
210
211 #[test]
213 fn test_encoder_lifecycle() {
214 let device = MlxDevice::new().expect("device");
215 let mut enc = device.command_encoder().expect("command_encoder");
216 enc.commit_and_wait()
218 .expect("commit_and_wait on empty encoder");
219 }
220
221 #[test]
223 fn test_buffer_pool_reuse() {
224 let device = MlxDevice::new().expect("device");
225 let mut pool = MlxBufferPool::new();
226
227 let buf1 = pool
229 .alloc(&device, 1024, DType::F32, vec![256])
230 .expect("pool alloc 1");
231 let buf1_ptr = buf1.contents_ptr();
232 let buf1_byte_len = buf1.byte_len();
233
234 pool.release(buf1);
236 assert_eq!(pool.free_count(), 1);
237
238 let buf2 = pool
240 .alloc(&device, 1024, DType::F32, vec![256])
241 .expect("pool alloc 2");
242 let buf2_ptr = buf2.contents_ptr();
243 let buf2_byte_len = buf2.byte_len();
244
245 assert_eq!(buf1_ptr, buf2_ptr, "Pool should reuse the same Metal buffer");
246 assert_eq!(buf1_byte_len, buf2_byte_len, "Byte lengths should match");
247 assert_eq!(pool.free_count(), 0, "Free list should be empty after reuse");
248 }
249
250 #[test]
252 fn test_kernel_registry_caching() {
253 let device = MlxDevice::new().expect("device");
254 let mut registry = KernelRegistry::new();
255
256 registry.register_source(
258 "test_add",
259 r#"
260 #include <metal_stdlib>
261 using namespace metal;
262 kernel void test_add(
263 device float *a [[buffer(0)]],
264 device float *b [[buffer(1)]],
265 device float *c [[buffer(2)]],
266 uint id [[thread_position_in_grid]]
267 ) {
268 c[id] = a[id] + b[id];
269 }
270 "#,
271 );
272
273 assert!(!registry.is_cached("test_add"));
275 let p1 = registry
276 .get_pipeline("test_add", device.metal_device())
277 .expect("get_pipeline first call");
278 let p1_ptr = p1 as *const _;
279 assert!(registry.is_cached("test_add"));
280
281 let p2 = registry
283 .get_pipeline("test_add", device.metal_device())
284 .expect("get_pipeline second call");
285 let p2_ptr = p2 as *const _;
286
287 assert_eq!(
288 p1_ptr, p2_ptr,
289 "Second get_pipeline call should return the same cached pipeline"
290 );
291 }
292
293 #[test]
295 fn test_buffer_alloc_zero_len_error() {
296 let device = MlxDevice::new().expect("device");
297 let result = device.alloc_buffer(0, DType::F32, vec![]);
298 assert!(result.is_err(), "Zero-length allocation should fail");
299 match result {
300 Err(MlxError::InvalidArgument(_)) => {}
301 other => panic!("Expected InvalidArgument, got {:?}", other),
302 }
303 }
304
305 #[test]
307 fn test_kernel_not_found() {
308 let device = MlxDevice::new().expect("device");
309 let mut registry = KernelRegistry::new();
310 let result = registry.get_pipeline("nonexistent_kernel", device.metal_device());
311 assert!(result.is_err());
312 match result {
313 Err(MlxError::KernelNotFound(name)) => {
314 assert_eq!(name, "nonexistent_kernel");
315 }
316 other => panic!("Expected KernelNotFound, got {:?}", other),
317 }
318 }
319
320 #[test]
322 fn test_dtype_sizes() {
323 assert_eq!(DType::F32.size_of(), 4);
324 assert_eq!(DType::F16.size_of(), 2);
325 assert_eq!(DType::BF16.size_of(), 2);
326 assert_eq!(DType::U8.size_of(), 1);
327 assert_eq!(DType::U16.size_of(), 2);
328 assert_eq!(DType::U32.size_of(), 4);
329 assert_eq!(DType::I32.size_of(), 4);
330 }
331
332 #[test]
334 fn test_buffer_debug() {
335 let device = MlxDevice::new().expect("device");
336 let buf = device
337 .alloc_buffer(64, DType::F16, vec![4, 8])
338 .expect("alloc_buffer");
339 let debug_str = format!("{:?}", buf);
340 assert!(debug_str.contains("MlxBuffer"));
341 assert!(debug_str.contains("F16"));
342 assert!(debug_str.contains("[4, 8]"));
343 }
344
345 #[test]
347 fn test_error_display() {
348 let e = MlxError::DeviceNotFound;
349 assert!(format!("{e}").contains("Metal GPU device"));
350
351 let e = MlxError::ShaderCompilationError {
352 name: "foo".into(),
353 message: "syntax error".into(),
354 };
355 assert!(format!("{e}").contains("foo"));
356 assert!(format!("{e}").contains("syntax error"));
357 }
358
359 #[test]
361 fn test_buffer_pool_size_buckets() {
362 let device = MlxDevice::new().expect("device");
363 let mut pool = MlxBufferPool::new();
364
365 let buf_100 = pool.alloc(&device, 100, DType::U8, vec![100]).expect("alloc 100");
367 assert!(
368 buf_100.byte_len() >= 100,
369 "Buffer should be at least 100 bytes"
370 );
371 pool.release(buf_100);
372
373 let buf_128 = pool.alloc(&device, 128, DType::U8, vec![128]).expect("alloc 128");
375 assert!(buf_128.byte_len() >= 128);
376 pool.release(buf_128);
377
378 let buf_200 = pool.alloc(&device, 200, DType::U8, vec![200]).expect("alloc 200");
380 assert!(buf_200.byte_len() >= 200);
381 pool.release(buf_200);
382
383 assert_eq!(pool.free_count(), 2, "Two different bucket sizes in pool");
384 }
385}