1pub mod msl;
41pub mod wgsl;
42
43use scirs2_core::gpu::GpuBackend;
44
45pub const WORKGROUP_SIZE: usize = 256;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum OptimizerKernel {
54 Adam,
56 AdamW,
58 Sgd,
60 Rmsprop,
62 Adagrad,
64 Lamb,
66}
67
68impl OptimizerKernel {
69 pub fn id(self) -> &'static str {
71 match self {
72 Self::Adam => "adam",
73 Self::AdamW => "adamw",
74 Self::Sgd => "sgd",
75 Self::Rmsprop => "rmsprop",
76 Self::Adagrad => "adagrad",
77 Self::Lamb => "lamb",
78 }
79 }
80
81 pub fn source_for(self, backend: GpuBackend) -> Option<&'static str> {
83 match backend {
84 GpuBackend::Wgpu => Some(match self {
85 Self::Adam => wgsl::ADAM,
86 Self::AdamW => wgsl::ADAMW,
87 Self::Sgd => wgsl::SGD,
88 Self::Rmsprop => wgsl::RMSPROP,
89 Self::Adagrad => wgsl::ADAGRAD,
90 Self::Lamb => wgsl::LAMB,
91 }),
92 GpuBackend::Metal => Some(match self {
93 Self::Adam => msl::ADAM,
94 Self::AdamW => msl::ADAMW,
95 Self::Sgd => msl::SGD,
96 Self::Rmsprop => msl::RMSPROP,
97 Self::Adagrad => msl::ADAGRAD,
98 Self::Lamb => msl::LAMB,
99 }),
100 _ => None,
101 }
102 }
103
104 pub fn cache_key(self, backend: GpuBackend) -> &'static str {
106 let _ = backend;
109 self.id()
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum CollectiveKernel {
119 AllReduceMean,
122}
123
124impl CollectiveKernel {
125 pub fn id(self) -> &'static str {
127 match self {
128 Self::AllReduceMean => "all_reduce_mean",
129 }
130 }
131
132 pub fn source_for(self, backend: GpuBackend) -> Option<&'static str> {
134 match backend {
135 GpuBackend::Wgpu => Some(match self {
136 Self::AllReduceMean => wgsl::ALL_REDUCE_MEAN,
137 }),
138 GpuBackend::Metal => Some(match self {
139 Self::AllReduceMean => msl::ALL_REDUCE_MEAN,
140 }),
141 _ => None,
142 }
143 }
144
145 pub fn cache_key(self, backend: GpuBackend) -> &'static str {
147 let _ = backend;
148 self.id()
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 const ALL: [OptimizerKernel; 6] = [
157 OptimizerKernel::Adam,
158 OptimizerKernel::AdamW,
159 OptimizerKernel::Sgd,
160 OptimizerKernel::Rmsprop,
161 OptimizerKernel::Adagrad,
162 OptimizerKernel::Lamb,
163 ];
164
165 #[test]
169 fn msl_entry_points_are_extractable() {
170 for kernel in ALL {
171 let source = kernel
172 .source_for(GpuBackend::Metal)
173 .expect("every kernel has MSL");
174 let start = source
175 .find("kernel void ")
176 .expect("MSL source declares a kernel");
177 let rest = &source[start + "kernel void ".len()..];
178 let paren = rest.find('(').expect("entry point is followed by '('");
179 let name = &rest[..paren];
180 assert!(
181 !name.contains('\n'),
182 "{}: entry point spans lines",
183 kernel.id()
184 );
185 assert!(
186 !name.trim().is_empty(),
187 "{}: empty entry point",
188 kernel.id()
189 );
190 }
191 }
192
193 #[test]
198 fn wgsl_matches_the_reflection_parser() {
199 for kernel in ALL {
200 let source = kernel
201 .source_for(GpuBackend::Wgpu)
202 .expect("every kernel has WGSL");
203 let id = kernel.id();
204
205 assert!(
206 !source.contains("var<uniform>"),
207 "{id}: uniform blocks are packed in non-deterministic order"
208 );
209 assert!(
210 !source.contains("var<storage,read"),
211 "{id}: `var<storage,read>` is not recognised; use `var<storage, read>`"
212 );
213
214 let mut entry_lines = 0;
215 for line in source.lines() {
216 let trimmed = line.trim();
217 if trimmed.contains("@compute") {
218 entry_lines += 1;
219 assert!(
220 trimmed.contains("fn main("),
221 "{id}: @compute must share its line with `fn main(`"
222 );
223 }
224 if trimmed.contains("@binding(") {
225 assert!(
226 trimmed.contains("@group(0)"),
227 "{id}: every binding must be in @group(0)"
228 );
229 assert!(
230 trimmed.contains("var<"),
231 "{id}: binding attributes must share the declaration line"
232 );
233 }
234 }
235 assert_eq!(entry_lines, 1, "{id}: expected exactly one entry point");
236 }
237 }
238
239 #[test]
242 fn only_deterministic_buffer_names_are_used() {
243 const ALLOWED: [&str; 6] = ["x", "y", "a", "b", "result", "output"];
244 for kernel in ALL {
245 let source = kernel
246 .source_for(GpuBackend::Wgpu)
247 .expect("every kernel has WGSL");
248 for line in source.lines() {
249 let trimmed = line.trim();
250 if !trimmed.contains("@binding(") {
251 continue;
252 }
253 let after = trimmed
254 .split_once('>')
255 .map(|(_, rest)| rest)
256 .unwrap_or_default();
257 let name = after
258 .split_once(':')
259 .map(|(n, _)| n.trim())
260 .unwrap_or_default();
261 assert!(
262 ALLOWED.contains(&name),
263 "{}: buffer name {name:?} is not in the deterministic set {ALLOWED:?}",
264 kernel.id()
265 );
266 }
267 }
268 }
269
270 #[test]
271 fn unsupported_backends_have_no_source() {
272 assert!(OptimizerKernel::Adam.source_for(GpuBackend::Cpu).is_none());
273 assert!(OptimizerKernel::Adam.source_for(GpuBackend::Cuda).is_none());
274 assert!(OptimizerKernel::Adam
275 .source_for(GpuBackend::OpenCL)
276 .is_none());
277 }
278
279 const COLLECTIVE_ALL: [CollectiveKernel; 1] = [CollectiveKernel::AllReduceMean];
280
281 #[test]
282 fn collective_msl_entry_points_are_extractable() {
283 for kernel in COLLECTIVE_ALL {
284 let source = kernel
285 .source_for(GpuBackend::Metal)
286 .expect("every collective kernel has MSL");
287 let start = source
288 .find("kernel void ")
289 .expect("MSL source declares a kernel");
290 let rest = &source[start + "kernel void ".len()..];
291 let paren = rest.find('(').expect("entry point is followed by '('");
292 let name = &rest[..paren];
293 assert!(
294 !name.contains('\n'),
295 "{}: entry point spans lines",
296 kernel.id()
297 );
298 assert!(
299 !name.trim().is_empty(),
300 "{}: empty entry point",
301 kernel.id()
302 );
303 }
304 }
305
306 #[test]
307 fn collective_wgsl_matches_the_reflection_parser() {
308 for kernel in COLLECTIVE_ALL {
309 let source = kernel
310 .source_for(GpuBackend::Wgpu)
311 .expect("every collective kernel has WGSL");
312 let id = kernel.id();
313
314 assert!(
315 !source.contains("var<uniform>"),
316 "{id}: uniform blocks are packed in non-deterministic order"
317 );
318 assert!(
319 !source.contains("var<storage,read"),
320 "{id}: `var<storage,read>` is not recognised; use `var<storage, read>`"
321 );
322
323 let mut entry_lines = 0;
324 for line in source.lines() {
325 let trimmed = line.trim();
326 if trimmed.contains("@compute") {
327 entry_lines += 1;
328 assert!(
329 trimmed.contains("fn main("),
330 "{id}: @compute must share its line with `fn main(`"
331 );
332 }
333 if trimmed.contains("@binding(") {
334 assert!(
335 trimmed.contains("@group(0)"),
336 "{id}: every binding must be in @group(0)"
337 );
338 assert!(
339 trimmed.contains("var<"),
340 "{id}: binding attributes must share the declaration line"
341 );
342 }
343 }
344 assert_eq!(entry_lines, 1, "{id}: expected exactly one entry point");
345 }
346 }
347
348 #[test]
349 fn collective_only_deterministic_buffer_names_are_used() {
350 const ALLOWED: [&str; 6] = ["x", "y", "a", "b", "result", "output"];
351 for kernel in COLLECTIVE_ALL {
352 let source = kernel
353 .source_for(GpuBackend::Wgpu)
354 .expect("every collective kernel has WGSL");
355 for line in source.lines() {
356 let trimmed = line.trim();
357 if !trimmed.contains("@binding(") {
358 continue;
359 }
360 let after = trimmed
361 .split_once('>')
362 .map(|(_, rest)| rest)
363 .unwrap_or_default();
364 let name = after
365 .split_once(':')
366 .map(|(n, _)| n.trim())
367 .unwrap_or_default();
368 assert!(
369 ALLOWED.contains(&name),
370 "{}: buffer name {name:?} is not in the deterministic set {ALLOWED:?}",
371 kernel.id()
372 );
373 }
374 }
375 }
376
377 #[test]
378 fn collective_unsupported_backends_have_no_source() {
379 assert!(CollectiveKernel::AllReduceMean
380 .source_for(GpuBackend::Cpu)
381 .is_none());
382 assert!(CollectiveKernel::AllReduceMean
383 .source_for(GpuBackend::Cuda)
384 .is_none());
385 assert!(CollectiveKernel::AllReduceMean
386 .source_for(GpuBackend::OpenCL)
387 .is_none());
388 }
389}