1use std::hash::{DefaultHasher, Hash, Hasher};
19use std::sync::Arc;
20
21use crate::primitive::Primitive;
22use crate::{Array, DType, DeviceKind, Error, Result, default_stream};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum TemplateArg {
27 Int(i64),
29 Bool(bool),
31 DType(DType),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub(crate) struct KernelSpec {
37 name: String,
38 inputs: Vec<String>,
39 outputs: Vec<String>,
40 source: String,
41 header: String,
42 ensure_row_contiguous: bool,
43 atomic_outputs: bool,
44}
45
46#[derive(Debug, Clone)]
88pub struct MetalKernel {
89 spec: KernelSpec,
90}
91
92#[derive(Debug, Clone, Default, PartialEq)]
94pub struct KernelConfig {
95 pub grid: [usize; 3],
97 pub threadgroup: [usize; 3],
99 pub template: Vec<(String, TemplateArg)>,
101 pub init_value: Option<f64>,
103}
104
105#[derive(Debug, Clone)]
107pub struct KernelCall {
108 pub(crate) spec: Arc<KernelSpec>,
109 pub(crate) template: Vec<(String, TemplateArg)>,
110 pub(crate) grid: [usize; 3],
111 pub(crate) threadgroup: [usize; 3],
112 pub(crate) outputs: Vec<(DType, Vec<usize>)>,
113 pub(crate) initialised: bool,
115}
116
117impl PartialEq for KernelCall {
118 fn eq(&self, other: &Self) -> bool {
119 self.spec == other.spec
120 && self.template == other.template
121 && self.grid == other.grid
122 && self.threadgroup == other.threadgroup
123 && self.outputs == other.outputs
124 && self.initialised == other.initialised
125 }
126}
127
128impl Eq for KernelCall {}
129
130impl Hash for KernelCall {
131 fn hash<H: Hasher>(&self, state: &mut H) {
132 self.spec.hash(state);
133 self.template.hash(state);
134 self.grid.hash(state);
135 self.threadgroup.hash(state);
136 self.outputs.hash(state);
137 self.initialised.hash(state);
138 }
139}
140
141impl MetalKernel {
142 pub fn new(name: &str, inputs: &[&str], outputs: &[&str], source: &str) -> Self {
150 Self {
151 spec: KernelSpec {
152 name: name.to_owned(),
153 inputs: inputs.iter().map(|s| (*s).to_owned()).collect(),
154 outputs: outputs.iter().map(|s| (*s).to_owned()).collect(),
155 source: source.to_owned(),
156 header: String::new(),
157 ensure_row_contiguous: true,
158 atomic_outputs: false,
159 },
160 }
161 }
162
163 pub fn header(mut self, header: &str) -> Self {
165 self.spec.header = header.to_owned();
166 self
167 }
168
169 pub fn ensure_row_contiguous(mut self, yes: bool) -> Self {
172 self.spec.ensure_row_contiguous = yes;
173 self
174 }
175
176 pub fn atomic_outputs(mut self, yes: bool) -> Self {
178 self.spec.atomic_outputs = yes;
179 self
180 }
181
182 #[track_caller]
185 pub fn call(
186 &self,
187 inputs: &[Array],
188 outputs: &[(&[i32], DType)],
189 config: &KernelConfig,
190 ) -> Vec<Array> {
191 let ins: Vec<&Array> = inputs.iter().collect();
192 Array::sticky_vec(
193 "metal_kernel",
194 std::panic::Location::caller(),
195 &ins,
196 outputs.len().max(1),
197 || {
198 let shapes = outputs
199 .iter()
200 .map(|(shape, _)| {
201 let spec: Vec<i64> = shape.iter().map(|&d| i64::from(d)).collect();
202 crate::axes::dims(&spec)
203 })
204 .collect::<Result<Vec<_>>>()?;
205 let outs: Vec<(&[usize], DType)> = shapes
206 .iter()
207 .zip(outputs)
208 .map(|(s, (_, d))| (s.as_slice(), *d))
209 .collect();
210 self.call_impl(inputs, &outs, config)
211 },
212 )
213 }
214
215 pub(crate) fn call_impl(
218 &self,
219 inputs: &[Array],
220 outputs: &[(&[usize], DType)],
221 config: &KernelConfig,
222 ) -> Result<Vec<Array>> {
223 let invalid = |msg: String| Err(Error::InvalidArgument(format!("metal_kernel: {msg}")));
224 let spec = &self.spec;
225 let word = |s: &String| {
226 !s.is_empty()
227 && !s.starts_with(|c: char| c.is_ascii_digit())
228 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
229 };
230 if !word(&spec.name) || !spec.inputs.iter().chain(&spec.outputs).all(word) {
231 return invalid("names must be identifiers".into());
232 }
233 if inputs.len() != spec.inputs.len() || outputs.len() != spec.outputs.len() {
234 return invalid(format!(
235 "{} inputs and {} outputs expected, got {} and {}",
236 spec.inputs.len(),
237 spec.outputs.len(),
238 inputs.len(),
239 outputs.len()
240 ));
241 }
242 if outputs.is_empty() {
243 return invalid("a kernel needs at least one output".into());
244 }
245 if config.grid.contains(&0) || config.threadgroup.contains(&0) {
246 return invalid("grid and threadgroup sizes must be positive".into());
247 }
248 if config.threadgroup.iter().product::<usize>() > 1024 {
249 return invalid("at most 1,024 threads per threadgroup".into());
250 }
251 let all = inputs
252 .iter()
253 .map(Array::dtype)
254 .chain(outputs.iter().map(|o| o.1));
255 if let Some(dtype) = all
256 .chain(config.template.iter().filter_map(|(_, a)| match a {
257 TemplateArg::DType(d) => Some(*d),
258 _ => None,
259 }))
260 .find(|&d| msl_type(d).is_none())
261 {
262 return Err(Error::unsupported_dtype("metal_kernel", dtype));
263 }
264 if default_stream().device.kind != DeviceKind::Gpu {
265 return Err(Error::UnsupportedDevice(default_stream().device));
266 }
267 let mut operands = inputs.to_vec();
268 if let Some(v) = config.init_value {
269 for &(shape, dtype) in outputs {
270 operands.push(crate::ops::imp::broadcast_to(
271 &Array::from(v).try_astype(dtype)?,
272 shape,
273 )?);
274 }
275 }
276 let call = KernelCall {
277 spec: Arc::new(KernelSpec {
278 name: spec.name.clone(),
279 inputs: spec.inputs.clone(),
280 outputs: spec.outputs.clone(),
281 source: spec.source.clone(),
282 header: spec.header.clone(),
283 ensure_row_contiguous: spec.ensure_row_contiguous,
284 atomic_outputs: spec.atomic_outputs,
285 }),
286 template: config.template.clone(),
287 grid: config.grid,
288 threadgroup: config.threadgroup,
289 outputs: outputs.iter().map(|&(s, d)| (d, s.to_vec())).collect(),
290 initialised: config.init_value.is_some(),
291 };
292 Array::from_op(Primitive::MetalKernel(call), operands)
293 }
294}
295
296pub(crate) fn msl_type(dtype: DType) -> Option<&'static str> {
298 Some(match dtype {
299 DType::Bool => "bool",
300 DType::U8 => "uint8_t",
301 DType::U16 => "uint16_t",
302 DType::U32 => "uint32_t",
303 DType::U64 => "uint64_t",
304 DType::I8 => "int8_t",
305 DType::I16 => "int16_t",
306 DType::I32 => "int32_t",
307 DType::I64 => "int64_t",
308 DType::F16 => "float16_t",
309 DType::BF16 => "bfloat16_t",
310 DType::F32 => "float",
311 DType::F64 | DType::Complex64 => return None,
312 })
313}
314
315const ATTRIBUTES: [(&str, &str); 13] = [
317 ("thread_position_in_grid", "uint3"),
318 ("threads_per_grid", "uint3"),
319 ("threadgroup_position_in_grid", "uint3"),
320 ("threadgroups_per_grid", "uint3"),
321 ("thread_position_in_threadgroup", "uint3"),
322 ("threads_per_threadgroup", "uint3"),
323 ("thread_index_in_threadgroup", "uint"),
324 ("simdgroup_index_in_threadgroup", "uint"),
325 ("thread_index_in_simdgroup", "uint"),
326 ("simdgroups_per_threadgroup", "uint"),
327 ("threads_per_simdgroup", "uint"),
328 ("quadgroup_index_in_threadgroup", "uint"),
329 ("thread_index_in_quadgroup", "uint"),
330];
331
332fn uses(text: &str, word: &str) -> bool {
334 let ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
335 text.match_indices(word).any(|(i, _)| {
336 let before = text[..i].chars().next_back();
337 let after = text[i + word.len()..].chars().next();
338 !before.is_some_and(ident) && !after.is_some_and(ident)
339 })
340}
341
342pub(crate) struct InputLayout {
344 pub(crate) dtype: DType,
345 pub(crate) scalar: bool,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub(crate) enum Binding {
351 Input(usize),
353 Shape(usize),
355 Strides(usize),
357 Ndim(usize),
359 Output(usize),
361}
362
363impl KernelCall {
364 pub(crate) fn input_count(&self) -> usize {
366 self.spec.inputs.len()
367 }
368
369 pub(crate) fn row_contiguous_inputs(&self) -> bool {
371 self.spec.ensure_row_contiguous
372 }
373
374 pub(crate) fn bindings(&self, inputs: &[InputLayout]) -> Vec<Binding> {
376 let spec = &self.spec;
377 let mut out = Vec::new();
378 for (i, (name, layout)) in spec.inputs.iter().zip(inputs).enumerate() {
379 out.push(Binding::Input(i));
380 if layout.scalar {
381 continue;
382 }
383 if uses(&spec.source, &format!("{name}_shape")) {
384 out.push(Binding::Shape(i));
385 }
386 if uses(&spec.source, &format!("{name}_strides")) {
387 out.push(Binding::Strides(i));
388 }
389 if uses(&spec.source, &format!("{name}_ndim")) {
390 out.push(Binding::Ndim(i));
391 }
392 }
393 out.extend((0..spec.outputs.len()).map(Binding::Output));
394 out
395 }
396
397 pub(crate) fn source(&self, inputs: &[InputLayout]) -> (String, String) {
399 let spec = &self.spec;
400 let ty = |d: DType| msl_type(d).expect("checked when called");
401 let mut params = Vec::new();
402 for (k, binding) in self.bindings(inputs).into_iter().enumerate() {
403 params.push(match binding {
404 Binding::Input(i) => {
405 let (name, layout) = (&spec.inputs[i], &inputs[i]);
406 match layout.scalar {
407 true => format!(
408 "const constant {}& {name} [[buffer({k})]]",
409 ty(layout.dtype)
410 ),
411 false => {
412 format!("const device {}* {name} [[buffer({k})]]", ty(layout.dtype))
413 }
414 }
415 }
416 Binding::Shape(i) => {
417 format!(
418 "const constant int* {}_shape [[buffer({k})]]",
419 spec.inputs[i]
420 )
421 }
422 Binding::Strides(i) => {
423 format!(
424 "const constant int64_t* {}_strides [[buffer({k})]]",
425 spec.inputs[i]
426 )
427 }
428 Binding::Ndim(i) => {
429 format!(
430 "const constant int& {}_ndim [[buffer({k})]]",
431 spec.inputs[i]
432 )
433 }
434 Binding::Output(i) => {
435 let t = ty(self.outputs[i].0);
436 match spec.atomic_outputs {
437 true => format!("device atomic<{t}>* {} [[buffer({k})]]", spec.outputs[i]),
438 false => format!("device {t}* {} [[buffer({k})]]", spec.outputs[i]),
439 }
440 }
441 });
442 }
443 for (attr, t) in ATTRIBUTES {
444 if uses(&spec.source, attr) {
445 params.push(format!("{t} {attr} [[{attr}]]"));
446 }
447 }
448 let template: String = self
449 .template
450 .iter()
451 .map(|(name, arg)| match arg {
452 TemplateArg::Int(v) => format!(" constexpr int {name} = {v};\n"),
453 TemplateArg::Bool(v) => format!(" constexpr bool {name} = {v};\n"),
454 TemplateArg::DType(d) => format!(" typedef {} {name};\n", ty(*d)),
455 })
456 .collect();
457 let body = format!(
458 "{PRELUDE}{}\n[[kernel]] void KERNEL_NAME(\n {}) {{\n{template}{}\n}}\n",
459 spec.header,
460 params.join(",\n "),
461 spec.source
462 );
463 let mut h = DefaultHasher::new();
464 body.hash(&mut h);
465 let name = format!("nuvec_custom_{}_{:016x}", spec.name, h.finish());
466 (name.clone(), body.replace("KERNEL_NAME", &name))
467 }
468}
469
470const PRELUDE: &str = r#"#include <metal_stdlib>
473#include <metal_simdgroup>
474#include <metal_atomic>
475using namespace metal;
476typedef bfloat bfloat16_t;
477typedef half float16_t;
478
479// The element of a strided array at row-major position `elem` (MLX's `elem_to_loc`).
480inline int64_t elem_to_loc(uint elem, constant const int* shape, constant const int64_t* strides, int ndim) {
481 int64_t loc = 0;
482 for (int i = ndim - 1; i >= 0 && elem > 0; --i) {
483 loc += int64_t(elem % shape[i]) * strides[i];
484 elem /= shape[i];
485 }
486 return loc;
487}
488"#;