Skip to main content

candle_core/
custom_op.rs

1use crate::layout::LayoutRelation;
2use crate::op::{BackpropOp, Op};
3use crate::tensor::from_storage;
4use crate::{bail, CpuStorage, CudaStorage, Layout, MetalStorage, Result, Shape, Storage, Tensor};
5use std::sync::Arc;
6
7/// Unary ops that can be defined in user-land.
8pub trait CustomOp1 {
9    // Box<dyn> does not support const yet, so use a function to get the name.
10    fn name(&self) -> &'static str;
11
12    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
13    /// offsets etc so the associated layout should be used to access it.
14    fn cpu_fwd(&self, storage: &CpuStorage, layout: &Layout) -> Result<(CpuStorage, Shape)>;
15
16    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
17    /// offsets etc so the associated layout should be used to access it.
18    fn cuda_fwd(&self, _storage: &CudaStorage, _layout: &Layout) -> Result<(CudaStorage, Shape)> {
19        Err(crate::Error::Cuda(
20            format!("no cuda implementation for {}", self.name()).into(),
21        ))
22    }
23
24    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
25    /// offsets etc so the associated layout should be used to access it.
26    fn metal_fwd(
27        &self,
28        _storage: &MetalStorage,
29        _layout: &Layout,
30    ) -> Result<(MetalStorage, Shape)> {
31        Err(crate::Error::Metal(
32            format!("no metal implementation for {}", self.name()).into(),
33        ))
34    }
35
36    /// This function takes as argument the argument `arg` used in the forward pass, the result
37    /// produced by the forward operation `res` and the gradient of the result `grad_res`.
38    /// The function should return the gradient of the argument.
39    fn bwd(&self, _arg: &Tensor, _res: &Tensor, _grad_res: &Tensor) -> Result<Option<Tensor>> {
40        Err(crate::Error::BackwardNotSupported { op: self.name() })
41    }
42}
43
44pub trait CustomOp2 {
45    fn name(&self) -> &'static str;
46
47    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
48    /// offsets etc so the associated layout should be used to access it.
49    fn cpu_fwd(
50        &self,
51        s1: &CpuStorage,
52        l1: &Layout,
53        s2: &CpuStorage,
54        l2: &Layout,
55    ) -> Result<(CpuStorage, Shape)>;
56
57    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
58    /// offsets etc so the associated layout should be used to access it.
59    fn cuda_fwd(
60        &self,
61        _: &CudaStorage,
62        _: &Layout,
63        _: &CudaStorage,
64        _: &Layout,
65    ) -> Result<(CudaStorage, Shape)> {
66        Err(crate::Error::Cuda(
67            format!("no cuda implementation for {}", self.name()).into(),
68        ))
69    }
70
71    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
72    /// offsets etc so the associated layout should be used to access it.
73    fn metal_fwd(
74        &self,
75        _: &MetalStorage,
76        _: &Layout,
77        _: &MetalStorage,
78        _: &Layout,
79    ) -> Result<(MetalStorage, Shape)> {
80        Err(crate::Error::Metal(
81            format!("no metal implementation for {}", self.name()).into(),
82        ))
83    }
84
85    fn bwd(
86        &self,
87        _arg1: &Tensor,
88        _arg2: &Tensor,
89        _res: &Tensor,
90        _grad_res: &Tensor,
91    ) -> Result<(Option<Tensor>, Option<Tensor>)> {
92        Err(crate::Error::BackwardNotSupported { op: self.name() })
93    }
94}
95
96pub trait CustomOp3 {
97    fn name(&self) -> &'static str;
98
99    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
100    /// offsets etc so the associated layout should be used to access it.
101    fn cpu_fwd(
102        &self,
103        s1: &CpuStorage,
104        l1: &Layout,
105        s2: &CpuStorage,
106        l2: &Layout,
107        s3: &CpuStorage,
108        l3: &Layout,
109    ) -> Result<(CpuStorage, Shape)>;
110
111    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
112    /// offsets etc so the associated layout should be used to access it.
113    fn cuda_fwd(
114        &self,
115        _: &CudaStorage,
116        _: &Layout,
117        _: &CudaStorage,
118        _: &Layout,
119        _: &CudaStorage,
120        _: &Layout,
121    ) -> Result<(CudaStorage, Shape)> {
122        Err(crate::Error::Cuda(
123            format!("no cuda implementation for {}", self.name()).into(),
124        ))
125    }
126
127    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
128    /// offsets etc so the associated layout should be used to access it.
129    fn metal_fwd(
130        &self,
131        _: &MetalStorage,
132        _: &Layout,
133        _: &MetalStorage,
134        _: &Layout,
135        _: &MetalStorage,
136        _: &Layout,
137    ) -> Result<(MetalStorage, Shape)> {
138        Err(crate::Error::Metal(
139            format!("no metal implementation for {}", self.name()).into(),
140        ))
141    }
142
143    fn bwd(
144        &self,
145        _arg1: &Tensor,
146        _arg2: &Tensor,
147        _arg3: &Tensor,
148        _res: &Tensor,
149        _grad_res: &Tensor,
150    ) -> Result<(Option<Tensor>, Option<Tensor>, Option<Tensor>)> {
151        Err(crate::Error::BackwardNotSupported { op: self.name() })
152    }
153}
154
155impl Tensor {
156    /// Applies a unary custom op without backward support
157    pub fn apply_op1_no_bwd<C: CustomOp1>(&self, c: &C) -> Result<Self> {
158        let (storage, shape) = self.storage().apply_op1(self.layout(), c)?;
159        Ok(from_storage(storage, shape, BackpropOp::none(), false))
160    }
161
162    /// Applies a binary custom op without backward support
163    pub fn apply_op2_no_bwd<C: CustomOp2>(&self, rhs: &Self, c: &C) -> Result<Self> {
164        let (storage, shape) =
165            self.storage()
166                .apply_op2(self.layout(), &rhs.storage(), rhs.layout(), c)?;
167        Ok(from_storage(storage, shape, BackpropOp::none(), false))
168    }
169
170    /// Applies a ternary custom op without backward support
171    pub fn apply_op3_no_bwd<C: CustomOp3>(&self, t2: &Self, t3: &Self, c: &C) -> Result<Self> {
172        let (storage, shape) = self.storage().apply_op3(
173            self.layout(),
174            &t2.storage(),
175            t2.layout(),
176            &t3.storage(),
177            t3.layout(),
178            c,
179        )?;
180        Ok(from_storage(storage, shape, BackpropOp::none(), false))
181    }
182
183    /// Applies a unary custom op.
184    pub fn apply_op1_arc(&self, c: Arc<Box<dyn CustomOp1 + Send + Sync>>) -> Result<Self> {
185        let (storage, shape) = self
186            .storage()
187            .apply_op1(self.layout(), c.as_ref().as_ref())?;
188        let op = BackpropOp::new1(self, |s| Op::CustomOp1(s, c.clone()));
189        Ok(from_storage(storage, shape, op, false))
190    }
191
192    pub fn apply_op1<C: 'static + CustomOp1 + Send + Sync>(&self, c: C) -> Result<Self> {
193        self.apply_op1_arc(Arc::new(Box::new(c)))
194    }
195
196    /// Applies a binary custom op.
197    pub fn apply_op2_arc(
198        &self,
199        rhs: &Self,
200        c: Arc<Box<dyn CustomOp2 + Send + Sync>>,
201    ) -> Result<Self> {
202        let (storage, shape) = self.storage().apply_op2(
203            self.layout(),
204            &rhs.storage(),
205            rhs.layout(),
206            c.as_ref().as_ref(),
207        )?;
208        let op = BackpropOp::new2(self, rhs, |t1, t2| Op::CustomOp2(t1, t2, c.clone()));
209        Ok(from_storage(storage, shape, op, false))
210    }
211
212    pub fn apply_op2<C: 'static + CustomOp2 + Send + Sync>(&self, r: &Self, c: C) -> Result<Self> {
213        self.apply_op2_arc(r, Arc::new(Box::new(c)))
214    }
215
216    /// Applies a ternary custom op.
217    pub fn apply_op3_arc(
218        &self,
219        t2: &Self,
220        t3: &Self,
221        c: Arc<Box<dyn CustomOp3 + Send + Sync>>,
222    ) -> Result<Self> {
223        let (storage, shape) = self.storage().apply_op3(
224            self.layout(),
225            &t2.storage(),
226            t2.layout(),
227            &t3.storage(),
228            t3.layout(),
229            c.as_ref().as_ref(),
230        )?;
231        let op = BackpropOp::new3(self, t2, t3, |t1, t2, t3| {
232            Op::CustomOp3(t1, t2, t3, c.clone())
233        });
234        Ok(from_storage(storage, shape, op, false))
235    }
236
237    pub fn apply_op3<C: 'static + CustomOp3 + Send + Sync>(
238        &self,
239        t2: &Self,
240        t3: &Self,
241        c: C,
242    ) -> Result<Self> {
243        self.apply_op3_arc(t2, t3, Arc::new(Box::new(c)))
244    }
245}
246
247// In place ops.
248
249/// Unary ops that can be defined in user-land.
250/// These ops work in place and as such back-prop is unsupported.
251pub trait InplaceOp1 {
252    // Box<dyn> does not support const yet, so use a function to get the name.
253    fn name(&self) -> &'static str;
254
255    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
256    /// offsets etc so the associated layout should be used to access it.
257    fn cpu_fwd(&self, storage: &mut CpuStorage, layout: &Layout) -> Result<()>;
258
259    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
260    /// offsets etc so the associated layout should be used to access it.
261    fn cuda_fwd(&self, _storage: &mut CudaStorage, _layout: &Layout) -> Result<()> {
262        Err(crate::Error::Cuda(
263            format!("no cuda implementation for {}", self.name()).into(),
264        ))
265    }
266
267    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
268    /// offsets etc so the associated layout should be used to access it.
269    fn metal_fwd(&self, _storage: &mut MetalStorage, _layout: &Layout) -> Result<()> {
270        Err(crate::Error::Metal(
271            format!("no metal implementation for {}", self.name()).into(),
272        ))
273    }
274}
275
276/// In-place ops that can be defined in user-land.
277/// These ops work in-place and as such back-propagation is unsupported.
278pub trait InplaceOpN<const N: usize> {
279    fn name(&self) -> &'static str;
280
281    /// Defines the source access pattern of this in-place op.
282    /// Defaults to `None`, which rejects all in-place source aliasing.
283    fn src_access_pattern(&self) -> Option<AccessPattern> {
284        None
285    }
286
287    fn cpu_fwd(
288        &self,
289        dst: &mut CpuStorage,
290        dst_l: &Layout,
291        srcs: [(&CpuStorage, &Layout); N],
292    ) -> Result<()> {
293        let _ = (dst, dst_l, srcs);
294        bail!("no cpu implementation for {}", self.name())
295    }
296
297    fn cpu_fwd_aliased(
298        &self,
299        dst: &mut CpuStorage,
300        dst_l: &Layout,
301        srcs: [(Src<'_, CpuStorage>, &Layout); N],
302    ) -> Result<()> {
303        let _ = (dst, dst_l, srcs);
304        bail!("no aliased cpu implementation for {}", self.name())
305    }
306
307    fn cuda_fwd(
308        &self,
309        dst: &mut CudaStorage,
310        dst_l: &Layout,
311        srcs: [(&CudaStorage, &Layout); N],
312    ) -> Result<()> {
313        let _ = (dst, dst_l, srcs);
314        bail!("no cuda implementation for {}", self.name())
315    }
316
317    fn cuda_fwd_aliased(
318        &self,
319        dst: &mut CudaStorage,
320        dst_l: &Layout,
321        srcs: [(Src<'_, CudaStorage>, &Layout); N],
322    ) -> Result<()> {
323        let _ = (dst, dst_l, srcs);
324        bail!("no aliased cpu implementation for {}", self.name())
325    }
326
327    fn metal_fwd(
328        &self,
329        dst: &mut MetalStorage,
330        dst_l: &Layout,
331        srcs: [(&MetalStorage, &Layout); N],
332    ) -> Result<()> {
333        let _ = (dst, dst_l, srcs);
334        bail!("no metal implementation for {}", self.name())
335    }
336
337    fn metal_fwd_aliased(
338        &self,
339        dst: &mut MetalStorage,
340        dst_l: &Layout,
341        srcs: [(Src<'_, MetalStorage>, &Layout); N],
342    ) -> Result<()> {
343        let _ = (dst, dst_l, srcs);
344        bail!("no aliased metal implementation for {}", self.name())
345    }
346}
347
348#[derive(Debug)]
349pub enum Src<'a, S> {
350    Distinct(&'a S),
351    Aliased(LayoutRelation),
352}
353
354impl<S> Copy for Src<'_, S> {}
355
356impl<S> Clone for Src<'_, S> {
357    fn clone(&self) -> Self {
358        *self
359    }
360}
361
362// If all sources are distinct we return the entire array of tensors without the `Src` wrapper.
363pub(crate) fn all_distinct<'a, B, const N: usize>(
364    srcs: &[(Src<'a, B>, &'a Layout); N],
365) -> Option<[(&'a B, &'a Layout); N]> {
366    srcs.iter()
367        .all(|(s, _)| matches!(s, Src::Distinct(_)))
368        .then(|| {
369            std::array::from_fn(|i| match srcs[i] {
370                (Src::Distinct(s), l) => (s, l),
371                _ => unreachable!("checked immediately above"),
372            })
373        })
374}
375
376/// Indicates which indices a kernel reads, relative to the destination indices it writes.
377///
378/// When used with [`crate::LayoutRelation`] we can describe safe access patterns.
379/// For example `Elementwise` is safe to use with both `LayoutRelation::Identical` and `LayoutRelation::Disjoint`,
380/// while `Arbitrary` is only guaranteed to be safe with `LayoutRelation::Disjoint`.
381#[derive(Copy, Clone, PartialEq, Eq, Debug)]
382pub enum AccessPattern {
383    /// Reads only source index `i` when writing destination index `i`.
384    Elementwise,
385    /// Reads arbitrary source indices.
386    Arbitrary,
387}
388
389impl AccessPattern {
390    fn supports(self, rel: LayoutRelation) -> bool {
391        matches!(
392            (self, rel),
393            (
394                AccessPattern::Elementwise,
395                LayoutRelation::Identical | LayoutRelation::Disjoint
396            ) | (AccessPattern::Arbitrary, LayoutRelation::Disjoint)
397        )
398    }
399}
400
401macro_rules! forward_op1 {
402    ($fwd:ident, $fwd_aliased:ident, $storage:ty) => {
403        fn $fwd(
404            &self,
405            dst: &mut $storage,
406            dl: &Layout,
407            _: [(&$storage, &Layout); 0],
408        ) -> Result<()> {
409            InplaceOp1::$fwd(self, dst, dl)
410        }
411
412        fn $fwd_aliased(
413            &self,
414            dst: &mut $storage,
415            dl: &Layout,
416            _: [(Src<'_, $storage>, &Layout); 0],
417        ) -> Result<()> {
418            InplaceOp1::$fwd(self, dst, dl)
419        }
420    };
421}
422
423impl<C: InplaceOp1> InplaceOpN<0> for C {
424    fn name(&self) -> &'static str {
425        InplaceOp1::name(self)
426    }
427
428    forward_op1!(cpu_fwd, cpu_fwd_aliased, CpuStorage);
429    forward_op1!(cuda_fwd, cuda_fwd_aliased, CudaStorage);
430    forward_op1!(metal_fwd, metal_fwd_aliased, MetalStorage);
431}
432
433macro_rules! forward_op2 {
434    ($fwd:ident, $fwd_aliased:ident, $storage:ty) => {
435        fn $fwd(
436            &self,
437            dst: &mut $storage,
438            dl: &Layout,
439            srcs: [(&$storage, &Layout); 1],
440        ) -> Result<()> {
441            let [(s, sl)] = srcs;
442            InplaceOp2::$fwd(self, dst, dl, s, sl)
443        }
444
445        fn $fwd_aliased(
446            &self,
447            dst: &mut $storage,
448            dl: &Layout,
449            srcs: [(Src<'_, $storage>, &Layout); 1],
450        ) -> Result<()> {
451            match srcs {
452                [(Src::Distinct(s), sl)] => InplaceOp2::$fwd(self, dst, dl, s, sl),
453                _ => bail!(
454                    "{}: aliased input requires migrating to InplaceOpN",
455                    self.name()
456                ),
457            }
458        }
459    };
460}
461
462impl<C: InplaceOp2> InplaceOpN<1> for C {
463    fn name(&self) -> &'static str {
464        InplaceOp2::name(self)
465    }
466
467    forward_op2!(cpu_fwd, cpu_fwd_aliased, CpuStorage);
468    forward_op2!(cuda_fwd, cuda_fwd_aliased, CudaStorage);
469    forward_op2!(metal_fwd, metal_fwd_aliased, MetalStorage);
470}
471
472pub trait InplaceOp2 {
473    fn name(&self) -> &'static str;
474
475    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
476    /// offsets etc so the associated layout should be used to access it.
477    fn cpu_fwd(&self, s1: &mut CpuStorage, l1: &Layout, s2: &CpuStorage, l2: &Layout)
478        -> Result<()>;
479
480    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
481    /// offsets etc so the associated layout should be used to access it.
482    fn cuda_fwd(
483        &self,
484        s1: &mut CudaStorage,
485        l1: &Layout,
486        s2: &CudaStorage,
487        l2: &Layout,
488    ) -> Result<()> {
489        _ = (s1, l1, s2, l2);
490        Err(crate::Error::Cuda(
491            format!("no cuda implementation for {}", self.name()).into(),
492        ))
493    }
494
495    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
496    /// offsets etc so the associated layout should be used to access it.
497    fn metal_fwd(
498        &self,
499        s1: &mut MetalStorage,
500        l1: &Layout,
501        s2: &MetalStorage,
502        l2: &Layout,
503    ) -> Result<()> {
504        _ = (s1, l1, s2, l2);
505        Err(crate::Error::Metal(
506            format!("no metal implementation for {}", self.name()).into(),
507        ))
508    }
509}
510
511macro_rules! forward_op3 {
512    ($fwd:ident, $fwd_aliased:ident, $storage:ty) => {
513        fn $fwd(
514            &self,
515            dst: &mut $storage,
516            dl: &Layout,
517            srcs: [(&$storage, &Layout); 2],
518        ) -> Result<()> {
519            let [(s1, l1), (s2, l2)] = srcs;
520            InplaceOp3::$fwd(self, dst, dl, s1, l1, s2, l2)
521        }
522
523        fn $fwd_aliased(
524            &self,
525            dst: &mut $storage,
526            dl: &Layout,
527            srcs: [(Src<'_, $storage>, &Layout); 2],
528        ) -> Result<()> {
529            match srcs {
530                [(Src::Distinct(s1), l1), (Src::Distinct(s2), l2)] => {
531                    InplaceOp3::$fwd(self, dst, dl, s1, l1, s2, l2)
532                }
533                _ => bail!(
534                    "{}: aliased input requires migrating to InplaceOpN",
535                    self.name()
536                ),
537            }
538        }
539    };
540}
541
542impl<C: InplaceOp3> InplaceOpN<2> for C {
543    fn name(&self) -> &'static str {
544        InplaceOp3::name(self)
545    }
546
547    forward_op3!(cpu_fwd, cpu_fwd_aliased, CpuStorage);
548    forward_op3!(cuda_fwd, cuda_fwd_aliased, CudaStorage);
549    forward_op3!(metal_fwd, metal_fwd_aliased, MetalStorage);
550}
551
552pub trait InplaceOp3 {
553    fn name(&self) -> &'static str;
554
555    /// The forward pass, as run on a cpu device. Note that the storage can use arbitrary strides,
556    /// offsets etc so the associated layout should be used to access it.
557    fn cpu_fwd(
558        &self,
559        s1: &mut CpuStorage,
560        l1: &Layout,
561        s2: &CpuStorage,
562        l2: &Layout,
563        s3: &CpuStorage,
564        l3: &Layout,
565    ) -> Result<()>;
566
567    /// The forward pass, as run on a gpu device. Note that the storage can use arbitrary strides,
568    /// offsets etc so the associated layout should be used to access it.
569    fn cuda_fwd(
570        &self,
571        _: &mut CudaStorage,
572        _: &Layout,
573        _: &CudaStorage,
574        _: &Layout,
575        _: &CudaStorage,
576        _: &Layout,
577    ) -> Result<()> {
578        Err(crate::Error::Cuda(
579            format!("no cuda implementation for {}", self.name()).into(),
580        ))
581    }
582
583    /// The forward pass, as run on a metal gpu device. Note that the storage can use arbitrary strides,
584    /// offsets etc so the associated layout should be used to access it.
585    fn metal_fwd(
586        &self,
587        _: &mut MetalStorage,
588        _: &Layout,
589        _: &MetalStorage,
590        _: &Layout,
591        _: &MetalStorage,
592        _: &Layout,
593    ) -> Result<()> {
594        Err(crate::Error::Metal(
595            format!("no metal implementation for {}", self.name()).into(),
596        ))
597    }
598}
599
600impl Tensor {
601    /// Applies a custom op in-place for the `self` tensor.
602    ///
603    /// Tensors sharing underlying storage with `self` are classified and passed as [`Src::Aliased`].
604    /// Separate tensors are locked and passed as [`Src::Distinct`].
605    fn inplace_op<const N: usize, C: InplaceOpN<N>>(&self, srcs: [&Self; N], c: &C) -> Result<()> {
606        let name = c.name();
607
608        // Ensure writes cannot collide with themselves
609        if self.layout().has_internal_overlap() {
610            bail!("{name}: dst has repeated elements (zero-stride). Can not write in-place")
611        }
612
613        // Classify srcs wrt dst
614        let access = c.src_access_pattern();
615        let mut rels: [Option<LayoutRelation>; N] = [None; N];
616        for i in 0..N {
617            if !self.same_storage(srcs[i]) {
618                continue;
619            }
620            let rel = Layout::relation(self.layout(), srcs[i].layout());
621            match access {
622                Some(a) if a.supports(rel) => rels[i] = Some(rel),
623                Some(a) => bail!(
624                    "src {i} shares storage with dst ({rel:?}), which is not supported for the access pattern of `{name}` ({a:?})."
625                ),
626                None => bail!(
627                    "src {i} shares storage with dst, and `{name}` does not support aliased operands."
628                ),
629            }
630        }
631
632        // Acquire locks in order sorted by `Tensor::storage_key`.
633        // Avoids deadlock from cycle of waiting locks.
634        let dst_key = self.storage_key();
635
636        let mut order: [usize; N] = std::array::from_fn(|i| i);
637        order.sort_unstable_by_key(|&i| srcs[i].storage_key());
638
639        let mut guards: [Option<_>; N] = std::array::from_fn(|_| None);
640        let mut dst: Option<_> = None;
641
642        for &i in order.iter() {
643            if rels[i].is_some() {
644                continue; // Aliased. Read through `dst`
645            }
646            let key = srcs[i].storage_key();
647            if key > dst_key && dst.is_none() {
648                dst = Some(self.storage_mut());
649            }
650            // Two sources sharing the same allocation, but distinct from dst.
651            // `read_recursive` allows for shared read access
652            guards[i] = Some(srcs[i].storage());
653        }
654        // If `dst` is not yet set we acquire it from `self` now.
655        let mut dst = match dst {
656            Some(g) => g,
657            None => self.storage_mut(),
658        };
659
660        let operands: [(Src<'_, Storage>, &Layout); N] = std::array::from_fn(|i| {
661            let s = match (&guards[i], rels[i]) {
662                (Some(g), None) => Src::Distinct(&**g),
663                (None, Some(rel)) => Src::Aliased(rel),
664                _ => unreachable!(
665                    "Source is either distinct or aliased. Other match patterns should be impossible"
666                ),
667            };
668            (s, srcs[i].layout())
669        });
670
671        dst.inplace_op(self.layout(), operands, c)
672    }
673
674    /// Applies a unary custom op in place.
675    pub fn inplace_op1<C: InplaceOp1>(&self, c: &C) -> Result<()> {
676        self.inplace_op([], c)
677    }
678
679    /// Applies a binary custom op in place (for the first tensor).
680    pub fn inplace_op2<C: InplaceOpN<1>>(&self, rhs: &Self, c: &C) -> Result<()> {
681        self.inplace_op([rhs], c)
682    }
683
684    /// Applies a ternary custom op in place (for the first tensor).
685    pub fn inplace_op3<C: InplaceOpN<2>>(&self, t2: &Self, t3: &Self, c: &C) -> Result<()> {
686        self.inplace_op([t2, t3], c)
687    }
688}