nxpu_backend_coreml/
lib.rs1use nxpu_analysis::analyze;
7use nxpu_backend_core::{
8 Backend, BackendError, BackendOptions, BackendOutput, Diagnostic, DiagnosticLevel,
9 OutputContent, OutputFile, Precision,
10};
11use nxpu_ir::Module;
12use prost::Message;
13
14mod lower;
15#[doc(hidden)]
16pub mod proto;
17
18#[derive(Debug)]
20pub struct CoreMlBackend;
21
22impl Backend for CoreMlBackend {
23 fn name(&self) -> &str {
24 "CoreML"
25 }
26
27 fn targets(&self) -> &[&str] {
28 &["coreml", "apple-ane"]
29 }
30
31 fn preferred_precision(&self) -> Precision {
32 Precision::F16
33 }
34
35 fn compile(
36 &self,
37 module: &Module,
38 _opts: &BackendOptions,
39 ) -> Result<BackendOutput, BackendError> {
40 if module.entry_points.is_empty() {
41 return Err(BackendError::Other("no entry points in module".into()));
42 }
43
44 let mut files = Vec::new();
45 let mut diagnostics = Vec::new();
46
47 for (i, ep) in module.entry_points.iter().enumerate() {
48 let pattern = analyze::classify_entry_point(module, i).map_err(|e| {
49 BackendError::Unsupported(format!("entry point '{}': {e}", ep.name))
50 })?;
51 if let analyze::KernelPattern::Unknown { reason } = &pattern {
52 return Err(BackendError::Unsupported(format!(
53 "entry point '{}': unrecognized pattern: {reason}",
54 ep.name
55 )));
56 }
57
58 let summary = match &pattern {
59 analyze::KernelPattern::MatMul { .. } => "matmul",
60 analyze::KernelPattern::QuantizedMatMul { .. } => "quantized matmul",
61 analyze::KernelPattern::ElementWise { op, .. } => op.op_name(),
62 analyze::KernelPattern::Conv2D { .. } => "conv",
63 analyze::KernelPattern::Pool { kind, .. } => match kind {
64 analyze::PoolKind::Max => "max_pool",
65 analyze::PoolKind::Avg => "avg_pool",
66 },
67 analyze::KernelPattern::Activation { op, .. } => op.op_name(),
68 analyze::KernelPattern::Reduce { op, .. } => op.op_name(),
69 analyze::KernelPattern::Transpose { .. } => "transpose",
70 analyze::KernelPattern::Reshape { .. } => "reshape",
71 analyze::KernelPattern::Normalization { .. } => "batch_norm",
72 analyze::KernelPattern::Concat { .. } => "concat",
73 analyze::KernelPattern::Split { .. } => "split",
74 analyze::KernelPattern::Attention { .. } => "scaled_dot_product_attention",
75 analyze::KernelPattern::Gather { .. } => "gather",
76 analyze::KernelPattern::Scatter { .. } => "scatter",
77 analyze::KernelPattern::ElementWiseChain { .. } => "element-wise chain",
78 analyze::KernelPattern::Unknown { .. } => "Unknown",
79 };
80
81 diagnostics.push(Diagnostic {
82 level: DiagnosticLevel::Info,
83 message: format!("entry point '{}': MIL op {}", ep.name, summary),
84 });
85
86 let model = lower::build_model(&pattern, &ep.name)?;
87 let bytes = model.encode_to_vec();
88
89 let filename = if module.entry_points.len() == 1 {
90 "output.mlmodel".into()
91 } else {
92 format!("{}.mlmodel", ep.name)
93 };
94
95 files.push(OutputFile {
96 name: filename,
97 content: OutputContent::Binary(bytes),
98 });
99 }
100
101 Ok(BackendOutput { files, diagnostics })
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use nxpu_backend_core::BackendOptions;
109
110 #[test]
111 fn backend_metadata() {
112 let backend = CoreMlBackend;
113 assert_eq!(backend.name(), "CoreML");
114 assert!(backend.targets().contains(&"coreml"));
115 assert!(backend.targets().contains(&"apple-ane"));
116 }
117
118 #[test]
119 fn compile_matmul_wgsl() {
120 let source = std::fs::read_to_string(concat!(
121 env!("CARGO_MANIFEST_DIR"),
122 "/../../examples/matmul.wgsl"
123 ))
124 .unwrap();
125 let module = nxpu_parser::parse(&source).unwrap();
126
127 let output = CoreMlBackend
128 .compile(&module, &BackendOptions::default())
129 .unwrap();
130 assert_eq!(output.files.len(), 1);
131 assert_eq!(output.files[0].name, "output.mlmodel");
132
133 let bytes = match &output.files[0].content {
134 OutputContent::Binary(b) => b,
135 _ => panic!("expected binary output"),
136 };
137
138 let model = proto::Model::decode(bytes.as_slice()).unwrap();
139 assert_eq!(model.specification_version, proto::SPECIFICATION_VERSION);
140 let proto::model::Type::MlProgram(prog) = model.r#type.as_ref().unwrap();
141 assert_eq!(
142 prog.functions[0].block.as_ref().unwrap().operations[0].r#type,
143 "matmul"
144 );
145 }
146
147 #[test]
148 fn compile_vecadd_wgsl() {
149 let source = r#"
150@group(0) @binding(0) var<storage, read> a: array<f32>;
151@group(0) @binding(1) var<storage, read> b: array<f32>;
152@group(0) @binding(2) var<storage, read_write> c: array<f32>;
153struct Params { N: u32 }
154@group(0) @binding(3) var<uniform> params: Params;
155@compute @workgroup_size(256)
156fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
157 let idx = gid.x;
158 if (idx >= params.N) { return; }
159 c[idx] = a[idx] + b[idx];
160}
161"#;
162 let module = nxpu_parser::parse(source).unwrap();
163 let output = CoreMlBackend
164 .compile(&module, &BackendOptions::default())
165 .unwrap();
166
167 let bytes = match &output.files[0].content {
168 OutputContent::Binary(b) => b,
169 _ => panic!("expected binary"),
170 };
171 let model = proto::Model::decode(bytes.as_slice()).unwrap();
172 let proto::model::Type::MlProgram(prog) = model.r#type.as_ref().unwrap();
173 assert_eq!(
174 prog.functions[0].block.as_ref().unwrap().operations[0].r#type,
175 "add"
176 );
177 }
178}