1use crate::module::{Module, Param};
2use crate::{
3 error::{ConvBuildError, Exception},
4 ops::{conv1d, conv2d, zeros},
5 random::uniform,
6 Array,
7};
8use mlx_internal_macros::{Buildable, Builder};
9use mlx_macros::ModuleParameters;
10
11use crate::utils::{SingleOrPair, SingleOrTriple};
12
13#[derive(Debug, Clone, Builder)]
15#[builder(
16 root = crate,
17 build_with = build_conv1d,
18 err = ConvBuildError,
19)]
20pub struct Conv1dBuilder {
21 pub input_channels: i32,
23
24 pub output_channels: i32,
26
27 pub kernel_size: i32,
29
30 #[builder(optional, default = Conv1d::DEFAULT_BIAS)]
33 pub bias: bool,
34
35 #[builder(optional, default = Conv1d::DEFAULT_STRIDE)]
37 pub stride: i32,
38
39 #[builder(optional, default = Conv1d::DEFAULT_PADDING)]
41 pub padding: i32,
42
43 #[builder(optional, default = Conv1d::DEFAULT_DILATION)]
45 pub dilation: i32,
46
47 #[builder(optional, default = Conv1d::DEFAULT_GROUPS)]
49 pub groups: i32,
50}
51
52fn build_conv1d(builder: Conv1dBuilder) -> Result<Conv1d, ConvBuildError> {
53 let input_channels = builder.input_channels;
54 let output_channels = builder.output_channels;
55 let kernel_size = builder.kernel_size;
56 let with_bias = builder.bias;
57 let groups = checked_groups(input_channels, output_channels, builder.groups)?;
58
59 let scale = f32::sqrt(1.0f32 / (input_channels * kernel_size) as f32);
60 let weight = uniform::<_, f32>(
61 -scale,
62 scale,
63 &[output_channels, kernel_size, input_channels / groups],
64 None,
65 )?;
66 let bias = if with_bias {
67 Some(zeros::<f32>(&[output_channels])?)
68 } else {
69 None
70 };
71
72 Ok(Conv1d {
73 weight: Param::new(weight),
74 bias: Param::new(bias),
75 stride: builder.stride,
76 padding: builder.padding,
77 dilation: builder.dilation,
78 groups,
79 })
80}
81
82#[derive(Debug, Clone, ModuleParameters, Buildable)]
90#[module(root = crate)]
91#[buildable(root = crate)]
92pub struct Conv1d {
93 #[param]
95 pub weight: Param<Array>,
96
97 #[param]
99 pub bias: Param<Option<Array>>,
100
101 pub stride: i32,
103
104 pub padding: i32,
106
107 pub dilation: i32,
109
110 pub groups: i32,
112}
113
114impl Conv1d {
115 pub const DEFAULT_BIAS: bool = true;
117
118 pub const DEFAULT_STRIDE: i32 = 1;
120
121 pub const DEFAULT_PADDING: i32 = 0;
123
124 pub const DEFAULT_DILATION: i32 = 1;
126
127 pub const DEFAULT_GROUPS: i32 = 1;
129}
130
131impl Module<&Array> for Conv1d {
132 type Error = Exception;
133 type Output = Array;
134
135 fn forward(&mut self, x: &Array) -> Result<Array, Self::Error> {
136 let mut y = conv1d(
137 x,
138 self.weight.as_ref(),
139 self.stride,
140 self.padding,
141 self.dilation,
142 self.groups,
143 )?;
144 if let Some(bias) = &self.bias.value {
145 y += bias;
146 }
147 Ok(y)
148 }
149
150 fn training_mode(&mut self, _: bool) {}
151}
152
153#[derive(Debug, Clone, Builder)]
155#[builder(
156 root = crate,
157 build_with = build_conv2d,
158 err = ConvBuildError,
159)]
160pub struct Conv2dBuilder {
161 pub input_channels: i32,
163
164 pub output_channels: i32,
166
167 pub kernel_size: SingleOrPair<i32>,
169
170 #[builder(optional, default = Conv2d::DEFAULT_BIAS)]
173 pub bias: bool,
174
175 #[builder(optional, default = Conv2d::DEFAULT_STRIDE)]
177 pub stride: SingleOrPair<i32>,
178
179 #[builder(optional, default = Conv2d::DEFAULT_PADDING)]
181 pub padding: SingleOrPair<i32>,
182
183 #[builder(optional, default = Conv2d::DEFAULT_DILATION)]
185 pub dilation: SingleOrPair<i32>,
186
187 #[builder(optional, default = Conv2d::DEFAULT_GROUPS)]
189 pub groups: i32,
190}
191
192fn build_conv2d(builder: Conv2dBuilder) -> Result<Conv2d, ConvBuildError> {
193 let input_channels = builder.input_channels;
194 let output_channels = builder.output_channels;
195 let kernel_size: (i32, i32) = builder.kernel_size.into();
196 let with_bias = builder.bias;
197 let padding = builder.padding.into();
198 let stride = builder.stride.into();
199 let dilation = builder.dilation.into();
200 let groups = checked_groups(input_channels, output_channels, builder.groups)?;
201
202 let scale = f32::sqrt(1.0 / (input_channels * kernel_size.0 * kernel_size.1) as f32);
203 let weight = uniform::<_, f32>(
204 -scale,
205 scale,
206 &[
207 output_channels,
208 kernel_size.0,
209 kernel_size.1,
210 input_channels / groups,
211 ],
212 None,
213 )?;
214 let bias = if with_bias {
215 Some(zeros::<f32>(&[output_channels])?)
216 } else {
217 None
218 };
219
220 Ok(Conv2d {
221 weight: Param::new(weight),
222 bias: Param::new(bias),
223 stride,
224 padding,
225 dilation,
226 groups,
227 })
228}
229
230#[derive(Debug, Clone, ModuleParameters, Buildable)]
239#[module(root = crate)]
240#[buildable(root = crate)]
241pub struct Conv2d {
242 #[param]
244 pub weight: Param<Array>,
245
246 #[param]
248 pub bias: Param<Option<Array>>,
249
250 pub stride: (i32, i32),
252
253 pub padding: (i32, i32),
255
256 pub dilation: (i32, i32),
258
259 pub groups: i32,
261}
262
263impl Conv2d {
264 pub const DEFAULT_BIAS: bool = true;
266
267 pub const DEFAULT_STRIDE: SingleOrPair = SingleOrPair::Pair(1, 1);
269
270 pub const DEFAULT_PADDING: SingleOrPair = SingleOrPair::Pair(0, 0);
272
273 pub const DEFAULT_DILATION: SingleOrPair = SingleOrPair::Pair(1, 1);
275
276 pub const DEFAULT_GROUPS: i32 = 1;
278}
279
280impl Module<&Array> for Conv2d {
281 type Error = Exception;
282 type Output = Array;
283
284 fn forward(&mut self, x: &Array) -> Result<Array, Self::Error> {
285 let mut y = conv2d(
286 x,
287 self.weight.as_ref(),
288 self.stride,
289 self.padding,
290 self.dilation,
291 self.groups,
292 )?;
293 if let Some(bias) = &self.bias.value {
294 y += bias;
295 }
296 Ok(y)
297 }
298
299 fn training_mode(&mut self, _: bool) {}
300}
301
302#[derive(Debug, Clone, Builder)]
304#[builder(
305 root = crate,
306 build_with = build_conv3d,
307 err = ConvBuildError,
308)]
309pub struct Conv3dBuilder {
310 pub input_channels: i32,
312
313 pub output_channels: i32,
315
316 pub kernel_size: SingleOrTriple<i32>,
318
319 #[builder(optional, default = Conv3d::DEFAULT_BIAS)]
322 pub bias: bool,
323
324 #[builder(optional, default = Conv3d::DEFAULT_STRIDE)]
326 pub stride: SingleOrTriple<i32>,
327
328 #[builder(optional, default = Conv3d::DEFAULT_PADDING)]
330 pub padding: SingleOrTriple<i32>,
331
332 #[builder(optional, default = Conv3d::DEFAULT_DILATION)]
334 pub dilation: SingleOrTriple<i32>,
335
336 #[builder(optional, default = Conv3d::DEFAULT_GROUPS)]
338 pub groups: i32,
339}
340
341fn build_conv3d(builder: Conv3dBuilder) -> Result<Conv3d, ConvBuildError> {
342 let input_channels = builder.input_channels;
343 let output_channels = builder.output_channels;
344 let kernel_size: (i32, i32, i32) = builder.kernel_size.into();
345 let with_bias = builder.bias;
346 let padding = builder.padding.into();
347 let stride = builder.stride.into();
348 let dilation = builder.dilation.into();
349 if builder.groups != 1 {
350 return Err(ConvBuildError::GroupedConv3d(builder.groups));
351 }
352
353 let scale =
354 f32::sqrt(1.0 / (input_channels * kernel_size.0 * kernel_size.1 * kernel_size.2) as f32);
355 let weight = uniform::<_, f32>(
356 -scale,
357 scale,
358 &[
359 output_channels,
360 kernel_size.0,
361 kernel_size.1,
362 kernel_size.2,
363 input_channels,
364 ],
365 None,
366 )?;
367 let bias = if with_bias {
368 Some(zeros::<f32>(&[output_channels])?)
369 } else {
370 None
371 };
372
373 Ok(Conv3d {
374 weight: Param::new(weight),
375 bias: Param::new(bias),
376 stride,
377 padding,
378 dilation,
379 groups: builder.groups,
380 })
381}
382
383#[derive(Debug, Clone, ModuleParameters, Buildable)]
392#[module(root = crate)]
393#[buildable(root = crate)]
394pub struct Conv3d {
395 #[param]
397 pub weight: Param<Array>,
398
399 #[param]
401 pub bias: Param<Option<Array>>,
402
403 pub stride: (i32, i32, i32),
405
406 pub padding: (i32, i32, i32),
408
409 pub dilation: (i32, i32, i32),
411
412 pub groups: i32,
414}
415
416impl Conv3d {
417 pub const DEFAULT_BIAS: bool = true;
419
420 pub const DEFAULT_STRIDE: SingleOrTriple<i32> = SingleOrTriple::Triple(1, 1, 1);
422
423 pub const DEFAULT_PADDING: SingleOrTriple<i32> = SingleOrTriple::Triple(0, 0, 0);
425
426 pub const DEFAULT_DILATION: SingleOrTriple<i32> = SingleOrTriple::Triple(1, 1, 1);
428
429 pub const DEFAULT_GROUPS: i32 = 1;
431}
432
433impl Module<&Array> for Conv3d {
434 type Error = Exception;
435 type Output = Array;
436
437 fn forward(&mut self, x: &Array) -> Result<Array, Self::Error> {
438 let mut y = crate::ops::conv3d(
439 x,
440 self.weight.as_ref(),
441 self.stride,
442 self.padding,
443 self.dilation,
444 self.groups,
445 )?;
446 if let Some(bias) = &self.bias.value {
447 y += bias;
448 }
449 Ok(y)
450 }
451
452 fn training_mode(&mut self, _: bool) {}
453}
454
455fn checked_groups(
458 input_channels: i32,
459 output_channels: i32,
460 groups: i32,
461) -> Result<i32, ConvBuildError> {
462 if groups < 1 || input_channels % groups != 0 || output_channels % groups != 0 {
463 return Err(ConvBuildError::InvalidGroups {
464 groups,
465 input_channels,
466 output_channels,
467 });
468 }
469 Ok(groups)
470}
471
472#[cfg(test)]
475mod tests {
476 use crate::module::Module;
477 use crate::{random::uniform, Dtype};
478 use float_eq::assert_float_eq;
479
480 use crate::nn::Conv1d;
481
482 #[test]
483 fn test_conv1d() {
484 crate::random::seed(819).unwrap();
485 let a = uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
486 assert_eq!(a.shape(), &[2, 8, 16]);
487 assert_eq!(a.dtype(), Dtype::Float32);
488 assert_float_eq!(
489 a.mean(None).unwrap().item_exact::<f32>(),
490 0.512_987_5,
491 abs <= 0.010_259_75
492 );
493 assert_float_eq!(
494 a.sum(None).unwrap().item_exact::<f32>(),
495 131.324_8,
496 abs <= 2.626_496
497 );
498 let result = Conv1d::new(16, 2, 8).unwrap().forward(&a).unwrap();
499 assert_eq!(result.shape(), &[2, 1, 2]);
500 assert_eq!(result.dtype(), Dtype::Float32);
501 assert_float_eq!(
502 result.mean(None).unwrap().item_exact::<f32>(),
503 0.264_865_2,
504 abs <= 0.005_297_303_7
505 );
506 assert_float_eq!(
507 result.sum(None).unwrap().item_exact::<f32>(),
508 1.059_460_8,
509 abs <= 0.021_189_215
510 );
511 }
512
513 #[test]
514 fn test_conv2d() {
515 crate::random::seed(62).unwrap();
516 let a = uniform::<_, f32>(0.0, 1.0, &[2, 8, 8, 4], None).unwrap();
517 assert_eq!(a.shape(), &[2, 8, 8, 4]);
518 assert_eq!(a.dtype(), Dtype::Float32);
519 assert_float_eq!(
520 a.mean(None).unwrap().item_exact::<f32>(),
521 0.522_504_27,
522 abs <= 0.010_450_086
523 );
524 assert_float_eq!(
525 a.sum(None).unwrap().item_exact::<f32>(),
526 267.522_2,
527 abs <= 5.350_444
528 );
529 let result = crate::nn::Conv2d::new(4, 2, (8, 8))
530 .unwrap()
531 .forward(&a)
532 .unwrap();
533 assert_eq!(result.shape(), &[2, 1, 1, 2]);
534 assert_eq!(result.dtype(), Dtype::Float32);
535 assert_float_eq!(
536 result.mean(None).unwrap().item_exact::<f32>(),
537 -0.279_321_5,
538 abs <= 0.005_586_43
539 );
540 assert_float_eq!(
541 result.sum(None).unwrap().item_exact::<f32>(),
542 -1.117_286,
543 abs <= 0.022_345_72
544 );
545 }
546
547 #[test]
548 fn test_conv1d_depthwise_matches_mlx() {
549 use crate::{builder::Builder, nn::Conv1dBuilder, ops::arange};
550
551 let x = &arange::<_, f32>(None, 48, None)
552 .unwrap()
553 .reshape(&[2, 6, 4])
554 .unwrap()
555 / 10.0;
556 let w = &(&arange::<_, f32>(None, 12, None)
557 .unwrap()
558 .reshape(&[4, 3, 1])
559 .unwrap()
560 / 7.0)
561 - 0.5;
562
563 let mut conv = Conv1dBuilder::new(4, 4, 3)
564 .padding(1)
565 .groups(4)
566 .build()
567 .unwrap();
568 assert_eq!(conv.weight.shape(), &[4, 3, 1]);
569 conv.weight.value = w;
570 let y = conv.forward(&x).unwrap();
571 assert_eq!(y.shape(), &[2, 6, 4]);
572
573 #[rustfmt::skip]
575 let expected: [f32; 48] = [
576 -0.08571428, 0.1142857, 0.4857143, 1.028571,
577 -0.3142857, 0.2214286, 1.014286, 2.064286,
578 -0.7428572, 0.3071429, 1.614286, 3.178571,
579 -1.171429, 0.3928572, 2.214286, 4.292857,
580 -1.6, 0.4785715, 2.814286, 5.407143,
581 -1.514286, 0.02857148, 1.742857, 3.628572,
582 -1.457143, 0.8000001, 3.228571, 5.828571,
583 -2.885714, 0.7357144, 4.614286, 8.75,
584 -3.314286, 0.8214287, 5.214286, 9.864285,
585 -3.742857, 0.907143, 5.814286, 10.97857,
586 -4.171429, 0.9928573, 6.414286, 12.09286,
587 -3.571429, 0.02857158, 3.8, 7.742857,
588 ];
589 for (got, want) in y.to_vec_exact::<f32>().unwrap().iter().zip(expected) {
590 assert_float_eq!(*got, want, abs <= 1e-5 * (1.0 + want.abs()));
591 }
592 }
593
594 #[test]
595 fn test_conv2d_grouped_weight_shape() {
596 use crate::{builder::Builder, nn::Conv2dBuilder};
597
598 let mut conv = Conv2dBuilder::new(4, 8, 3).groups(2).build().unwrap();
599 assert_eq!(conv.weight.shape(), &[8, 3, 3, 2]);
600 let x = uniform::<_, f32>(0.0, 1.0, &[1, 5, 5, 4], None).unwrap();
601 assert_eq!(conv.forward(&x).unwrap().shape(), &[1, 3, 3, 8]);
602 }
603
604 #[test]
605 fn test_conv_groups_must_divide_channels() {
606 use crate::{
607 builder::Builder,
608 error::ConvBuildError,
609 nn::{Conv1dBuilder, Conv2dBuilder, Conv3dBuilder},
610 };
611
612 let invalid = |groups, input_channels, output_channels| ConvBuildError::InvalidGroups {
613 groups,
614 input_channels,
615 output_channels,
616 };
617 assert_eq!(
618 Conv1dBuilder::new(6, 4, 3).groups(4).build().unwrap_err(),
619 invalid(4, 6, 4)
620 );
621 assert_eq!(
622 Conv1dBuilder::new(4, 6, 3).groups(4).build().unwrap_err(),
623 invalid(4, 4, 6)
624 );
625 assert_eq!(
626 Conv1dBuilder::new(4, 4, 3).groups(0).build().unwrap_err(),
627 invalid(0, 4, 4)
628 );
629 assert_eq!(
630 Conv2dBuilder::new(4, 6, 3).groups(4).build().unwrap_err(),
631 invalid(4, 4, 6)
632 );
633 assert_eq!(
634 Conv3dBuilder::new(4, 4, 3).groups(2).build().unwrap_err(),
635 ConvBuildError::GroupedConv3d(2)
636 );
637 }
638}