teeny_core/graph/mod.rs
1/*
2 * Copyright (c) 2026 Teenygrad.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use alloc::{collections::BTreeMap, rc::Rc, string::String, sync::Arc, vec, vec::Vec};
18use core::{any::Any, cell::RefCell};
19
20use crate::{
21 dtype::{Dtype, Float, RankedTensor, Tensor},
22 nn::{
23 Layer,
24 activation::{
25 elu::{Celu, Elu, Selu},
26 gelu::{Gelu, Mish},
27 hard::{Hardshrink, Hardsigmoid, Hardswish, Hardtanh, Relu6},
28 misc::{LeakyRelu, Softplus, Softshrink, Softsign, Threshold},
29 relu::Relu,
30 sigmoid::{Logsigmoid, Sigmoid, Silu},
31 softmax::Softmax,
32 tanh::{Tanh, Tanhshrink},
33 },
34 batchnorm::{BatchNorm1d, BatchNorm2d, BatchNorm3d},
35 conv1d::Conv1d,
36 conv2d::Conv2d,
37 conv3d::Conv3d,
38 flatten::Flatten,
39 groupnorm::GroupNorm,
40 instancenorm::{InstanceNorm1d, InstanceNorm2d, InstanceNorm3d},
41 layernorm::LayerNorm,
42 linear::Linear,
43 pad::{
44 CircularPad1d, CircularPad2d, CircularPad3d, ConstantPad1d, ConstantPad2d,
45 ConstantPad3d, ReflectionPad1d, ReflectionPad2d, ReflectionPad3d, ReplicationPad1d,
46 ReplicationPad2d, ReplicationPad3d,
47 },
48 pool::{
49 AvgPool1d, AvgPool2d, AvgPool3d, LpPool1d, LpPool2d, LpPool3d, MaxPool1d, MaxPool2d,
50 MaxPool3d,
51 },
52 rmsnorm::RmsNorm,
53 },
54};
55
56/// Graph-to-FXGraph lowering.
57pub mod compiler;
58
59// ---------------------------------------------------------------------------
60// Shape — dynamic tensor shape used throughout the graph IR
61// ---------------------------------------------------------------------------
62
63/// A dynamic shape vector. Each element is either a known size (`Some(n)`) or a
64/// dynamic/unknown dimension (`None`), e.g. a batch axis whose size is determined
65/// at runtime.
66pub type Shape = Vec<Option<usize>>;
67
68// ---------------------------------------------------------------------------
69// Runtime dtype tag — used in the graph since D is erased at the node level
70// ---------------------------------------------------------------------------
71
72/// Runtime dtype tag: the graph-level (type-erased) representation of a tensor's dtype,
73/// mirroring `dtype::Dtype`'s implementors.
74#[derive(Copy, Clone, Debug, PartialEq, Eq)]
75pub enum DtypeRepr {
76 /// `bool`.
77 Bool,
78 /// Signed 8-bit integer.
79 I8,
80 /// Signed 16-bit integer.
81 I16,
82 /// Signed 32-bit integer.
83 I32,
84 /// Signed 64-bit integer.
85 I64,
86 /// Unsigned 8-bit integer.
87 U8,
88 /// Unsigned 16-bit integer.
89 U16,
90 /// Unsigned 32-bit integer.
91 U32,
92 /// Unsigned 64-bit integer.
93 U64,
94 /// 16-bit float.
95 F16,
96 /// `bfloat16`.
97 BF16,
98 /// 32-bit float.
99 F32,
100 /// 64-bit float.
101 F64,
102}
103
104// ---------------------------------------------------------------------------
105// Graph IR
106// ---------------------------------------------------------------------------
107
108/// Trait implemented by user-defined ops.
109pub trait CustomOp: Any + Send + Sync {
110 /// Identifier used in error messages and debug output.
111 fn name(&self) -> &str;
112
113 /// Compute the output shape given the shapes of all input tensors in order.
114 fn infer_output_shape(&self, input_shapes: &[&Shape]) -> Shape;
115
116 /// Expose `self` as `&dyn Any` so the custom lowering can downcast to the
117 /// concrete op type. Implement as `fn as_any(&self) -> &dyn Any { self }`.
118 fn as_any(&self) -> &dyn Any;
119
120 /// Return kernel lowering info so `TritonLowering` can compile this op
121 /// without a project-specific middleware. Return `None` to keep the
122 /// existing middleware / error behaviour.
123 ///
124 /// Tuple layout: `(name, kernel_source, entry_point_name, runtime_op)`.
125 /// `entry_point_name` is the PTX symbol name, conventionally `"{name}_entry_point"`.
126 fn lower(&self) -> Option<(String, String, String, Arc<dyn crate::model::RuntimeOp>)> {
127 None
128 }
129
130 /// Return the backward kernel source for this op (used in training mode).
131 /// Return an empty string if this op has no backward pass.
132 fn lower_backward_source(&self) -> String {
133 String::new()
134 }
135}
136
137/// Wrapper around `Arc<dyn CustomOp>` that implements `Debug` for [`Op`].
138#[derive(Clone)]
139pub struct CustomData(pub Arc<dyn CustomOp>);
140
141impl CustomData {
142 /// Wraps a [`CustomOp`] implementation.
143 pub fn new<T: CustomOp>(op: T) -> Self {
144 Self(Arc::new(op))
145 }
146
147 /// The wrapped op's name.
148 pub fn name(&self) -> &str {
149 self.0.name()
150 }
151
152 /// The wrapped op's inferred output shape.
153 pub fn infer_output_shape(&self, input_shapes: &[&Shape]) -> Shape {
154 self.0.infer_output_shape(input_shapes)
155 }
156
157 /// Downcast to a concrete op type via [`CustomOp::as_any`].
158 pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
159 self.0.as_any().downcast_ref::<T>()
160 }
161}
162
163impl core::fmt::Debug for CustomData {
164 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165 write!(f, "Custom({})", self.0.name())
166 }
167}
168
169/// A single computational-graph operation. Each variant corresponds to one `nn` layer or
170/// primitive op; variant fields are that op's configuration (mirroring the corresponding
171/// `nn::*` layer struct's fields).
172#[derive(Debug, Clone)]
173pub enum Op {
174 /// Model input placeholder.
175 Input,
176
177 // --- Linear / MLP ---
178 /// Fully-connected layer (see `nn::linear::Linear`).
179 Linear {
180 /// Size of the last input dimension.
181 in_features: usize,
182 /// Size of the last output dimension.
183 out_features: usize,
184 /// Whether a learned bias is added.
185 has_bias: bool,
186 },
187 /// Flattens all spatial dimensions into a single feature vector (see `nn::flatten::Flatten`).
188 Flatten,
189
190 // --- Normalisation ---
191 /// 1-D batch normalization (see `nn::batchnorm::BatchNorm1d`).
192 BatchNorm1d {
193 /// Number of channels/features.
194 num_features: usize,
195 /// Numerical stability constant.
196 eps: f64,
197 /// Running-stats exponential moving average weight.
198 momentum: f64,
199 /// Whether to learn per-channel scale/shift parameters.
200 affine: bool,
201 /// Whether to maintain running mean/variance across batches.
202 track_running_stats: bool,
203 },
204 /// 2-D batch normalization (see `nn::batchnorm::BatchNorm2d`).
205 BatchNorm2d {
206 /// Number of channels/features.
207 num_features: usize,
208 /// Numerical stability constant.
209 eps: f64,
210 /// Running-stats exponential moving average weight.
211 momentum: f64,
212 /// Whether to learn per-channel scale/shift parameters.
213 affine: bool,
214 /// Whether to maintain running mean/variance across batches.
215 track_running_stats: bool,
216 },
217 /// 3-D batch normalization (see `nn::batchnorm::BatchNorm3d`).
218 BatchNorm3d {
219 /// Number of channels/features.
220 num_features: usize,
221 /// Numerical stability constant.
222 eps: f64,
223 /// Running-stats exponential moving average weight.
224 momentum: f64,
225 /// Whether to learn per-channel scale/shift parameters.
226 affine: bool,
227 /// Whether to maintain running mean/variance across batches.
228 track_running_stats: bool,
229 },
230 /// Layer normalization (see `nn::layernorm::LayerNorm`).
231 LayerNorm {
232 /// Shape of the trailing axes to normalize over.
233 normalized_shape: alloc::vec::Vec<usize>,
234 /// Numerical stability constant.
235 eps: f64,
236 /// Whether to learn per-channel scale/shift parameters.
237 affine: bool,
238 },
239 /// RMS normalization (see `nn::rmsnorm::RmsNorm`).
240 RmsNorm {
241 /// Shape of the trailing axes to normalize over.
242 normalized_shape: alloc::vec::Vec<usize>,
243 /// Numerical stability constant.
244 eps: f64,
245 /// Whether to learn per-channel scale/shift parameters.
246 affine: bool,
247 },
248 /// Group normalization (see `nn::groupnorm::GroupNorm`).
249 GroupNorm {
250 /// Number of groups.
251 num_groups: usize,
252 /// Number of channels.
253 num_channels: usize,
254 /// Numerical stability constant.
255 eps: f64,
256 /// Whether to learn per-channel scale/shift parameters.
257 affine: bool,
258 },
259 /// 1-D instance normalization (see `nn::instancenorm::InstanceNorm1d`).
260 InstanceNorm1d {
261 /// Number of channels/features.
262 num_features: usize,
263 /// Numerical stability constant.
264 eps: f64,
265 /// Running-stats exponential moving average weight.
266 momentum: f64,
267 /// Whether to learn per-channel scale/shift parameters.
268 affine: bool,
269 /// Whether to maintain running mean/variance across batches.
270 track_running_stats: bool,
271 },
272 /// 2-D instance normalization (see `nn::instancenorm::InstanceNorm2d`).
273 InstanceNorm2d {
274 /// Number of channels/features.
275 num_features: usize,
276 /// Numerical stability constant.
277 eps: f64,
278 /// Running-stats exponential moving average weight.
279 momentum: f64,
280 /// Whether to learn per-channel scale/shift parameters.
281 affine: bool,
282 /// Whether to maintain running mean/variance across batches.
283 track_running_stats: bool,
284 },
285 /// 3-D instance normalization (see `nn::instancenorm::InstanceNorm3d`).
286 InstanceNorm3d {
287 /// Number of channels/features.
288 num_features: usize,
289 /// Numerical stability constant.
290 eps: f64,
291 /// Running-stats exponential moving average weight.
292 momentum: f64,
293 /// Whether to learn per-channel scale/shift parameters.
294 affine: bool,
295 /// Whether to maintain running mean/variance across batches.
296 track_running_stats: bool,
297 },
298
299 // --- Convolution ---
300 /// 1-D convolution (see `nn::conv1d::Conv1d`).
301 Conv1d {
302 /// Number of input channels.
303 in_channels: usize,
304 /// Number of output channels.
305 out_channels: usize,
306 /// Convolution/pooling kernel length.
307 kernel_l: usize,
308 /// Stride between kernel applications.
309 stride: usize,
310 /// Zero-padding applied to the input.
311 padding: usize,
312 /// Whether a learned bias is added.
313 has_bias: bool,
314 },
315 /// 2-D convolution (see `nn::conv2d::Conv2d`).
316 Conv2d {
317 /// Number of input channels.
318 in_channels: usize,
319 /// Number of output channels.
320 out_channels: usize,
321 /// Convolution/pooling kernel height.
322 kernel_h: usize,
323 /// Convolution/pooling kernel width.
324 kernel_w: usize,
325 /// Vertical stride.
326 stride_h: usize,
327 /// Horizontal stride.
328 stride_w: usize,
329 /// Vertical zero-padding.
330 padding_h: usize,
331 /// Horizontal zero-padding.
332 padding_w: usize,
333 /// Number of blocked/grouped connections (1 = standard).
334 groups: usize,
335 /// Whether a learned bias is added.
336 has_bias: bool,
337 },
338 /// 3-D convolution (see `nn::conv3d::Conv3d`).
339 Conv3d {
340 /// Number of input channels.
341 in_channels: usize,
342 /// Number of output channels.
343 out_channels: usize,
344 /// Convolution/pooling kernel depth.
345 kernel_d: usize,
346 /// Convolution/pooling kernel height.
347 kernel_h: usize,
348 /// Convolution/pooling kernel width.
349 kernel_w: usize,
350 /// Stride along the depth dimension.
351 stride_d: usize,
352 /// Vertical stride.
353 stride_h: usize,
354 /// Horizontal stride.
355 stride_w: usize,
356 /// Zero-padding along the depth dimension.
357 padding_d: usize,
358 /// Vertical zero-padding.
359 padding_h: usize,
360 /// Horizontal zero-padding.
361 padding_w: usize,
362 /// Whether a learned bias is added.
363 has_bias: bool,
364 },
365
366 /// Fused Conv2d + BatchNorm2d (inference-only) + SiLU forward.
367 ///
368 /// The BN parameters (scale, shift) are stored as precomputed affine
369 /// constants — not raw mean/var/gamma/beta. Produced by `Graph::optimise()`
370 /// when it detects the pattern `Conv2d(no bias) → BatchNorm2d → Silu`.
371 ///
372 /// `bn_eps` is carried forward only for reference; the BN affine constants
373 /// are passed at runtime via the `bn_scale` and `bn_shift` parameters.
374 Conv2dBnSilu {
375 /// Number of input channels.
376 in_channels: usize,
377 /// Number of output channels.
378 out_channels: usize,
379 /// Convolution/pooling kernel height.
380 kernel_h: usize,
381 /// Convolution/pooling kernel width.
382 kernel_w: usize,
383 /// Vertical stride.
384 stride_h: usize,
385 /// Horizontal stride.
386 stride_w: usize,
387 /// Vertical zero-padding.
388 padding_h: usize,
389 /// Horizontal zero-padding.
390 padding_w: usize,
391 /// Number of blocked/grouped connections (1 = standard).
392 groups: usize,
393 /// The fused BatchNorm's numerical stability constant (kept for reference only; the
394 /// affine constants are passed at runtime via `bn_scale`/`bn_shift`).
395 bn_eps: f64,
396 },
397
398 // --- Pooling ---
399 /// 1-D average pooling (see `nn::pool::AvgPool1d`).
400 AvgPool1d {
401 /// Convolution/pooling kernel length.
402 kernel_l: usize,
403 /// Stride between kernel applications.
404 stride: usize,
405 },
406 /// 2-D average pooling (see `nn::pool::AvgPool2d`).
407 AvgPool2d {
408 /// Convolution/pooling kernel height.
409 kernel_h: usize,
410 /// Convolution/pooling kernel width.
411 kernel_w: usize,
412 /// Vertical stride.
413 stride_h: usize,
414 /// Horizontal stride.
415 stride_w: usize,
416 },
417 /// 3-D average pooling (see `nn::pool::AvgPool3d`).
418 AvgPool3d {
419 /// Convolution/pooling kernel depth.
420 kernel_d: usize,
421 /// Convolution/pooling kernel height.
422 kernel_h: usize,
423 /// Convolution/pooling kernel width.
424 kernel_w: usize,
425 /// Stride along the depth dimension.
426 stride_d: usize,
427 /// Vertical stride.
428 stride_h: usize,
429 /// Horizontal stride.
430 stride_w: usize,
431 },
432 /// 1-D max pooling (see `nn::pool::MaxPool1d`).
433 MaxPool1d {
434 /// Convolution/pooling kernel length.
435 kernel_l: usize,
436 /// Stride between kernel applications.
437 stride: usize,
438 },
439 /// 2-D max pooling (see `nn::pool::MaxPool2d`).
440 MaxPool2d {
441 /// Convolution/pooling kernel height.
442 kernel_h: usize,
443 /// Convolution/pooling kernel width.
444 kernel_w: usize,
445 /// Vertical stride.
446 stride_h: usize,
447 /// Horizontal stride.
448 stride_w: usize,
449 /// Vertical padding.
450 pad_h: usize,
451 /// Horizontal padding.
452 pad_w: usize,
453 },
454 /// 3-D max pooling (see `nn::pool::MaxPool3d`).
455 MaxPool3d {
456 /// Convolution/pooling kernel depth.
457 kernel_d: usize,
458 /// Convolution/pooling kernel height.
459 kernel_h: usize,
460 /// Convolution/pooling kernel width.
461 kernel_w: usize,
462 /// Stride along the depth dimension.
463 stride_d: usize,
464 /// Vertical stride.
465 stride_h: usize,
466 /// Horizontal stride.
467 stride_w: usize,
468 },
469 /// 1-D power-average (Lp) pooling (see `nn::pool::LpPool1d`).
470 LpPool1d {
471 /// Convolution/pooling kernel length.
472 kernel_l: usize,
473 /// Stride between kernel applications.
474 stride: usize,
475 /// The `p` in the p-norm.
476 p: f64,
477 },
478 /// 2-D power-average (Lp) pooling (see `nn::pool::LpPool2d`).
479 LpPool2d {
480 /// Convolution/pooling kernel height.
481 kernel_h: usize,
482 /// Convolution/pooling kernel width.
483 kernel_w: usize,
484 /// Vertical stride.
485 stride_h: usize,
486 /// Horizontal stride.
487 stride_w: usize,
488 /// The `p` in the p-norm.
489 p: f64,
490 },
491 /// 3-D power-average (Lp) pooling (see `nn::pool::LpPool3d`).
492 LpPool3d {
493 /// Convolution/pooling kernel depth.
494 kernel_d: usize,
495 /// Convolution/pooling kernel height.
496 kernel_h: usize,
497 /// Convolution/pooling kernel width.
498 kernel_w: usize,
499 /// Stride along the depth dimension.
500 stride_d: usize,
501 /// Vertical stride.
502 stride_h: usize,
503 /// Horizontal stride.
504 stride_w: usize,
505 /// The `p` in the p-norm.
506 p: f64,
507 },
508
509 // --- Upsample ---
510 /// Nearest-neighbour 2-D upsampling.
511 /// Output shape: `[N, C, H * scale_h, W * scale_w]`.
512 UpsampleNearest2d {
513 /// Vertical upsampling scale factor.
514 scale_h: usize,
515 /// Horizontal upsampling scale factor.
516 scale_w: usize,
517 },
518
519 // --- Padding ---
520 /// 1-D constant padding (see `nn::pad::ConstantPad1d`).
521 ConstantPad1d {
522 /// Left padding.
523 pad_left: usize,
524 /// Right padding.
525 pad_right: usize,
526 /// The constant fill value.
527 value: f64,
528 },
529 /// 2-D constant padding (see `nn::pad::ConstantPad2d`).
530 ConstantPad2d {
531 /// Left padding.
532 pad_l: usize,
533 /// Right padding.
534 pad_r: usize,
535 /// Top padding.
536 pad_t: usize,
537 /// Bottom padding.
538 pad_b: usize,
539 /// The constant fill value.
540 value: f64,
541 },
542 /// 3-D constant padding (see `nn::pad::ConstantPad3d`).
543 ConstantPad3d {
544 /// Padding before the depth dimension.
545 pad_d1: usize,
546 /// Padding after the depth dimension.
547 pad_d2: usize,
548 /// Padding before the height dimension.
549 pad_h1: usize,
550 /// Padding after the height dimension.
551 pad_h2: usize,
552 /// Padding before the width dimension.
553 pad_w1: usize,
554 /// Padding after the width dimension.
555 pad_w2: usize,
556 /// The constant fill value.
557 value: f64,
558 },
559 /// 1-D reflection padding (see `nn::pad::ReflectionPad1d`).
560 ReflectionPad1d {
561 /// Left padding.
562 pad_left: usize,
563 /// Right padding.
564 pad_right: usize,
565 },
566 /// 2-D reflection padding (see `nn::pad::ReflectionPad2d`).
567 ReflectionPad2d {
568 /// Left padding.
569 pad_l: usize,
570 /// Right padding.
571 pad_r: usize,
572 /// Top padding.
573 pad_t: usize,
574 /// Bottom padding.
575 pad_b: usize,
576 },
577 /// 3-D reflection padding (see `nn::pad::ReflectionPad3d`).
578 ReflectionPad3d {
579 /// Padding before the depth dimension.
580 pad_d1: usize,
581 /// Padding after the depth dimension.
582 pad_d2: usize,
583 /// Padding before the height dimension.
584 pad_h1: usize,
585 /// Padding after the height dimension.
586 pad_h2: usize,
587 /// Padding before the width dimension.
588 pad_w1: usize,
589 /// Padding after the width dimension.
590 pad_w2: usize,
591 },
592 /// 1-D replication padding (see `nn::pad::ReplicationPad1d`).
593 ReplicationPad1d {
594 /// Left padding.
595 pad_left: usize,
596 /// Right padding.
597 pad_right: usize,
598 },
599 /// 2-D replication padding (see `nn::pad::ReplicationPad2d`).
600 ReplicationPad2d {
601 /// Left padding.
602 pad_l: usize,
603 /// Right padding.
604 pad_r: usize,
605 /// Top padding.
606 pad_t: usize,
607 /// Bottom padding.
608 pad_b: usize,
609 },
610 /// 3-D replication padding (see `nn::pad::ReplicationPad3d`).
611 ReplicationPad3d {
612 /// Padding before the depth dimension.
613 pad_d1: usize,
614 /// Padding after the depth dimension.
615 pad_d2: usize,
616 /// Padding before the height dimension.
617 pad_h1: usize,
618 /// Padding after the height dimension.
619 pad_h2: usize,
620 /// Padding before the width dimension.
621 pad_w1: usize,
622 /// Padding after the width dimension.
623 pad_w2: usize,
624 },
625 /// 1-D circular padding (see `nn::pad::CircularPad1d`).
626 CircularPad1d {
627 /// Left padding.
628 pad_left: usize,
629 /// Right padding.
630 pad_right: usize,
631 },
632 /// 2-D circular padding (see `nn::pad::CircularPad2d`).
633 CircularPad2d {
634 /// Left padding.
635 pad_l: usize,
636 /// Right padding.
637 pad_r: usize,
638 /// Top padding.
639 pad_t: usize,
640 /// Bottom padding.
641 pad_b: usize,
642 },
643 /// 3-D circular padding (see `nn::pad::CircularPad3d`).
644 CircularPad3d {
645 /// Padding before the depth dimension.
646 pad_d1: usize,
647 /// Padding after the depth dimension.
648 pad_d2: usize,
649 /// Padding before the height dimension.
650 pad_h1: usize,
651 /// Padding after the height dimension.
652 pad_h2: usize,
653 /// Padding before the width dimension.
654 pad_w1: usize,
655 /// Padding after the width dimension.
656 pad_w2: usize,
657 },
658
659 // --- Activation ---
660 /// ReLU activation (see `nn::activation::relu::Relu`).
661 Relu,
662 /// ELU activation (see `nn::activation::elu::Elu`).
663 Elu {
664 /// The `alpha` parameter.
665 alpha: f64,
666 },
667 /// SELU activation (see `nn::activation::elu::Selu`).
668 Selu,
669 /// CELU activation (see `nn::activation::elu::Celu`).
670 Celu {
671 /// The `alpha` parameter.
672 alpha: f64,
673 },
674 /// GELU activation (see `nn::activation::gelu::Gelu`).
675 Gelu,
676 /// Mish activation (see `nn::activation::gelu::Mish`).
677 Mish,
678 /// Hardtanh activation (see `nn::activation::hard::Hardtanh`).
679 Hardtanh {
680 /// The lower clamp bound.
681 min_val: f64,
682 /// The upper clamp bound.
683 max_val: f64,
684 },
685 /// ReLU6 activation (see `nn::activation::hard::Relu6`).
686 Relu6,
687 /// Hard-sigmoid activation (see `nn::activation::hard::Hardsigmoid`).
688 Hardsigmoid,
689 /// Hard-swish activation (see `nn::activation::hard::Hardswish`).
690 Hardswish,
691 /// Hardshrink activation (see `nn::activation::hard::Hardshrink`).
692 Hardshrink {
693 /// The shrinkage threshold.
694 lambda: f64,
695 },
696 /// Leaky ReLU activation (see `nn::activation::misc::LeakyRelu`).
697 LeakyRelu {
698 /// The slope applied to negative inputs.
699 negative_slope: f64,
700 },
701 /// Threshold activation (see `nn::activation::misc::Threshold`).
702 Threshold {
703 /// The threshold value.
704 threshold: f64,
705 /// The constant fill value.
706 value: f64,
707 },
708 /// Softsign activation (see `nn::activation::misc::Softsign`).
709 Softsign,
710 /// Softshrink activation (see `nn::activation::misc::Softshrink`).
711 Softshrink {
712 /// The shrinkage threshold.
713 lambda: f64,
714 },
715 /// Softplus activation (see `nn::activation::misc::Softplus`).
716 Softplus {
717 /// The `beta` parameter.
718 beta: f64,
719 /// The threshold value.
720 threshold: f64,
721 },
722 /// Sigmoid activation (see `nn::activation::sigmoid::Sigmoid`).
723 Sigmoid,
724 /// SiLU/Swish activation (see `nn::activation::sigmoid::Silu`).
725 Silu,
726 /// Log-sigmoid activation (see `nn::activation::sigmoid::Logsigmoid`).
727 Logsigmoid,
728 /// Tanh activation (see `nn::activation::tanh::Tanh`).
729 Tanh,
730 /// Tanhshrink activation (see `nn::activation::tanh::Tanhshrink`).
731 Tanhshrink,
732 /// Softmax activation (see `nn::activation::softmax::Softmax`).
733 Softmax {
734 /// The dimension to operate along.
735 dim: usize,
736 },
737
738 // --- Attention ---
739 /// Multi-head self-attention with Flash Attention 2 and position encoding.
740 /// Represents the full `Attention.forward()` in PSABlock:
741 /// qkv conv → FA2 → pe depthwise conv → proj conv → residual add.
742 /// Input/output shape: `[N, c, H, W]`.
743 Attention {
744 /// Number of channels.
745 c: usize,
746 /// Number of attention heads.
747 num_heads: usize,
748 /// Per-head key/query dimension.
749 key_dim: usize,
750 },
751
752 // --- Tensor structural ops ---
753 /// Element-wise addition of two tensors with identical shapes.
754 Add,
755 /// Extract one contiguous channel slice from a 4-D NCHW tensor.
756 /// Output shape: `[N, chunk_c, H, W]`.
757 ChannelChunk {
758 /// Total number of channels across all inputs/outputs.
759 c_total: usize,
760 /// Number of channels in this chunk.
761 chunk_c: usize,
762 /// Channel offset of this chunk within the total.
763 chunk_offset: usize,
764 },
765 /// Concatenate N 4-D NCHW tensors along the channel dimension.
766 /// Output shape: `[N, c_total, H, W]`.
767 ChannelCat {
768 /// Total number of channels across all inputs/outputs.
769 c_total: usize,
770 },
771 /// Adds a (C,) bias vector to a (B, C, H, W) feature map — NC layout (N=B*H*W).
772 /// Output shape equals input shape.
773 ChannelBiasAdd {
774 /// Number of channels.
775 c: usize,
776 },
777
778 /// User-defined op. Shape and dtype must be provided via [`Graph::add_node`]
779 /// or [`SymTensor::record_custom`] — the base system cannot infer them.
780 Custom {
781 /// The wrapped user-defined op.
782 data: CustomData,
783 },
784
785 // -----------------------------------------------------------------------
786 // ONNX-sourced ops — added to let the ONNX loader build a complete graph.
787 // Triton/CPU lowering is not yet implemented for these variants.
788 // -----------------------------------------------------------------------
789
790 // --- Element-wise unary math ---
791 /// Element-wise absolute value (ONNX `Abs`).
792 Abs,
793 /// Element-wise negation (ONNX `Neg`).
794 Neg,
795 /// Element-wise ceiling (ONNX `Ceil`).
796 Ceil,
797 /// Element-wise floor (ONNX `Floor`).
798 Floor,
799 /// Element-wise round-to-nearest-even (ONNX `Round`).
800 Round,
801 /// Element-wise square root (ONNX `Sqrt`).
802 Sqrt,
803 /// Element-wise reciprocal, `1/x` (ONNX `Reciprocal`).
804 Reciprocal,
805 /// Element-wise natural exponential (ONNX `Exp`).
806 Exp,
807 /// Element-wise natural logarithm (ONNX `Log`).
808 Log,
809 /// Element-wise error function (ONNX `Erf`).
810 Erf,
811 /// Element-wise sign (ONNX `Sign`).
812 Sign,
813 /// Element-wise NaN test (ONNX `IsNaN`).
814 IsNaN,
815 /// Element-wise infinity test (ONNX `IsInf`).
816 IsInf {
817 /// Whether to treat negative infinity as infinite.
818 detect_negative: bool,
819 /// Whether to treat positive infinity as infinite.
820 detect_positive: bool,
821 },
822 /// Element-wise logical NOT (ONNX `Not`).
823 Not,
824 /// Element-wise bitwise NOT (ONNX `BitwiseNot`).
825 BitwiseNot,
826 /// Element-wise sine (ONNX `Sin`).
827 Sin,
828 /// Element-wise cosine (ONNX `Cos`).
829 Cos,
830 /// Element-wise tangent (ONNX `Tan`).
831 Tan,
832 /// Element-wise arcsine (ONNX `Asin`).
833 Asin,
834 /// Element-wise arccosine (ONNX `Acos`).
835 Acos,
836 /// Element-wise arctangent (ONNX `Atan`).
837 Atan,
838 /// Element-wise hyperbolic sine (ONNX `Sinh`).
839 Sinh,
840 /// Element-wise hyperbolic cosine (ONNX `Cosh`).
841 Cosh,
842 /// Element-wise inverse hyperbolic sine (ONNX `Asinh`).
843 Asinh,
844 /// Element-wise inverse hyperbolic cosine (ONNX `Acosh`).
845 Acosh,
846 /// Element-wise inverse hyperbolic tangent (ONNX `Atanh`).
847 Atanh,
848
849 // --- Element-wise binary / variadic ---
850 /// Element-wise multiplication (ONNX `Mul`).
851 Mul,
852 /// Element-wise subtraction (ONNX `Sub`).
853 Sub,
854 /// Element-wise division (ONNX `Div`).
855 Div,
856 /// Element-wise exponentiation (ONNX `Pow`).
857 Pow,
858 /// Element-wise modulo (ONNX `Mod`).
859 Mod {
860 /// Whether to use C-style (`fmod`) semantics instead of Python-style modulo.
861 fmod: bool,
862 },
863 /// Element-wise minimum across inputs (ONNX `Min`).
864 ElemMin,
865 /// Element-wise maximum across inputs (ONNX `Max`).
866 ElemMax,
867 /// Element-wise mean across inputs (ONNX `Mean`).
868 ElemMean,
869 /// Element-wise sum across inputs (ONNX `Sum`).
870 ElemSum,
871 /// Element-wise equality (ONNX `Equal`).
872 Equal,
873 /// Element-wise greater-than (ONNX `Greater`).
874 Greater,
875 /// Element-wise greater-than-or-equal (ONNX `GreaterOrEqual`).
876 GreaterOrEqual,
877 /// Element-wise less-than (ONNX `Less`).
878 Less,
879 /// Element-wise less-than-or-equal (ONNX `LessOrEqual`).
880 LessOrEqual,
881 /// Element-wise logical AND (ONNX `And`).
882 And,
883 /// Element-wise logical OR (ONNX `Or`).
884 Or,
885 /// Element-wise logical XOR (ONNX `Xor`).
886 Xor,
887 /// Element-wise bitwise AND (ONNX `BitwiseAnd`).
888 BitwiseAnd,
889 /// Element-wise bitwise OR (ONNX `BitwiseOr`).
890 BitwiseOr,
891 /// Element-wise bitwise XOR (ONNX `BitwiseXor`).
892 BitwiseXor,
893 /// Element-wise bit shift (ONNX `BitShift`).
894 BitShift {
895 /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
896 direction: alloc::string::String,
897 },
898
899 // --- Tensor structural ---
900 /// Reshapes a tensor without changing its data (ONNX `Reshape`).
901 Reshape,
902 /// Permutes a tensor's dimensions (ONNX `Transpose`).
903 Transpose {
904 /// The output permutation of input dimensions.
905 perm: alloc::vec::Vec<usize>,
906 },
907 /// Removes size-1 dimensions (ONNX `Squeeze`).
908 Squeeze {
909 /// The axes to operate along.
910 axes: alloc::vec::Vec<i64>,
911 },
912 /// Inserts size-1 dimensions (ONNX `Unsqueeze`).
913 Unsqueeze {
914 /// The axes to operate along.
915 axes: alloc::vec::Vec<i64>,
916 },
917 /// Concatenates tensors along an axis (ONNX `Concat`).
918 Concat {
919 /// The axis to operate along.
920 axis: i64,
921 },
922 /// Splits a tensor into multiple outputs along an axis (ONNX `Split`).
923 Split {
924 /// The axis to operate along.
925 axis: i64,
926 /// Number of outputs to split into.
927 num_outputs: usize,
928 },
929 /// Extracts a slice of a tensor (ONNX `Slice`).
930 Slice,
931 /// Gathers slices along an axis using an index tensor (ONNX `Gather`).
932 Gather {
933 /// The axis to operate along.
934 axis: i64,
935 },
936 /// Gathers individual elements along an axis (ONNX `GatherElements`).
937 GatherElements {
938 /// The axis to operate along.
939 axis: i64,
940 },
941 /// Gathers slices using N-D indices (ONNX `GatherND`).
942 GatherND {
943 /// Number of leading batch dimensions.
944 batch_dims: i64,
945 },
946 /// Scatters individual elements along an axis (ONNX `ScatterElements`).
947 ScatterElements {
948 /// The axis to operate along.
949 axis: i64,
950 },
951 /// Scatters slices using N-D indices (ONNX `ScatterND`).
952 ScatterND,
953 /// Tiles a tensor by repeating it (ONNX `Tile`).
954 Tile,
955 /// Broadcasts a tensor to a larger shape (ONNX `Expand`).
956 Expand,
957 /// Returns a tensor's shape as a 1-D tensor (ONNX `Shape`).
958 ShapeOf {
959 /// Start index.
960 start: i64,
961 /// End index.
962 end: i64,
963 },
964 /// Returns the total number of elements (ONNX `Size`).
965 SizeOf,
966 /// Passes the input through unchanged (ONNX `Identity`).
967 Identity,
968 /// Casts a tensor to another dtype (ONNX `Cast`).
969 Cast {
970 /// Target dtype.
971 to: DtypeRepr,
972 },
973 /// Casts a tensor to match another tensor's dtype (ONNX `CastLike`).
974 CastLike,
975 /// Element-wise conditional selection (ONNX `Where`).
976 Where,
977 /// Selects slices along an axis using a boolean mask (ONNX `Compress`).
978 Compress {
979 /// The axis to operate along.
980 axis: i64,
981 },
982 /// Generates a range of values (ONNX `Range`).
983 Range,
984 /// Constant tensor (value embedded in the ONNX model).
985 Constant {
986 /// The dtype to use.
987 dtype: DtypeRepr,
988 /// The tensor shape.
989 shape: Shape,
990 },
991 /// Creates a constant-filled tensor of a given shape (ONNX `ConstantOfShape`).
992 ConstantOfShape {
993 /// The dtype to use.
994 dtype: DtypeRepr,
995 },
996 /// Extracts the upper or lower triangular part of a matrix (ONNX `Trilu`).
997 Trilu {
998 /// Whether to keep the upper (vs. lower) triangular part.
999 upper: bool,
1000 },
1001 /// Reinterprets a tensor's bits as another dtype without conversion.
1002 BitCast {
1003 /// Target dtype.
1004 to: DtypeRepr,
1005 },
1006 /// Generic padding (ONNX `Pad`).
1007 Pad {
1008 /// The mode string selecting op-specific behavior.
1009 mode: alloc::string::String,
1010 },
1011 /// Reverses variable-length sequences along an axis (ONNX `ReverseSequence`).
1012 ReverseSequence {
1013 /// The batch axis.
1014 batch_axis: i64,
1015 /// The time axis.
1016 time_axis: i64,
1017 },
1018 /// Returns the indices of non-zero elements (ONNX `NonZero`).
1019 NonZero,
1020 /// Scatters values along an axis (deprecated ONNX `Scatter`, superseded by `ScatterElements`).
1021 Scatter {
1022 /// The axis to operate along.
1023 axis: i64,
1024 },
1025 /// Scatters an entire tensor into another at given indices.
1026 TensorScatter,
1027
1028 // --- Matrix ---
1029 /// General matrix multiply: `alpha * A @ B + beta * C` (ONNX `Gemm`).
1030 Gemm {
1031 /// The `alpha` parameter.
1032 alpha: f64,
1033 /// The `beta` parameter.
1034 beta: f64,
1035 /// Whether to transpose the first matrix operand.
1036 trans_a: bool,
1037 /// Whether to transpose the second matrix operand.
1038 trans_b: bool,
1039 },
1040 /// Matrix multiplication (ONNX `MatMul`).
1041 MatMul,
1042 /// Integer matrix multiplication (ONNX `MatMulInteger`).
1043 MatMulInteger,
1044 /// Einstein-summation contraction (ONNX `Einsum`).
1045 Einsum {
1046 /// The Einstein-summation equation string.
1047 equation: alloc::string::String,
1048 },
1049 /// Matrix determinant (ONNX `Det`).
1050 Det,
1051 /// Quantized linear matrix multiplication (ONNX `QLinearMatMul`).
1052 QLinearMatMul,
1053
1054 // --- Convolution extras ---
1055 /// Transposed (deconvolution) 2-D convolution (ONNX `ConvTranspose`).
1056 ConvTranspose {
1057 /// Number of input channels.
1058 in_channels: usize,
1059 /// Number of output channels.
1060 out_channels: usize,
1061 /// Convolution/pooling kernel height.
1062 kernel_h: usize,
1063 /// Convolution/pooling kernel width.
1064 kernel_w: usize,
1065 /// Vertical stride.
1066 stride_h: usize,
1067 /// Horizontal stride.
1068 stride_w: usize,
1069 /// Vertical zero-padding.
1070 padding_h: usize,
1071 /// Horizontal zero-padding.
1072 padding_w: usize,
1073 /// Additional vertical padding added to the output (transposed convolution).
1074 output_padding_h: usize,
1075 /// Additional horizontal padding added to the output (transposed convolution).
1076 output_padding_w: usize,
1077 /// Number of blocked/grouped connections (1 = standard).
1078 groups: usize,
1079 /// Whether a learned bias is added.
1080 has_bias: bool,
1081 },
1082 /// Integer convolution (ONNX `ConvInteger`).
1083 ConvInteger {
1084 /// Number of blocked/grouped connections (1 = standard).
1085 groups: usize,
1086 },
1087 /// Deformable convolution (ONNX `DeformConv`).
1088 DeformConv {
1089 /// Number of blocked/grouped connections (1 = standard).
1090 group: usize,
1091 /// Number of groups for deformable-convolution offset channels.
1092 offset_group: usize,
1093 },
1094 /// Quantized linear convolution (ONNX `QLinearConv`).
1095 QLinearConv {
1096 /// Number of blocked/grouped connections (1 = standard).
1097 groups: usize,
1098 },
1099 /// Combines sliding local blocks into a large tensor (ONNX `Col2Im`, inverse of im2col).
1100 Col2Im {
1101 /// Convolution/pooling kernel height.
1102 kernel_h: usize,
1103 /// Convolution/pooling kernel width.
1104 kernel_w: usize,
1105 },
1106 /// Stateful causal 1-D convolution, carrying a sliding-window state between calls (ONNX
1107 /// `CausalConvWithState`).
1108 CausalConvWithState {
1109 /// Name of the activation function applied after the convolution (empty = none).
1110 activation: alloc::string::String,
1111 },
1112
1113 // --- Reductions ---
1114 /// Sum reduction along axes (ONNX `ReduceSum`).
1115 ReduceSum {
1116 /// Whether to retain reduced dimensions with length 1.
1117 keepdims: bool,
1118 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1119 noop_with_empty_axes: bool,
1120 },
1121 /// Mean reduction along axes (ONNX `ReduceMean`).
1122 ReduceMean {
1123 /// Whether to retain reduced dimensions with length 1.
1124 keepdims: bool,
1125 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1126 noop_with_empty_axes: bool,
1127 },
1128 /// Max reduction along axes (ONNX `ReduceMax`).
1129 ReduceMax {
1130 /// Whether to retain reduced dimensions with length 1.
1131 keepdims: bool,
1132 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1133 noop_with_empty_axes: bool,
1134 },
1135 /// Min reduction along axes (ONNX `ReduceMin`).
1136 ReduceMin {
1137 /// Whether to retain reduced dimensions with length 1.
1138 keepdims: bool,
1139 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1140 noop_with_empty_axes: bool,
1141 },
1142 /// Product reduction along axes (ONNX `ReduceProd`).
1143 ReduceProd {
1144 /// Whether to retain reduced dimensions with length 1.
1145 keepdims: bool,
1146 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1147 noop_with_empty_axes: bool,
1148 },
1149 /// L1-norm reduction along axes (ONNX `ReduceL1`).
1150 ReduceL1 {
1151 /// Whether to retain reduced dimensions with length 1.
1152 keepdims: bool,
1153 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1154 noop_with_empty_axes: bool,
1155 },
1156 /// L2-norm reduction along axes (ONNX `ReduceL2`).
1157 ReduceL2 {
1158 /// Whether to retain reduced dimensions with length 1.
1159 keepdims: bool,
1160 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1161 noop_with_empty_axes: bool,
1162 },
1163 /// Log-sum reduction along axes (ONNX `ReduceLogSum`).
1164 ReduceLogSum {
1165 /// Whether to retain reduced dimensions with length 1.
1166 keepdims: bool,
1167 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1168 noop_with_empty_axes: bool,
1169 },
1170 /// Log-sum-exp reduction along axes (ONNX `ReduceLogSumExp`).
1171 ReduceLogSumExp {
1172 /// Whether to retain reduced dimensions with length 1.
1173 keepdims: bool,
1174 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1175 noop_with_empty_axes: bool,
1176 },
1177 /// Sum-of-squares reduction along axes (ONNX `ReduceSumSquare`).
1178 ReduceSumSquare {
1179 /// Whether to retain reduced dimensions with length 1.
1180 keepdims: bool,
1181 /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1182 noop_with_empty_axes: bool,
1183 },
1184 /// Cumulative sum along an axis (ONNX `CumSum`).
1185 CumSum {
1186 /// Whether to exclude the current element from the cumulative result.
1187 exclusive: bool,
1188 /// Whether to accumulate in reverse order.
1189 reverse: bool,
1190 },
1191 /// Cumulative product along an axis (ONNX `CumProd`).
1192 CumProd {
1193 /// Whether to exclude the current element from the cumulative result.
1194 exclusive: bool,
1195 /// Whether to accumulate in reverse order.
1196 reverse: bool,
1197 },
1198 /// Index of the maximum along an axis (ONNX `ArgMax`).
1199 ArgMax {
1200 /// The axis to operate along.
1201 axis: i64,
1202 /// Whether to retain reduced dimensions with length 1.
1203 keepdims: bool,
1204 /// Whether ties select the last (rather than first) matching index.
1205 select_last_index: bool,
1206 },
1207 /// Index of the minimum along an axis (ONNX `ArgMin`).
1208 ArgMin {
1209 /// The axis to operate along.
1210 axis: i64,
1211 /// Whether to retain reduced dimensions with length 1.
1212 keepdims: bool,
1213 /// Whether ties select the last (rather than first) matching index.
1214 select_last_index: bool,
1215 },
1216 /// Average-pools over the entire spatial extent (ONNX `GlobalAveragePool`).
1217 GlobalAvgPool,
1218 /// Max-pools over the entire spatial extent (ONNX `GlobalMaxPool`).
1219 GlobalMaxPool,
1220 /// Lp-norm normalization along an axis (ONNX `LpNormalization`).
1221 LpNormalization {
1222 /// The axis to operate along.
1223 axis: i64,
1224 /// The `p` in the p-norm.
1225 p: i64,
1226 },
1227 /// Mean/variance normalization along axes (ONNX `MeanVarianceNormalization`).
1228 MeanVarianceNormalization {
1229 /// The axes to operate along.
1230 axes: alloc::vec::Vec<i64>,
1231 },
1232
1233 // --- Additional activations ---
1234 /// Log-softmax along an axis (ONNX `LogSoftmax`).
1235 LogSoftmax {
1236 /// The axis to operate along.
1237 axis: i64,
1238 },
1239 /// One-hot of the argmax along an axis (ONNX `Hardmax`).
1240 Hardmax {
1241 /// The axis to operate along.
1242 axis: i64,
1243 },
1244 /// Parametric ReLU, with a learned per-channel slope (ONNX `PRelu`).
1245 PRelu,
1246 /// ReLU that zeroes values at or below `alpha` (ONNX `ThresholdedRelu`).
1247 ThresholdedRelu {
1248 /// The `alpha` parameter.
1249 alpha: f64,
1250 },
1251 /// Shrinks values toward zero by `lambd`, with a `bias` offset (ONNX `Shrink`).
1252 Shrink {
1253 /// The shrinkage threshold.
1254 lambd: f64,
1255 /// A bias/offset value.
1256 bias: f64,
1257 },
1258 /// Clamps values to a `[min, max]` range (ONNX `Clip`).
1259 Clip,
1260 /// Swish/SiLU activation (ONNX `Swish`).
1261 Swish,
1262 /// Multi-head attention (ONNX `MultiHeadAttention`).
1263 MultiHeadAttention {
1264 /// Number of query attention heads.
1265 q_num_heads: usize,
1266 /// Number of key/value attention heads.
1267 kv_num_heads: usize,
1268 },
1269 /// Attention with a user-defined score-modification function (ONNX `FlexAttention`). The
1270 /// score-modification subgraph itself is not captured (consistent with `Loop`/`If`/`Scan`
1271 /// not capturing their subgraph bodies).
1272 FlexAttention {
1273 /// Attention score scale factor, if explicitly specified.
1274 scale: f64,
1275 },
1276 /// Linear-complexity attention (e.g. gated delta rule variants), optionally carrying state
1277 /// between calls (ONNX `LinearAttention`).
1278 LinearAttention {
1279 /// Number of query attention heads.
1280 q_num_heads: usize,
1281 /// Number of key/value attention heads.
1282 kv_num_heads: usize,
1283 /// Name of the state-update rule (e.g. a gated-delta variant).
1284 update_rule: alloc::string::String,
1285 /// Attention score scale factor, if explicitly specified.
1286 scale: f64,
1287 },
1288
1289 // --- Normalisation (generic) ---
1290 /// Local response normalization (ONNX `LRN`).
1291 LRN {
1292 /// The `alpha` parameter.
1293 alpha: f64,
1294 /// The `beta` parameter.
1295 beta: f64,
1296 /// A bias/offset value.
1297 bias: f64,
1298 /// Window/kernel size.
1299 size: usize,
1300 },
1301
1302 // --- Recurrent ---
1303 /// Long short-term memory recurrent layer (ONNX `LSTM`).
1304 Lstm {
1305 /// Size of the hidden state.
1306 hidden_size: usize,
1307 /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1308 direction: alloc::string::String,
1309 /// Whether to run the recurrence in both directions.
1310 bidirectional: bool,
1311 },
1312 /// Gated recurrent unit layer (ONNX `GRU`).
1313 Gru {
1314 /// Size of the hidden state.
1315 hidden_size: usize,
1316 /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1317 direction: alloc::string::String,
1318 /// Whether to run the recurrence in both directions.
1319 bidirectional: bool,
1320 },
1321 /// Simple recurrent layer (ONNX `RNN`).
1322 Rnn {
1323 /// Size of the hidden state.
1324 hidden_size: usize,
1325 /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1326 direction: alloc::string::String,
1327 /// Whether to run the recurrence in both directions.
1328 bidirectional: bool,
1329 },
1330
1331 // --- Resize / spatial ---
1332 /// Resizes a tensor (interpolation) (ONNX `Resize`).
1333 Resize {
1334 /// The mode string selecting op-specific behavior.
1335 mode: alloc::string::String,
1336 /// How resized coordinates map back to the input (ONNX `Resize` mode string).
1337 coordinate_transformation_mode: alloc::string::String,
1338 /// Whether to apply an anti-aliasing filter when downsampling.
1339 antialias: bool,
1340 },
1341 /// Samples a tensor at grid-specified locations (ONNX `GridSample`).
1342 GridSample {
1343 /// The mode string selecting op-specific behavior.
1344 mode: alloc::string::String,
1345 /// How out-of-bounds sample coordinates are handled.
1346 padding_mode: alloc::string::String,
1347 /// Whether corner pixels are aligned (vs. edge-aligned) when sampling/resizing.
1348 align_corners: bool,
1349 },
1350 /// Rearranges spatial blocks into depth/channels (ONNX `SpaceToDepth`).
1351 SpaceToDepth {
1352 /// Block size for the space/depth rearrangement.
1353 blocksize: usize,
1354 },
1355 /// Rearranges depth/channels into spatial blocks (ONNX `DepthToSpace`).
1356 DepthToSpace {
1357 /// Block size for the space/depth rearrangement.
1358 blocksize: usize,
1359 /// The mode string selecting op-specific behavior.
1360 mode: alloc::string::String,
1361 },
1362 /// Region-of-interest pooling with bilinear alignment (ONNX `RoiAlign`).
1363 RoiAlign {
1364 /// Output region height.
1365 output_h: usize,
1366 /// Output region width.
1367 output_w: usize,
1368 /// Number of sampling points per output bin (0 = adaptive).
1369 sampling_ratio: i64,
1370 /// Scale factor mapping ROI coordinates to the input feature map.
1371 spatial_scale: f64,
1372 },
1373 /// Generates a 2-D/3-D sampling grid from an affine matrix (ONNX `AffineGrid`).
1374 AffineGrid {
1375 /// Whether corner pixels are aligned (vs. edge-aligned) when sampling/resizing.
1376 align_corners: bool,
1377 },
1378 /// Inverse of max pooling, using stored indices (ONNX `MaxUnpool`).
1379 MaxUnpool {
1380 /// Convolution/pooling kernel height.
1381 kernel_h: usize,
1382 /// Convolution/pooling kernel width.
1383 kernel_w: usize,
1384 /// Vertical stride.
1385 stride_h: usize,
1386 /// Horizontal stride.
1387 stride_w: usize,
1388 },
1389 /// Crops or pads a tensor to a target shape, centered (ONNX `CenterCropPad`).
1390 CenterCropPad {
1391 /// The axes to operate along.
1392 axes: alloc::vec::Vec<i64>,
1393 },
1394 /// Filters overlapping boxes by score (ONNX `NonMaxSuppression`).
1395 NonMaxSuppression {
1396 /// Whether boxes are given as `(center_x, center_y, width, height)` instead of corners.
1397 center_point_box: bool,
1398 },
1399
1400 // --- Misc ---
1401 /// Returns the top-K values/indices along an axis (ONNX `TopK`).
1402 TopK {
1403 /// The axis to operate along.
1404 axis: i64,
1405 /// Whether to return the largest (vs. smallest) K values.
1406 largest: bool,
1407 /// Whether outputs are sorted.
1408 sorted: bool,
1409 },
1410 /// Returns unique elements (ONNX `Unique`).
1411 Unique {
1412 /// Whether outputs are sorted.
1413 sorted: bool,
1414 },
1415 /// Dropout regularization (ONNX `Dropout`).
1416 Dropout {
1417 /// Whether dropout is active (vs. a no-op at inference).
1418 training_mode: bool,
1419 },
1420 /// Creates an identity-like 2-D tensor (ONNX `EyeLike`).
1421 EyeLike {
1422 /// The dtype to use.
1423 dtype: Option<DtypeRepr>,
1424 /// Diagonal offset.
1425 k: i64,
1426 },
1427 /// One-hot encodes indices along an axis (ONNX `OneHot`).
1428 OneHot {
1429 /// The axis to operate along.
1430 axis: i64,
1431 },
1432 /// Samples from a Bernoulli distribution using input probabilities (ONNX `Bernoulli`).
1433 Bernoulli {
1434 /// The dtype to use.
1435 dtype: Option<DtypeRepr>,
1436 },
1437 /// Samples uniform random values with another tensor's shape (ONNX `RandomUniformLike`).
1438 RandomUniformLike {
1439 /// The dtype to use.
1440 dtype: Option<DtypeRepr>,
1441 /// Upper bound of the sampling range.
1442 high: f64,
1443 /// Lower bound of the sampling range.
1444 low: f64,
1445 },
1446 /// Rotary position embedding (ONNX `RotaryEmbedding`).
1447 RotaryEmbedding,
1448
1449 // --- Quantisation ---
1450 /// Linear quantization to a lower-precision dtype (ONNX `QuantizeLinear`).
1451 QuantizeLinear {
1452 /// The axis to operate along.
1453 axis: i64,
1454 /// Whether to saturate (clamp) out-of-range values instead of wrapping.
1455 saturate: bool,
1456 },
1457 /// Linear dequantization back to a floating-point dtype (ONNX `DequantizeLinear`).
1458 DequantizeLinear {
1459 /// The axis to operate along.
1460 axis: i64,
1461 },
1462 /// Dynamically computes quantization parameters and quantizes (ONNX `DynamicQuantizeLinear`).
1463 DynamicQuantizeLinear,
1464
1465 // --- Signal ---
1466 /// Discrete Fourier transform (ONNX `DFT`).
1467 Dft {
1468 /// Whether to compute the inverse transform.
1469 inverse: bool,
1470 /// Whether to return only the non-redundant half of the spectrum.
1471 onesided: bool,
1472 },
1473 /// Short-time Fourier transform (ONNX `STFT`).
1474 Stft,
1475 /// Generates a mel-scale filterbank matrix (ONNX `MelWeightMatrix`).
1476 MelWeightMatrix,
1477 /// Generates a Hann window (ONNX `HannWindow`).
1478 HannWindow {
1479 /// Whether the window is periodic (vs. symmetric).
1480 periodic: bool,
1481 },
1482 /// Generates a Blackman window (ONNX `BlackmanWindow`).
1483 BlackmanWindow {
1484 /// Whether the window is periodic (vs. symmetric).
1485 periodic: bool,
1486 },
1487 /// Generates a Hamming window (ONNX `HammingWindow`).
1488 HammingWindow {
1489 /// Whether the window is periodic (vs. symmetric).
1490 periodic: bool,
1491 },
1492
1493 // --- Loss ---
1494 /// Negative log-likelihood loss (ONNX `NegativeLogLikelihoodLoss`).
1495 NegativeLogLikelihoodLoss {
1496 /// The reduction mode applied to the per-element loss (e.g. `"mean"`, `"sum"`, `"none"`).
1497 reduction: alloc::string::String,
1498 },
1499 /// Softmax + cross-entropy loss (ONNX `SoftmaxCrossEntropyLoss`).
1500 SoftmaxCrossEntropyLoss {
1501 /// The reduction mode applied to the per-element loss (e.g. `"mean"`, `"sum"`, `"none"`).
1502 reduction: alloc::string::String,
1503 },
1504
1505 // --- Sequences ---
1506 /// Indexes into a sequence (ONNX `SequenceAt`).
1507 SequenceAt,
1508 /// Constructs a sequence from tensors (ONNX `SequenceConstruct`).
1509 SequenceConstruct,
1510 /// Constructs an empty sequence (ONNX `SequenceEmpty`).
1511 SequenceEmpty,
1512 /// Removes an element from a sequence (ONNX `SequenceErase`).
1513 SequenceErase,
1514 /// Inserts an element into a sequence (ONNX `SequenceInsert`).
1515 SequenceInsert,
1516 /// Returns a sequence's length (ONNX `SequenceLength`).
1517 SequenceLength,
1518 /// Applies a subgraph to each element of a sequence (ONNX `SequenceMap`).
1519 SequenceMap,
1520 /// Splits a tensor into a sequence along an axis (ONNX `SplitToSequence`).
1521 SplitToSequence {
1522 /// The axis to operate along.
1523 axis: i64,
1524 /// Whether to retain reduced dimensions with length 1.
1525 keepdims: bool,
1526 },
1527 /// Concatenates a sequence's elements into one tensor (ONNX `ConcatFromSequence`).
1528 ConcatFromSequence {
1529 /// The axis to operate along.
1530 axis: i64,
1531 /// Whether to insert a new axis for the concatenation dimension.
1532 new_axis: bool,
1533 },
1534 /// Extracts the value from an optional (ONNX `OptionalGetElement`).
1535 OptionalGetElement,
1536 /// Tests whether an optional has a value (ONNX `OptionalHasElement`).
1537 OptionalHasElement,
1538
1539 // --- Control flow ---
1540 /// Generic looping construct over a subgraph (ONNX `Loop`).
1541 Loop,
1542 /// Applies a subgraph iteratively over input sequences (ONNX `Scan`).
1543 Scan {
1544 /// Number of inputs treated as scanned sequences.
1545 num_scan_inputs: i64,
1546 },
1547 /// Conditional branch over subgraphs (ONNX `If`).
1548 If,
1549
1550 // --- Optimiser ops ---
1551 /// Adagrad optimizer step (ONNX `Adagrad`).
1552 Adagrad,
1553 /// Adam optimizer step (ONNX `Adam`).
1554 Adam,
1555 /// Momentum optimizer step (ONNX `Momentum`).
1556 Momentum,
1557 /// Computes gradients of a subgraph (ONNX `Gradient`).
1558 Gradient,
1559
1560 // --- String / NLP ---
1561 /// Normalizes strings (case folding, stop-word removal) (ONNX `StringNormalizer`).
1562 StringNormalizer,
1563 /// Tests strings against a regex (ONNX `RegexFullMatch`).
1564 RegexFullMatch {
1565 /// The regular expression pattern.
1566 pattern: alloc::string::String,
1567 },
1568 /// Concatenates strings element-wise (ONNX `StringConcat`).
1569 StringConcat,
1570 /// Splits strings on a delimiter (ONNX `StringSplit`).
1571 StringSplit,
1572 /// Computes TF-IDF n-gram features (ONNX `TfIdfVectorizer`).
1573 TfIdfVectorizer,
1574 /// Maps categorical labels to/from encoded values (ONNX `LabelEncoder`).
1575 LabelEncoder,
1576
1577 // --- Other ML ---
1578 /// Selects elements from a tensor by index (ONNX-ML `ArrayFeatureExtractor`).
1579 ArrayFeatureExtractor,
1580 /// Binarizes values against a threshold (ONNX-ML `Binarizer`).
1581 Binarizer {
1582 /// The threshold value.
1583 threshold: f64,
1584 },
1585 /// Decision-tree ensemble inference (ONNX-ML `TreeEnsemble`).
1586 TreeEnsemble,
1587 /// Decodes an encoded image (e.g. PNG/JPEG) into a tensor (ONNX `ImageDecoder`).
1588 ImageDecoder,
1589}
1590
1591/// One node in a [`Graph`]: an [`Op`] plus its producer indices, dtype, and output shape.
1592#[derive(Debug, Clone)]
1593pub struct GraphNode {
1594 /// This node's operation.
1595 pub op: Op,
1596 /// Indices of producer nodes in `Graph::nodes`; empty for `Input`.
1597 pub inputs: Vec<usize>,
1598 /// This node's output dtype.
1599 pub dtype: DtypeRepr,
1600 /// Output shape of this node. `None` in a slot means a dynamic/unknown
1601 /// dimension (e.g. the batch axis).
1602 pub shape: Shape,
1603}
1604
1605/// The traced computational graph: a list of [`GraphNode`]s plus optional node names.
1606#[derive(Debug, Default, Clone)]
1607pub struct Graph {
1608 /// The graph's nodes, in the order they were recorded.
1609 pub nodes: Vec<GraphNode>,
1610 /// Node index → dotted name captured from [`crate::name_scope`] at recording time.
1611 pub names: BTreeMap<usize, String>,
1612}
1613
1614impl Graph {
1615 /// Creates an empty graph.
1616 pub fn new() -> Self {
1617 Self::default()
1618 }
1619
1620 /// Appends a new node to the graph, returning its index.
1621 pub fn add_node(
1622 &mut self,
1623 op: Op,
1624 inputs: Vec<usize>,
1625 dtype: DtypeRepr,
1626 shape: Shape,
1627 ) -> usize {
1628 let id = self.nodes.len();
1629 self.nodes.push(GraphNode {
1630 op,
1631 inputs,
1632 dtype,
1633 shape,
1634 });
1635 #[cfg(feature = "std")]
1636 if let Some(name) = crate::name_scope::current_scope() {
1637 self.names.insert(id, name);
1638 }
1639 id
1640 }
1641
1642 /// Returns node indices in topological order (producers before consumers)
1643 /// using Kahn's algorithm. Panics if the graph contains a cycle.
1644 pub fn topological_sort(&self) -> Vec<usize> {
1645 let n = self.nodes.len();
1646 let mut in_degree = vec![0usize; n];
1647 let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
1648
1649 for (id, node) in self.nodes.iter().enumerate() {
1650 for &input in &node.inputs {
1651 in_degree[id] += 1;
1652 dependents[input].push(id);
1653 }
1654 }
1655
1656 let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
1657 let mut order = Vec::with_capacity(n);
1658
1659 while let Some(id) = queue.pop() {
1660 order.push(id);
1661 for &dep in &dependents[id] {
1662 in_degree[dep] -= 1;
1663 if in_degree[dep] == 0 {
1664 queue.push(dep);
1665 }
1666 }
1667 }
1668
1669 assert_eq!(order.len(), n, "graph contains a cycle");
1670 order
1671 }
1672
1673 /// Rewrite the graph by fusing compatible op sequences into single fused ops.
1674 ///
1675 /// Currently recognises: `Conv2d(no bias) → BatchNorm2d → Silu`
1676 /// and replaces the triple with a single `Conv2dBnSilu` node.
1677 ///
1678 /// Nodes that are absorbed into a fused node are removed from the output
1679 /// graph; remaining node indices are renumbered contiguously.
1680 pub fn optimise(&self) -> Graph {
1681 let n = self.nodes.len();
1682
1683 // Build per-node consumer counts so we can detect single-use nodes.
1684 let mut n_consumers = vec![0usize; n];
1685 for node in &self.nodes {
1686 for &inp in &node.inputs {
1687 n_consumers[inp] += 1;
1688 }
1689 }
1690
1691 // dead[i] — node i is absorbed into a downstream fused node.
1692 let mut dead = vec![false; n];
1693 // node_override[i] — replacement (op, inputs) for node i.
1694 let mut node_override: Vec<Option<(Op, Vec<usize>)>> = vec![None; n];
1695
1696 // `silu_idx` indexes multiple parallel collections below (self.nodes, n_consumers,
1697 // node_override), not just one -- an iterator/enumerate() rewrite wouldn't be clearer.
1698 #[allow(clippy::needless_range_loop)]
1699 for silu_idx in 0..n {
1700 if !matches!(self.nodes[silu_idx].op, Op::Silu) {
1701 continue;
1702 }
1703 if self.nodes[silu_idx].inputs.len() != 1 {
1704 continue;
1705 }
1706
1707 let bn_idx = self.nodes[silu_idx].inputs[0];
1708 if !matches!(self.nodes[bn_idx].op, Op::BatchNorm2d { .. }) {
1709 continue;
1710 }
1711 if n_consumers[bn_idx] != 1 {
1712 continue;
1713 }
1714 if self.nodes[bn_idx].inputs.len() != 1 {
1715 continue;
1716 }
1717
1718 let conv_idx = self.nodes[bn_idx].inputs[0];
1719 if !matches!(
1720 self.nodes[conv_idx].op,
1721 Op::Conv2d {
1722 has_bias: false,
1723 ..
1724 }
1725 ) {
1726 continue;
1727 }
1728 if n_consumers[conv_idx] != 1 {
1729 continue;
1730 }
1731
1732 let (
1733 in_channels,
1734 out_channels,
1735 kernel_h,
1736 kernel_w,
1737 stride_h,
1738 stride_w,
1739 padding_h,
1740 padding_w,
1741 groups,
1742 ) = if let Op::Conv2d {
1743 in_channels,
1744 out_channels,
1745 kernel_h,
1746 kernel_w,
1747 stride_h,
1748 stride_w,
1749 padding_h,
1750 padding_w,
1751 groups,
1752 ..
1753 } = self.nodes[conv_idx].op
1754 {
1755 (
1756 in_channels,
1757 out_channels,
1758 kernel_h,
1759 kernel_w,
1760 stride_h,
1761 stride_w,
1762 padding_h,
1763 padding_w,
1764 groups,
1765 )
1766 } else {
1767 unreachable!()
1768 };
1769
1770 let bn_eps = if let Op::BatchNorm2d { eps, .. } = self.nodes[bn_idx].op {
1771 eps
1772 } else {
1773 unreachable!()
1774 };
1775
1776 dead[conv_idx] = true;
1777 dead[bn_idx] = true;
1778 node_override[silu_idx] = Some((
1779 Op::Conv2dBnSilu {
1780 in_channels,
1781 out_channels,
1782 kernel_h,
1783 kernel_w,
1784 stride_h,
1785 stride_w,
1786 padding_h,
1787 padding_w,
1788 groups,
1789 bn_eps,
1790 },
1791 self.nodes[conv_idx].inputs.clone(),
1792 ));
1793 }
1794
1795 // Build old → new index mapping (dead nodes are skipped).
1796 let mut old_to_new = vec![0usize; n];
1797 let mut new_count = 0usize;
1798 for i in 0..n {
1799 if !dead[i] {
1800 old_to_new[i] = new_count;
1801 new_count += 1;
1802 }
1803 }
1804
1805 // Emit new graph in topological order (original ordering is already valid).
1806 let mut new_graph = Graph::new();
1807 for old_idx in 0..n {
1808 if dead[old_idx] {
1809 continue;
1810 }
1811 let node = &self.nodes[old_idx];
1812 let (op, inputs) =
1813 if let Some((fused_op, fused_inputs)) = node_override[old_idx].clone() {
1814 let mapped = fused_inputs.iter().map(|&i| old_to_new[i]).collect();
1815 (fused_op, mapped)
1816 } else {
1817 let mapped = node.inputs.iter().map(|&i| old_to_new[i]).collect();
1818 (node.op.clone(), mapped)
1819 };
1820 let new_idx = new_graph.nodes.len();
1821 new_graph.nodes.push(GraphNode {
1822 op,
1823 inputs,
1824 dtype: node.dtype,
1825 shape: node.shape.clone(),
1826 });
1827 if let Some(name) = self.names.get(&old_idx) {
1828 new_graph.names.insert(new_idx, name.clone());
1829 }
1830 }
1831
1832 new_graph
1833 }
1834}
1835
1836// ---------------------------------------------------------------------------
1837// Shape inference — computes the output shape for each Op given an input shape
1838// ---------------------------------------------------------------------------
1839
1840fn infer_output_shape(op: &Op, inputs: &[&Shape]) -> Shape {
1841 // Constant has no tensor inputs — its shape is embedded in the op itself.
1842 if let Op::Constant { shape, .. } = op {
1843 return shape.clone();
1844 }
1845 // Zero-input ops that produce no tensor output.
1846 if matches!(op, Op::SequenceEmpty | Op::OptionalHasElement) {
1847 return vec![];
1848 }
1849 let input = inputs[0];
1850 match op {
1851 Op::Input => input.clone(),
1852
1853 // Element-wise / shape-preserving — output shape = input shape
1854 Op::Relu
1855 | Op::Elu { .. }
1856 | Op::Selu
1857 | Op::Celu { .. }
1858 | Op::Gelu
1859 | Op::Mish
1860 | Op::Hardtanh { .. }
1861 | Op::Relu6
1862 | Op::Hardsigmoid
1863 | Op::Hardswish
1864 | Op::Hardshrink { .. }
1865 | Op::LeakyRelu { .. }
1866 | Op::Threshold { .. }
1867 | Op::Softsign
1868 | Op::Softshrink { .. }
1869 | Op::Softplus { .. }
1870 | Op::Sigmoid
1871 | Op::Silu
1872 | Op::Logsigmoid
1873 | Op::Tanh
1874 | Op::Tanhshrink
1875 | Op::Softmax { .. }
1876 | Op::BatchNorm1d { .. }
1877 | Op::BatchNorm2d { .. }
1878 | Op::BatchNorm3d { .. }
1879 | Op::LayerNorm { .. }
1880 | Op::RmsNorm { .. }
1881 | Op::GroupNorm { .. }
1882 | Op::InstanceNorm1d { .. }
1883 | Op::InstanceNorm2d { .. }
1884 | Op::InstanceNorm3d { .. } => input.clone(),
1885
1886 Op::Linear { out_features, .. } => {
1887 // [..., in_features] → [..., out_features]
1888 let mut out = input[..input.len() - 1].to_vec();
1889 out.push(Some(*out_features));
1890 out
1891 }
1892
1893 Op::Flatten => {
1894 // [N, C, H, W, ...] → [N, C*H*W*...]
1895 let rest = &input[1..];
1896 let flat: Option<usize> = rest
1897 .iter()
1898 .try_fold(1usize, |acc, dim| dim.map(|d| acc * d));
1899 vec![input[0], flat]
1900 }
1901
1902 // --- Convolution ---
1903 Op::Conv1d {
1904 out_channels,
1905 kernel_l,
1906 stride,
1907 padding,
1908 ..
1909 } => {
1910 // [N, C_in, L] → [N, C_out, L_out]
1911 let l_out = input[2].map(|l| (l + 2 * padding - kernel_l) / stride + 1);
1912 vec![input[0], Some(*out_channels), l_out]
1913 }
1914
1915 Op::Conv2d {
1916 out_channels,
1917 kernel_h,
1918 kernel_w,
1919 stride_h,
1920 stride_w,
1921 padding_h,
1922 padding_w,
1923 ..
1924 }
1925 | Op::Conv2dBnSilu {
1926 out_channels,
1927 kernel_h,
1928 kernel_w,
1929 stride_h,
1930 stride_w,
1931 padding_h,
1932 padding_w,
1933 ..
1934 } => {
1935 // [N, C_in, H, W] → [N, C_out, H_out, W_out]
1936 let h_out = input[2].map(|h| (h + 2 * padding_h - kernel_h) / stride_h + 1);
1937 let w_out = input[3].map(|w| (w + 2 * padding_w - kernel_w) / stride_w + 1);
1938 vec![input[0], Some(*out_channels), h_out, w_out]
1939 }
1940
1941 Op::Conv3d {
1942 out_channels,
1943 kernel_d,
1944 kernel_h,
1945 kernel_w,
1946 stride_d,
1947 stride_h,
1948 stride_w,
1949 padding_d,
1950 padding_h,
1951 padding_w,
1952 ..
1953 } => {
1954 // [N, C_in, D, H, W] → [N, C_out, D_out, H_out, W_out]
1955 let d_out = input[2].map(|d| (d + 2 * padding_d - kernel_d) / stride_d + 1);
1956 let h_out = input[3].map(|h| (h + 2 * padding_h - kernel_h) / stride_h + 1);
1957 let w_out = input[4].map(|w| (w + 2 * padding_w - kernel_w) / stride_w + 1);
1958 vec![input[0], Some(*out_channels), d_out, h_out, w_out]
1959 }
1960
1961 // --- Pooling ---
1962 Op::AvgPool1d { kernel_l, stride } | Op::MaxPool1d { kernel_l, stride } => {
1963 let l_out = input[2].map(|l| (l - kernel_l) / stride + 1);
1964 vec![input[0], input[1], l_out]
1965 }
1966
1967 Op::LpPool1d {
1968 kernel_l, stride, ..
1969 } => {
1970 let l_out = input[2].map(|l| (l - kernel_l) / stride + 1);
1971 vec![input[0], input[1], l_out]
1972 }
1973
1974 Op::AvgPool2d {
1975 kernel_h,
1976 kernel_w,
1977 stride_h,
1978 stride_w,
1979 } => {
1980 let h_out = input[2].map(|h| (h - kernel_h) / stride_h + 1);
1981 let w_out = input[3].map(|w| (w - kernel_w) / stride_w + 1);
1982 vec![input[0], input[1], h_out, w_out]
1983 }
1984
1985 Op::MaxPool2d {
1986 kernel_h,
1987 kernel_w,
1988 stride_h,
1989 stride_w,
1990 pad_h,
1991 pad_w,
1992 } => {
1993 let h_out = input[2].map(|h| (h + 2 * pad_h - kernel_h) / stride_h + 1);
1994 let w_out = input[3].map(|w| (w + 2 * pad_w - kernel_w) / stride_w + 1);
1995 vec![input[0], input[1], h_out, w_out]
1996 }
1997
1998 Op::LpPool2d {
1999 kernel_h,
2000 kernel_w,
2001 stride_h,
2002 stride_w,
2003 ..
2004 } => {
2005 let h_out = input[2].map(|h| (h - kernel_h) / stride_h + 1);
2006 let w_out = input[3].map(|w| (w - kernel_w) / stride_w + 1);
2007 vec![input[0], input[1], h_out, w_out]
2008 }
2009
2010 Op::AvgPool3d {
2011 kernel_d,
2012 kernel_h,
2013 kernel_w,
2014 stride_d,
2015 stride_h,
2016 stride_w,
2017 }
2018 | Op::MaxPool3d {
2019 kernel_d,
2020 kernel_h,
2021 kernel_w,
2022 stride_d,
2023 stride_h,
2024 stride_w,
2025 } => {
2026 let d_out = input[2].map(|d| (d - kernel_d) / stride_d + 1);
2027 let h_out = input[3].map(|h| (h - kernel_h) / stride_h + 1);
2028 let w_out = input[4].map(|w| (w - kernel_w) / stride_w + 1);
2029 vec![input[0], input[1], d_out, h_out, w_out]
2030 }
2031
2032 Op::LpPool3d {
2033 kernel_d,
2034 kernel_h,
2035 kernel_w,
2036 stride_d,
2037 stride_h,
2038 stride_w,
2039 ..
2040 } => {
2041 let d_out = input[2].map(|d| (d - kernel_d) / stride_d + 1);
2042 let h_out = input[3].map(|h| (h - kernel_h) / stride_h + 1);
2043 let w_out = input[4].map(|w| (w - kernel_w) / stride_w + 1);
2044 vec![input[0], input[1], d_out, h_out, w_out]
2045 }
2046
2047 // --- Upsample ---
2048 Op::UpsampleNearest2d { scale_h, scale_w } => {
2049 // [N, C, H, W] → [N, C, H * scale_h, W * scale_w]
2050 let h_out = input[2].map(|h| h * scale_h);
2051 let w_out = input[3].map(|w| w * scale_w);
2052 vec![input[0], input[1], h_out, w_out]
2053 }
2054
2055 // --- Padding ---
2056 Op::ConstantPad1d {
2057 pad_left,
2058 pad_right,
2059 ..
2060 }
2061 | Op::ReflectionPad1d {
2062 pad_left,
2063 pad_right,
2064 }
2065 | Op::ReplicationPad1d {
2066 pad_left,
2067 pad_right,
2068 }
2069 | Op::CircularPad1d {
2070 pad_left,
2071 pad_right,
2072 } => {
2073 // [N, C, L] → [N, C, L + pad_left + pad_right]
2074 let l_out = input[2].map(|l| l + pad_left + pad_right);
2075 vec![input[0], input[1], l_out]
2076 }
2077
2078 Op::ConstantPad2d {
2079 pad_l,
2080 pad_r,
2081 pad_t,
2082 pad_b,
2083 ..
2084 }
2085 | Op::ReflectionPad2d {
2086 pad_l,
2087 pad_r,
2088 pad_t,
2089 pad_b,
2090 }
2091 | Op::ReplicationPad2d {
2092 pad_l,
2093 pad_r,
2094 pad_t,
2095 pad_b,
2096 }
2097 | Op::CircularPad2d {
2098 pad_l,
2099 pad_r,
2100 pad_t,
2101 pad_b,
2102 } => {
2103 // [N, C, H, W] → [N, C, H + pad_t + pad_b, W + pad_l + pad_r]
2104 let h_out = input[2].map(|h| h + pad_t + pad_b);
2105 let w_out = input[3].map(|w| w + pad_l + pad_r);
2106 vec![input[0], input[1], h_out, w_out]
2107 }
2108
2109 Op::ConstantPad3d {
2110 pad_d1,
2111 pad_d2,
2112 pad_h1,
2113 pad_h2,
2114 pad_w1,
2115 pad_w2,
2116 ..
2117 }
2118 | Op::ReflectionPad3d {
2119 pad_d1,
2120 pad_d2,
2121 pad_h1,
2122 pad_h2,
2123 pad_w1,
2124 pad_w2,
2125 }
2126 | Op::ReplicationPad3d {
2127 pad_d1,
2128 pad_d2,
2129 pad_h1,
2130 pad_h2,
2131 pad_w1,
2132 pad_w2,
2133 }
2134 | Op::CircularPad3d {
2135 pad_d1,
2136 pad_d2,
2137 pad_h1,
2138 pad_h2,
2139 pad_w1,
2140 pad_w2,
2141 } => {
2142 // [N, C, D, H, W] → padded on each spatial dim
2143 let d_out = input[2].map(|d| d + pad_d1 + pad_d2);
2144 let h_out = input[3].map(|h| h + pad_h1 + pad_h2);
2145 let w_out = input[4].map(|w| w + pad_w1 + pad_w2);
2146 vec![input[0], input[1], d_out, h_out, w_out]
2147 }
2148
2149 Op::Attention { .. } => input.clone(),
2150
2151 Op::Add => input.clone(),
2152
2153 Op::ChannelChunk { chunk_c, .. } => {
2154 // [N, c_total, H, W] → [N, chunk_c, H, W]
2155 vec![input[0], Some(*chunk_c), input[2], input[3]]
2156 }
2157
2158 Op::ChannelCat { c_total } => {
2159 // multi-input; c_total encodes the output channel count
2160 vec![input[0], Some(*c_total), input[2], input[3]]
2161 }
2162
2163 Op::ChannelBiasAdd { .. } => input.to_vec(),
2164
2165 Op::Custom { data } => data.infer_output_shape(inputs),
2166
2167 // -------------------------------------------------------------------
2168 // ONNX-sourced ops — shape inference below.
2169 // For ops whose output shape equals the primary input shape (element-
2170 // wise, identity-like, or shape-tracked via ONNX value_info), we just
2171 // clone the input shape. Ops with genuinely different output shapes
2172 // have explicit arms.
2173 // -------------------------------------------------------------------
2174
2175 // Unary element-wise — output shape = input shape
2176 Op::Abs
2177 | Op::Neg
2178 | Op::Ceil
2179 | Op::Floor
2180 | Op::Round
2181 | Op::Sqrt
2182 | Op::Reciprocal
2183 | Op::Exp
2184 | Op::Log
2185 | Op::Erf
2186 | Op::Sign
2187 | Op::IsNaN
2188 | Op::IsInf { .. }
2189 | Op::Not
2190 | Op::BitwiseNot
2191 | Op::Sin
2192 | Op::Cos
2193 | Op::Tan
2194 | Op::Asin
2195 | Op::Acos
2196 | Op::Atan
2197 | Op::Sinh
2198 | Op::Cosh
2199 | Op::Asinh
2200 | Op::Acosh
2201 | Op::Atanh
2202 | Op::PRelu
2203 | Op::ThresholdedRelu { .. }
2204 | Op::Shrink { .. }
2205 | Op::Clip
2206 | Op::Swish
2207 | Op::LogSoftmax { .. }
2208 | Op::Hardmax { .. }
2209 | Op::Dropout { .. }
2210 | Op::Identity
2211 | Op::LRN { .. }
2212 | Op::MeanVarianceNormalization { .. }
2213 | Op::LpNormalization { .. }
2214 | Op::Pad { .. }
2215 | Op::ReverseSequence { .. }
2216 | Op::Trilu { .. }
2217 | Op::CumSum { .. }
2218 | Op::CumProd { .. }
2219 | Op::QuantizeLinear { .. }
2220 | Op::DequantizeLinear { .. }
2221 | Op::DynamicQuantizeLinear
2222 | Op::Bernoulli { .. }
2223 | Op::RandomUniformLike { .. }
2224 | Op::EyeLike { .. }
2225 | Op::RotaryEmbedding
2226 | Op::MultiHeadAttention { .. }
2227 | Op::FlexAttention { .. }
2228 | Op::LinearAttention { .. }
2229 | Op::CausalConvWithState { .. } => input.clone(),
2230
2231 // Binary / variadic element-wise — approximate as first-input shape
2232 Op::Mul
2233 | Op::Sub
2234 | Op::Div
2235 | Op::Pow
2236 | Op::Mod { .. }
2237 | Op::ElemMin
2238 | Op::ElemMax
2239 | Op::ElemMean
2240 | Op::ElemSum
2241 | Op::Equal
2242 | Op::Greater
2243 | Op::GreaterOrEqual
2244 | Op::Less
2245 | Op::LessOrEqual
2246 | Op::And
2247 | Op::Or
2248 | Op::Xor
2249 | Op::BitwiseAnd
2250 | Op::BitwiseOr
2251 | Op::BitwiseXor
2252 | Op::BitShift { .. }
2253 | Op::Cast { .. }
2254 | Op::CastLike
2255 | Op::BitCast { .. }
2256 | Op::Where => input.clone(),
2257
2258 // Structural ops where output shape = input shape or is unknown at
2259 // static inference time (ONNX value_info carries the true shape).
2260 Op::Reshape
2261 | Op::Squeeze { .. }
2262 | Op::Unsqueeze { .. }
2263 | Op::Slice
2264 | Op::Gather { .. }
2265 | Op::GatherElements { .. }
2266 | Op::GatherND { .. }
2267 | Op::ScatterElements { .. }
2268 | Op::ScatterND
2269 | Op::Tile
2270 | Op::Expand
2271 | Op::Compress { .. }
2272 | Op::Range
2273 | Op::ConstantOfShape { .. }
2274 | Op::NonZero
2275 | Op::Scatter { .. }
2276 | Op::TensorScatter
2277 | Op::Resize { .. }
2278 | Op::GridSample { .. }
2279 | Op::AffineGrid { .. }
2280 | Op::CenterCropPad { .. } => input.clone(),
2281
2282 Op::Transpose { perm } => {
2283 if perm.is_empty() {
2284 input.iter().rev().cloned().collect()
2285 } else {
2286 perm.iter()
2287 .map(|&i| input.get(i).copied().unwrap_or(None))
2288 .collect()
2289 }
2290 }
2291
2292 Op::Concat { axis } => {
2293 let rank = input.len();
2294 if rank == 0 {
2295 return input.clone();
2296 }
2297 let ax = axis.rem_euclid(rank as i64) as usize;
2298 let mut out = input.clone();
2299 // Sum the concatenated axis across all inputs.
2300 out[ax] = inputs
2301 .iter()
2302 .try_fold(0usize, |acc, s| {
2303 s.get(ax).copied().unwrap_or(None).map(|d| acc + d)
2304 })
2305 .map(Some)
2306 .unwrap_or(None);
2307 out
2308 }
2309
2310 Op::Split { axis, num_outputs } => {
2311 let rank = input.len();
2312 if rank == 0 {
2313 return input.clone();
2314 }
2315 let ax = axis.rem_euclid(rank as i64) as usize;
2316 let mut out = input.clone();
2317 out[ax] = input[ax].map(|d| d / num_outputs.max(&1));
2318 out
2319 }
2320
2321 Op::ShapeOf { start, end } => {
2322 let rank = input.len() as i64;
2323 let s = start.rem_euclid(rank.max(1));
2324 let e = end.rem_euclid(rank.max(1));
2325 vec![Some((e - s).max(0) as usize)]
2326 }
2327
2328 Op::SizeOf => vec![Some(1)],
2329
2330 Op::Gemm {
2331 trans_a, trans_b, ..
2332 } => {
2333 let m = if *trans_a {
2334 input.get(1)
2335 } else {
2336 input.first()
2337 }
2338 .copied()
2339 .unwrap_or(None);
2340 let n = if inputs.len() >= 2 {
2341 let b = inputs[1];
2342 if *trans_b { b.first() } else { b.get(1) }
2343 .copied()
2344 .unwrap_or(None)
2345 } else {
2346 None
2347 };
2348 vec![m, n]
2349 }
2350
2351 Op::MatMul | Op::MatMulInteger | Op::QLinearMatMul => {
2352 if inputs.len() >= 2 && !input.is_empty() {
2353 let other = inputs[1];
2354 let mut out = input[..input.len() - 1].to_vec();
2355 out.push(other.last().copied().unwrap_or(None));
2356 out
2357 } else {
2358 input.clone()
2359 }
2360 }
2361
2362 Op::Einsum { .. }
2363 | Op::Det
2364 | Op::Col2Im { .. }
2365 | Op::ConvInteger { .. }
2366 | Op::DeformConv { .. }
2367 | Op::QLinearConv { .. } => input.clone(),
2368
2369 Op::ConvTranspose {
2370 out_channels,
2371 kernel_h,
2372 kernel_w,
2373 stride_h,
2374 stride_w,
2375 padding_h,
2376 padding_w,
2377 output_padding_h,
2378 output_padding_w,
2379 ..
2380 } => {
2381 let h_out =
2382 input[2].map(|h| (h - 1) * stride_h - 2 * padding_h + kernel_h + output_padding_h);
2383 let w_out =
2384 input[3].map(|w| (w - 1) * stride_w - 2 * padding_w + kernel_w + output_padding_w);
2385 vec![input[0], Some(*out_channels), h_out, w_out]
2386 }
2387
2388 Op::ReduceSum { keepdims, .. }
2389 | Op::ReduceMean { keepdims, .. }
2390 | Op::ReduceMax { keepdims, .. }
2391 | Op::ReduceMin { keepdims, .. }
2392 | Op::ReduceProd { keepdims, .. }
2393 | Op::ReduceL1 { keepdims, .. }
2394 | Op::ReduceL2 { keepdims, .. }
2395 | Op::ReduceLogSum { keepdims, .. }
2396 | Op::ReduceLogSumExp { keepdims, .. }
2397 | Op::ReduceSumSquare { keepdims, .. } => {
2398 // Without axis info at static-inference time, approximate:
2399 // keepdims=true → same rank, keepdims=false → reduce all → scalar.
2400 if *keepdims {
2401 input.clone()
2402 } else {
2403 vec![Some(1)]
2404 }
2405 }
2406
2407 Op::ArgMax { axis, keepdims, .. } | Op::ArgMin { axis, keepdims, .. } => {
2408 if input.is_empty() {
2409 return vec![];
2410 }
2411 let ax = axis.rem_euclid(input.len() as i64) as usize;
2412 if *keepdims {
2413 let mut out = input.clone();
2414 out[ax] = Some(1);
2415 out
2416 } else {
2417 let mut out = input.clone();
2418 out.remove(ax);
2419 out
2420 }
2421 }
2422
2423 Op::GlobalAvgPool | Op::GlobalMaxPool => {
2424 let mut out = input[..2.min(input.len())].to_vec();
2425 for _ in 2..input.len() {
2426 out.push(Some(1));
2427 }
2428 out
2429 }
2430
2431 Op::Lstm {
2432 hidden_size,
2433 bidirectional,
2434 ..
2435 }
2436 | Op::Gru {
2437 hidden_size,
2438 bidirectional,
2439 ..
2440 }
2441 | Op::Rnn {
2442 hidden_size,
2443 bidirectional,
2444 ..
2445 } => {
2446 let num_dirs: usize = if *bidirectional { 2 } else { 1 };
2447 // [seq_len, num_directions, batch, hidden_size] (approximate)
2448 vec![
2449 input.first().copied().unwrap_or(None),
2450 Some(num_dirs),
2451 input.get(1).copied().unwrap_or(None),
2452 Some(*hidden_size),
2453 ]
2454 }
2455
2456 Op::SpaceToDepth { blocksize } => {
2457 let c_out = input[1].map(|c| c * blocksize * blocksize);
2458 let h_out = input[2].map(|h| h / blocksize);
2459 let w_out = input[3].map(|w| w / blocksize);
2460 vec![input[0], c_out, h_out, w_out]
2461 }
2462
2463 Op::DepthToSpace { blocksize, .. } => {
2464 let c_out = input[1].map(|c| c / (blocksize * blocksize));
2465 let h_out = input[2].map(|h| h * blocksize);
2466 let w_out = input[3].map(|w| w * blocksize);
2467 vec![input[0], c_out, h_out, w_out]
2468 }
2469
2470 Op::RoiAlign {
2471 output_h, output_w, ..
2472 } => {
2473 vec![input[0], input[1], Some(*output_h), Some(*output_w)]
2474 }
2475
2476 Op::MaxUnpool {
2477 kernel_h,
2478 kernel_w,
2479 stride_h,
2480 stride_w,
2481 } => {
2482 let h_out = input[2].map(|h| (h - 1) * stride_h + kernel_h);
2483 let w_out = input[3].map(|w| (w - 1) * stride_w + kernel_w);
2484 vec![input[0], input[1], h_out, w_out]
2485 }
2486
2487 Op::NonMaxSuppression { .. } => vec![None, Some(3)],
2488
2489 Op::TopK { axis, .. } => {
2490 // Second input is k (runtime). Return input shape as approximation.
2491 let _ = axis;
2492 input.clone()
2493 }
2494
2495 Op::Unique { .. } => input.clone(),
2496 Op::OneHot { .. } => input.clone(),
2497
2498 Op::NegativeLogLikelihoodLoss { .. } | Op::SoftmaxCrossEntropyLoss { .. } => {
2499 vec![Some(1)]
2500 }
2501
2502 Op::Dft { onesided, .. } => {
2503 // DFT last dim: full=N, onesided=N/2+1. Approximate.
2504 if *onesided && input.len() >= 2 {
2505 let mut out = input.clone();
2506 *out.last_mut().unwrap() = None;
2507 out
2508 } else {
2509 input.clone()
2510 }
2511 }
2512
2513 Op::Stft
2514 | Op::MelWeightMatrix
2515 | Op::HannWindow { .. }
2516 | Op::BlackmanWindow { .. }
2517 | Op::HammingWindow { .. } => input.clone(),
2518
2519 Op::SequenceAt
2520 | Op::SequenceConstruct
2521 | Op::SequenceErase
2522 | Op::SequenceInsert
2523 | Op::SequenceLength
2524 | Op::SequenceMap
2525 | Op::SplitToSequence { .. }
2526 | Op::ConcatFromSequence { .. }
2527 | Op::OptionalGetElement
2528 | Op::Loop
2529 | Op::Scan { .. }
2530 | Op::If
2531 | Op::Adagrad
2532 | Op::Adam
2533 | Op::Momentum
2534 | Op::Gradient
2535 | Op::StringNormalizer
2536 | Op::RegexFullMatch { .. }
2537 | Op::StringConcat
2538 | Op::StringSplit
2539 | Op::TfIdfVectorizer
2540 | Op::LabelEncoder
2541 | Op::ArrayFeatureExtractor
2542 | Op::Binarizer { .. }
2543 | Op::TreeEnsemble
2544 | Op::ImageDecoder => input.clone(),
2545
2546 // Handled by early returns above the match; arms required for exhaustiveness.
2547 Op::Constant { shape, .. } => shape.clone(),
2548 Op::SequenceEmpty | Op::OptionalHasElement => vec![],
2549 }
2550}
2551
2552// ---------------------------------------------------------------------------
2553// SymTensor — a tensor that writes to the graph on every operation
2554// ---------------------------------------------------------------------------
2555
2556/// A symbolic tensor handle. Every layer operation on a `SymTensor` records
2557/// itself in the shared `Graph` and returns a new `SymTensor` pointing to
2558/// the new node. Cloning is cheap — it shares the graph via `Rc`.
2559#[derive(Clone)]
2560pub struct SymTensor {
2561 /// This tensor's node index in `graph`.
2562 pub node_id: usize,
2563 /// The shared graph this tensor's operations record into.
2564 pub graph: Rc<RefCell<Graph>>,
2565 /// This tensor's dtype.
2566 pub dtype: DtypeRepr,
2567 /// Output shape of this tensor. `None` in a slot means a dynamic/unknown
2568 /// dimension (e.g. the batch axis).
2569 pub shape: Shape,
2570}
2571
2572// SymTensor satisfies Tensor<D, RANK> for any D and RANK — shape is tracked
2573// dynamically at runtime; the compile-time SHAPE constant is zeroed (unused).
2574impl<D: Dtype, const RANK: usize> RankedTensor<D, RANK> for SymTensor {
2575 const SHAPE: [usize; RANK] = [0; RANK];
2576}
2577impl<D: Dtype, const RANK: usize> Tensor<D, RANK> for SymTensor {}
2578
2579impl SymTensor {
2580 /// Create an input placeholder, returning both the tensor and the shared
2581 /// graph handle. Keep the graph handle to inspect the result after tracing.
2582 ///
2583 /// Use `None` for dynamic dimensions (e.g. the batch axis):
2584 /// ```ignore
2585 /// SymTensor::input(DtypeRepr::F32, vec![None, Some(784)])
2586 /// ```
2587 pub fn input(dtype: DtypeRepr, shape: Shape) -> (Self, Rc<RefCell<Graph>>) {
2588 let graph = Rc::new(RefCell::new(Graph::new()));
2589 let node_id = graph
2590 .borrow_mut()
2591 .add_node(Op::Input, vec![], dtype, shape.clone());
2592 let tensor = Self {
2593 node_id,
2594 graph: graph.clone(),
2595 dtype,
2596 shape,
2597 };
2598 (tensor, graph)
2599 }
2600
2601 /// Number of dimensions of this tensor.
2602 pub fn rank(&self) -> usize {
2603 self.shape.len()
2604 }
2605
2606 fn record(&self, op: Op) -> Self {
2607 let output_shape = infer_output_shape(&op, &[&self.shape]);
2608 self.record_with_shape(op, output_shape)
2609 }
2610
2611 fn record_with_shape(&self, op: Op, shape: Shape) -> Self {
2612 let node_id =
2613 self.graph
2614 .borrow_mut()
2615 .add_node(op, vec![self.node_id], self.dtype, shape.clone());
2616 Self {
2617 node_id,
2618 graph: self.graph.clone(),
2619 dtype: self.dtype,
2620 shape,
2621 }
2622 }
2623
2624 /// Record a custom op whose output shape is determined by [`CustomOp::infer_output_shape`].
2625 ///
2626 /// `self` is the primary (first) input. Pass additional inputs via
2627 /// `other_inputs`. Pass `dtype` to override the output element type;
2628 /// defaults to the primary input's dtype.
2629 pub fn record_custom(
2630 &self,
2631 data: CustomData,
2632 other_inputs: &[&SymTensor],
2633 dtype: Option<DtypeRepr>,
2634 ) -> Self {
2635 let mut shapes: Vec<&Shape> = vec![&self.shape];
2636 shapes.extend(other_inputs.iter().map(|t| &t.shape));
2637 let output_shape = data.infer_output_shape(&shapes);
2638
2639 let mut input_ids: Vec<usize> = vec![self.node_id];
2640 input_ids.extend(other_inputs.iter().map(|t| t.node_id));
2641
2642 let out_dtype = dtype.unwrap_or(self.dtype);
2643 let node_id = self.graph.borrow_mut().add_node(
2644 Op::Custom { data },
2645 input_ids,
2646 out_dtype,
2647 output_shape.clone(),
2648 );
2649 Self {
2650 node_id,
2651 graph: self.graph.clone(),
2652 dtype: out_dtype,
2653 shape: output_shape,
2654 }
2655 }
2656}
2657
2658// ---------------------------------------------------------------------------
2659// Layer<SymTensor> impls — record op instead of computing
2660// ---------------------------------------------------------------------------
2661
2662// --- Linear / MLP ---
2663
2664impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Linear<D, SymTensor, SymTensor, RANK> {
2665 type Output = SymTensor;
2666 fn call(&self, input: SymTensor) -> SymTensor {
2667 input.record(Op::Linear {
2668 in_features: self.in_features,
2669 out_features: self.out_features,
2670 has_bias: self.has_bias,
2671 })
2672 }
2673}
2674
2675impl<D: Dtype> Layer<SymTensor> for Flatten<D, SymTensor, SymTensor> {
2676 type Output = SymTensor;
2677 fn call(&self, input: SymTensor) -> SymTensor {
2678 input.record(Op::Flatten)
2679 }
2680}
2681
2682// --- Normalisation ---
2683
2684impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm1d<D, SymTensor, SymTensor, RANK> {
2685 type Output = SymTensor;
2686 fn call(&self, input: SymTensor) -> SymTensor {
2687 input.record(Op::BatchNorm1d {
2688 num_features: self.num_features,
2689 eps: self.eps,
2690 momentum: self.momentum,
2691 affine: self.affine,
2692 track_running_stats: self.track_running_stats,
2693 })
2694 }
2695}
2696
2697impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm2d<D, SymTensor, SymTensor, RANK> {
2698 type Output = SymTensor;
2699 fn call(&self, input: SymTensor) -> SymTensor {
2700 input.record(Op::BatchNorm2d {
2701 num_features: self.num_features,
2702 eps: self.eps,
2703 momentum: self.momentum,
2704 affine: self.affine,
2705 track_running_stats: self.track_running_stats,
2706 })
2707 }
2708}
2709
2710impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm3d<D, SymTensor, SymTensor, RANK> {
2711 type Output = SymTensor;
2712 fn call(&self, input: SymTensor) -> SymTensor {
2713 input.record(Op::BatchNorm3d {
2714 num_features: self.num_features,
2715 eps: self.eps,
2716 momentum: self.momentum,
2717 affine: self.affine,
2718 track_running_stats: self.track_running_stats,
2719 })
2720 }
2721}
2722
2723impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LayerNorm<D, SymTensor, SymTensor, RANK> {
2724 type Output = SymTensor;
2725 fn call(&self, input: SymTensor) -> SymTensor {
2726 input.record(Op::LayerNorm {
2727 normalized_shape: self.normalized_shape.clone(),
2728 eps: self.eps,
2729 affine: self.affine,
2730 })
2731 }
2732}
2733
2734impl<D: Dtype, const RANK: usize> Layer<SymTensor> for RmsNorm<D, SymTensor, SymTensor, RANK> {
2735 type Output = SymTensor;
2736 fn call(&self, input: SymTensor) -> SymTensor {
2737 input.record(Op::RmsNorm {
2738 normalized_shape: self.normalized_shape.clone(),
2739 eps: self.eps,
2740 affine: self.affine,
2741 })
2742 }
2743}
2744
2745impl<D: Dtype, const RANK: usize> Layer<SymTensor> for GroupNorm<D, SymTensor, SymTensor, RANK> {
2746 type Output = SymTensor;
2747 fn call(&self, input: SymTensor) -> SymTensor {
2748 input.record(Op::GroupNorm {
2749 num_groups: self.num_groups,
2750 num_channels: self.num_channels,
2751 eps: self.eps,
2752 affine: self.affine,
2753 })
2754 }
2755}
2756
2757impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2758 for InstanceNorm1d<D, SymTensor, SymTensor, RANK>
2759{
2760 type Output = SymTensor;
2761 fn call(&self, input: SymTensor) -> SymTensor {
2762 input.record(Op::InstanceNorm1d {
2763 num_features: self.num_features,
2764 eps: self.eps,
2765 momentum: self.momentum,
2766 affine: self.affine,
2767 track_running_stats: self.track_running_stats,
2768 })
2769 }
2770}
2771
2772impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2773 for InstanceNorm2d<D, SymTensor, SymTensor, RANK>
2774{
2775 type Output = SymTensor;
2776 fn call(&self, input: SymTensor) -> SymTensor {
2777 input.record(Op::InstanceNorm2d {
2778 num_features: self.num_features,
2779 eps: self.eps,
2780 momentum: self.momentum,
2781 affine: self.affine,
2782 track_running_stats: self.track_running_stats,
2783 })
2784 }
2785}
2786
2787impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2788 for InstanceNorm3d<D, SymTensor, SymTensor, RANK>
2789{
2790 type Output = SymTensor;
2791 fn call(&self, input: SymTensor) -> SymTensor {
2792 input.record(Op::InstanceNorm3d {
2793 num_features: self.num_features,
2794 eps: self.eps,
2795 momentum: self.momentum,
2796 affine: self.affine,
2797 track_running_stats: self.track_running_stats,
2798 })
2799 }
2800}
2801
2802// --- Convolution ---
2803
2804impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv1d<D, SymTensor, SymTensor, RANK> {
2805 type Output = SymTensor;
2806 fn call(&self, input: SymTensor) -> SymTensor {
2807 input.record(Op::Conv1d {
2808 in_channels: self.in_channels,
2809 out_channels: self.out_channels,
2810 kernel_l: self.kernel_l,
2811 stride: self.stride,
2812 padding: self.padding,
2813 has_bias: self.has_bias,
2814 })
2815 }
2816}
2817
2818impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv2d<D, SymTensor, SymTensor, RANK> {
2819 type Output = SymTensor;
2820 fn call(&self, input: SymTensor) -> SymTensor {
2821 input.record(Op::Conv2d {
2822 in_channels: self.in_channels,
2823 out_channels: self.out_channels,
2824 kernel_h: self.kernel_h,
2825 kernel_w: self.kernel_w,
2826 stride_h: self.stride_h,
2827 stride_w: self.stride_w,
2828 padding_h: self.padding_h,
2829 padding_w: self.padding_w,
2830 groups: self.groups,
2831 has_bias: self.has_bias,
2832 })
2833 }
2834}
2835
2836impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv3d<D, SymTensor, SymTensor, RANK> {
2837 type Output = SymTensor;
2838 fn call(&self, input: SymTensor) -> SymTensor {
2839 input.record(Op::Conv3d {
2840 in_channels: self.in_channels,
2841 out_channels: self.out_channels,
2842 kernel_d: self.kernel_d,
2843 kernel_h: self.kernel_h,
2844 kernel_w: self.kernel_w,
2845 stride_d: self.stride_d,
2846 stride_h: self.stride_h,
2847 stride_w: self.stride_w,
2848 padding_d: self.padding_d,
2849 padding_h: self.padding_h,
2850 padding_w: self.padding_w,
2851 has_bias: self.has_bias,
2852 })
2853 }
2854}
2855
2856// --- Pooling ---
2857
2858impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool1d<D, SymTensor, SymTensor, RANK> {
2859 type Output = SymTensor;
2860 fn call(&self, input: SymTensor) -> SymTensor {
2861 input.record(Op::AvgPool1d {
2862 kernel_l: self.kernel_l,
2863 stride: self.stride,
2864 })
2865 }
2866}
2867
2868impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool2d<D, SymTensor, SymTensor, RANK> {
2869 type Output = SymTensor;
2870 fn call(&self, input: SymTensor) -> SymTensor {
2871 input.record(Op::AvgPool2d {
2872 kernel_h: self.kernel_h,
2873 kernel_w: self.kernel_w,
2874 stride_h: self.stride_h,
2875 stride_w: self.stride_w,
2876 })
2877 }
2878}
2879
2880impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool3d<D, SymTensor, SymTensor, RANK> {
2881 type Output = SymTensor;
2882 fn call(&self, input: SymTensor) -> SymTensor {
2883 input.record(Op::AvgPool3d {
2884 kernel_d: self.kernel_d,
2885 kernel_h: self.kernel_h,
2886 kernel_w: self.kernel_w,
2887 stride_d: self.stride_d,
2888 stride_h: self.stride_h,
2889 stride_w: self.stride_w,
2890 })
2891 }
2892}
2893
2894impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool1d<D, SymTensor, SymTensor, RANK> {
2895 type Output = SymTensor;
2896 fn call(&self, input: SymTensor) -> SymTensor {
2897 input.record(Op::MaxPool1d {
2898 kernel_l: self.kernel_l,
2899 stride: self.stride,
2900 })
2901 }
2902}
2903
2904impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool2d<D, SymTensor, SymTensor, RANK> {
2905 type Output = SymTensor;
2906 fn call(&self, input: SymTensor) -> SymTensor {
2907 input.record(Op::MaxPool2d {
2908 kernel_h: self.kernel_h,
2909 kernel_w: self.kernel_w,
2910 stride_h: self.stride_h,
2911 stride_w: self.stride_w,
2912 pad_h: self.padding_h,
2913 pad_w: self.padding_w,
2914 })
2915 }
2916}
2917
2918impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool3d<D, SymTensor, SymTensor, RANK> {
2919 type Output = SymTensor;
2920 fn call(&self, input: SymTensor) -> SymTensor {
2921 input.record(Op::MaxPool3d {
2922 kernel_d: self.kernel_d,
2923 kernel_h: self.kernel_h,
2924 kernel_w: self.kernel_w,
2925 stride_d: self.stride_d,
2926 stride_h: self.stride_h,
2927 stride_w: self.stride_w,
2928 })
2929 }
2930}
2931
2932impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool1d<D, SymTensor, SymTensor, RANK> {
2933 type Output = SymTensor;
2934 fn call(&self, input: SymTensor) -> SymTensor {
2935 input.record(Op::LpPool1d {
2936 kernel_l: self.kernel_l,
2937 stride: self.stride,
2938 p: self.p,
2939 })
2940 }
2941}
2942
2943impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool2d<D, SymTensor, SymTensor, RANK> {
2944 type Output = SymTensor;
2945 fn call(&self, input: SymTensor) -> SymTensor {
2946 input.record(Op::LpPool2d {
2947 kernel_h: self.kernel_h,
2948 kernel_w: self.kernel_w,
2949 stride_h: self.stride_h,
2950 stride_w: self.stride_w,
2951 p: self.p,
2952 })
2953 }
2954}
2955
2956impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool3d<D, SymTensor, SymTensor, RANK> {
2957 type Output = SymTensor;
2958 fn call(&self, input: SymTensor) -> SymTensor {
2959 input.record(Op::LpPool3d {
2960 kernel_d: self.kernel_d,
2961 kernel_h: self.kernel_h,
2962 kernel_w: self.kernel_w,
2963 stride_d: self.stride_d,
2964 stride_h: self.stride_h,
2965 stride_w: self.stride_w,
2966 p: self.p,
2967 })
2968 }
2969}
2970
2971// --- Padding ---
2972
2973impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2974 for ConstantPad1d<D, SymTensor, SymTensor, RANK>
2975{
2976 type Output = SymTensor;
2977 fn call(&self, input: SymTensor) -> SymTensor {
2978 input.record(Op::ConstantPad1d {
2979 pad_left: self.pad_left,
2980 pad_right: self.pad_right,
2981 value: self.value,
2982 })
2983 }
2984}
2985
2986impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2987 for ConstantPad2d<D, SymTensor, SymTensor, RANK>
2988{
2989 type Output = SymTensor;
2990 fn call(&self, input: SymTensor) -> SymTensor {
2991 input.record(Op::ConstantPad2d {
2992 pad_l: self.pad_l,
2993 pad_r: self.pad_r,
2994 pad_t: self.pad_t,
2995 pad_b: self.pad_b,
2996 value: self.value,
2997 })
2998 }
2999}
3000
3001impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3002 for ConstantPad3d<D, SymTensor, SymTensor, RANK>
3003{
3004 type Output = SymTensor;
3005 fn call(&self, input: SymTensor) -> SymTensor {
3006 input.record(Op::ConstantPad3d {
3007 pad_d1: self.pad_d1,
3008 pad_d2: self.pad_d2,
3009 pad_h1: self.pad_h1,
3010 pad_h2: self.pad_h2,
3011 pad_w1: self.pad_w1,
3012 pad_w2: self.pad_w2,
3013 value: self.value,
3014 })
3015 }
3016}
3017
3018impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3019 for ReflectionPad1d<D, SymTensor, SymTensor, RANK>
3020{
3021 type Output = SymTensor;
3022 fn call(&self, input: SymTensor) -> SymTensor {
3023 input.record(Op::ReflectionPad1d {
3024 pad_left: self.pad_left,
3025 pad_right: self.pad_right,
3026 })
3027 }
3028}
3029
3030impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3031 for ReflectionPad2d<D, SymTensor, SymTensor, RANK>
3032{
3033 type Output = SymTensor;
3034 fn call(&self, input: SymTensor) -> SymTensor {
3035 input.record(Op::ReflectionPad2d {
3036 pad_l: self.pad_l,
3037 pad_r: self.pad_r,
3038 pad_t: self.pad_t,
3039 pad_b: self.pad_b,
3040 })
3041 }
3042}
3043
3044impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3045 for ReflectionPad3d<D, SymTensor, SymTensor, RANK>
3046{
3047 type Output = SymTensor;
3048 fn call(&self, input: SymTensor) -> SymTensor {
3049 input.record(Op::ReflectionPad3d {
3050 pad_d1: self.pad_d1,
3051 pad_d2: self.pad_d2,
3052 pad_h1: self.pad_h1,
3053 pad_h2: self.pad_h2,
3054 pad_w1: self.pad_w1,
3055 pad_w2: self.pad_w2,
3056 })
3057 }
3058}
3059
3060impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3061 for ReplicationPad1d<D, SymTensor, SymTensor, RANK>
3062{
3063 type Output = SymTensor;
3064 fn call(&self, input: SymTensor) -> SymTensor {
3065 input.record(Op::ReplicationPad1d {
3066 pad_left: self.pad_left,
3067 pad_right: self.pad_right,
3068 })
3069 }
3070}
3071
3072impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3073 for ReplicationPad2d<D, SymTensor, SymTensor, RANK>
3074{
3075 type Output = SymTensor;
3076 fn call(&self, input: SymTensor) -> SymTensor {
3077 input.record(Op::ReplicationPad2d {
3078 pad_l: self.pad_l,
3079 pad_r: self.pad_r,
3080 pad_t: self.pad_t,
3081 pad_b: self.pad_b,
3082 })
3083 }
3084}
3085
3086impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3087 for ReplicationPad3d<D, SymTensor, SymTensor, RANK>
3088{
3089 type Output = SymTensor;
3090 fn call(&self, input: SymTensor) -> SymTensor {
3091 input.record(Op::ReplicationPad3d {
3092 pad_d1: self.pad_d1,
3093 pad_d2: self.pad_d2,
3094 pad_h1: self.pad_h1,
3095 pad_h2: self.pad_h2,
3096 pad_w1: self.pad_w1,
3097 pad_w2: self.pad_w2,
3098 })
3099 }
3100}
3101
3102impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3103 for CircularPad1d<D, SymTensor, SymTensor, RANK>
3104{
3105 type Output = SymTensor;
3106 fn call(&self, input: SymTensor) -> SymTensor {
3107 input.record(Op::CircularPad1d {
3108 pad_left: self.pad_left,
3109 pad_right: self.pad_right,
3110 })
3111 }
3112}
3113
3114impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3115 for CircularPad2d<D, SymTensor, SymTensor, RANK>
3116{
3117 type Output = SymTensor;
3118 fn call(&self, input: SymTensor) -> SymTensor {
3119 input.record(Op::CircularPad2d {
3120 pad_l: self.pad_l,
3121 pad_r: self.pad_r,
3122 pad_t: self.pad_t,
3123 pad_b: self.pad_b,
3124 })
3125 }
3126}
3127
3128impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3129 for CircularPad3d<D, SymTensor, SymTensor, RANK>
3130{
3131 type Output = SymTensor;
3132 fn call(&self, input: SymTensor) -> SymTensor {
3133 input.record(Op::CircularPad3d {
3134 pad_d1: self.pad_d1,
3135 pad_d2: self.pad_d2,
3136 pad_h1: self.pad_h1,
3137 pad_h2: self.pad_h2,
3138 pad_w1: self.pad_w1,
3139 pad_w2: self.pad_w2,
3140 })
3141 }
3142}
3143
3144// --- Activation ---
3145
3146impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Relu<D, SymTensor, RANK> {
3147 type Output = SymTensor;
3148 fn call(&self, input: SymTensor) -> SymTensor {
3149 input.record(Op::Relu)
3150 }
3151}
3152
3153impl<D: Float, const RANK: usize> Layer<SymTensor> for Elu<D, SymTensor, RANK> {
3154 type Output = SymTensor;
3155 fn call(&self, input: SymTensor) -> SymTensor {
3156 input.record(Op::Elu { alpha: self.alpha })
3157 }
3158}
3159
3160impl<D: Float, const RANK: usize> Layer<SymTensor> for Selu<D, SymTensor, RANK> {
3161 type Output = SymTensor;
3162 fn call(&self, input: SymTensor) -> SymTensor {
3163 input.record(Op::Selu)
3164 }
3165}
3166
3167impl<D: Float, const RANK: usize> Layer<SymTensor> for Celu<D, SymTensor, RANK> {
3168 type Output = SymTensor;
3169 fn call(&self, input: SymTensor) -> SymTensor {
3170 input.record(Op::Celu { alpha: self.alpha })
3171 }
3172}
3173
3174impl<D: Float, const RANK: usize> Layer<SymTensor> for Gelu<D, SymTensor, RANK> {
3175 type Output = SymTensor;
3176 fn call(&self, input: SymTensor) -> SymTensor {
3177 input.record(Op::Gelu)
3178 }
3179}
3180
3181impl<D: Float, const RANK: usize> Layer<SymTensor> for Mish<D, SymTensor, RANK> {
3182 type Output = SymTensor;
3183 fn call(&self, input: SymTensor) -> SymTensor {
3184 input.record(Op::Mish)
3185 }
3186}
3187
3188impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardtanh<D, SymTensor, RANK> {
3189 type Output = SymTensor;
3190 fn call(&self, input: SymTensor) -> SymTensor {
3191 input.record(Op::Hardtanh {
3192 min_val: self.min_val,
3193 max_val: self.max_val,
3194 })
3195 }
3196}
3197
3198impl<D: Float, const RANK: usize> Layer<SymTensor> for Relu6<D, SymTensor, RANK> {
3199 type Output = SymTensor;
3200 fn call(&self, input: SymTensor) -> SymTensor {
3201 input.record(Op::Relu6)
3202 }
3203}
3204
3205impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardsigmoid<D, SymTensor, RANK> {
3206 type Output = SymTensor;
3207 fn call(&self, input: SymTensor) -> SymTensor {
3208 input.record(Op::Hardsigmoid)
3209 }
3210}
3211
3212impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardswish<D, SymTensor, RANK> {
3213 type Output = SymTensor;
3214 fn call(&self, input: SymTensor) -> SymTensor {
3215 input.record(Op::Hardswish)
3216 }
3217}
3218
3219impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardshrink<D, SymTensor, RANK> {
3220 type Output = SymTensor;
3221 fn call(&self, input: SymTensor) -> SymTensor {
3222 input.record(Op::Hardshrink {
3223 lambda: self.lambda,
3224 })
3225 }
3226}
3227
3228impl<D: Float, const RANK: usize> Layer<SymTensor> for LeakyRelu<D, SymTensor, RANK> {
3229 type Output = SymTensor;
3230 fn call(&self, input: SymTensor) -> SymTensor {
3231 input.record(Op::LeakyRelu {
3232 negative_slope: self.negative_slope,
3233 })
3234 }
3235}
3236
3237impl<D: Float, const RANK: usize> Layer<SymTensor> for Threshold<D, SymTensor, RANK> {
3238 type Output = SymTensor;
3239 fn call(&self, input: SymTensor) -> SymTensor {
3240 input.record(Op::Threshold {
3241 threshold: self.threshold,
3242 value: self.value,
3243 })
3244 }
3245}
3246
3247impl<D: Float, const RANK: usize> Layer<SymTensor> for Softsign<D, SymTensor, RANK> {
3248 type Output = SymTensor;
3249 fn call(&self, input: SymTensor) -> SymTensor {
3250 input.record(Op::Softsign)
3251 }
3252}
3253
3254impl<D: Float, const RANK: usize> Layer<SymTensor> for Softshrink<D, SymTensor, RANK> {
3255 type Output = SymTensor;
3256 fn call(&self, input: SymTensor) -> SymTensor {
3257 input.record(Op::Softshrink {
3258 lambda: self.lambda,
3259 })
3260 }
3261}
3262
3263impl<D: Float, const RANK: usize> Layer<SymTensor> for Softplus<D, SymTensor, RANK> {
3264 type Output = SymTensor;
3265 fn call(&self, input: SymTensor) -> SymTensor {
3266 input.record(Op::Softplus {
3267 beta: self.beta,
3268 threshold: self.threshold,
3269 })
3270 }
3271}
3272
3273impl<D: Float, const RANK: usize> Layer<SymTensor> for Sigmoid<D, SymTensor, RANK> {
3274 type Output = SymTensor;
3275 fn call(&self, input: SymTensor) -> SymTensor {
3276 input.record(Op::Sigmoid)
3277 }
3278}
3279
3280impl<D: Float, const RANK: usize> Layer<SymTensor> for Silu<D, SymTensor, RANK> {
3281 type Output = SymTensor;
3282 fn call(&self, input: SymTensor) -> SymTensor {
3283 input.record(Op::Silu)
3284 }
3285}
3286
3287impl<D: Float, const RANK: usize> Layer<SymTensor> for Logsigmoid<D, SymTensor, RANK> {
3288 type Output = SymTensor;
3289 fn call(&self, input: SymTensor) -> SymTensor {
3290 input.record(Op::Logsigmoid)
3291 }
3292}
3293
3294impl<D: Float, const RANK: usize> Layer<SymTensor> for Tanh<D, SymTensor, RANK> {
3295 type Output = SymTensor;
3296 fn call(&self, input: SymTensor) -> SymTensor {
3297 input.record(Op::Tanh)
3298 }
3299}
3300
3301impl<D: Float, const RANK: usize> Layer<SymTensor> for Tanhshrink<D, SymTensor, RANK> {
3302 type Output = SymTensor;
3303 fn call(&self, input: SymTensor) -> SymTensor {
3304 input.record(Op::Tanhshrink)
3305 }
3306}
3307
3308impl<D: Float, const RANK: usize> Layer<SymTensor> for Softmax<D, SymTensor, RANK> {
3309 type Output = SymTensor;
3310 fn call(&self, input: SymTensor) -> SymTensor {
3311 input.record(Op::Softmax { dim: self.dim })
3312 }
3313}
3314
3315// ---------------------------------------------------------------------------
3316// Tests
3317// ---------------------------------------------------------------------------
3318
3319#[cfg(test)]
3320mod tests {
3321 use super::*;
3322 use crate::{
3323 nn::{
3324 activation::{relu::Relu, softmax::Softmax},
3325 conv2d::Conv2d,
3326 linear::Linear,
3327 },
3328 sequential,
3329 };
3330
3331 #[test]
3332 fn test_sequential_graph_extraction() {
3333 let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(784)]);
3334
3335 let model = sequential![
3336 Linear::<f32, SymTensor, SymTensor, 2>::new(784, 128, true),
3337 Relu::<f32, SymTensor, 2>::new(),
3338 Linear::<f32, SymTensor, SymTensor, 2>::new(128, 10, true),
3339 Softmax::<f32, SymTensor, 2>::new(1)
3340 ];
3341
3342 let _out = Layer::call(&model, input);
3343
3344 let g = graph.borrow();
3345 assert_eq!(g.nodes.len(), 5);
3346 assert!(matches!(g.nodes[0].op, Op::Input));
3347 assert_eq!(g.nodes[0].shape, vec![None, Some(784)]);
3348
3349 assert!(matches!(
3350 g.nodes[1].op,
3351 Op::Linear {
3352 in_features: 784,
3353 out_features: 128,
3354 ..
3355 }
3356 ));
3357 assert_eq!(g.nodes[1].shape, vec![None, Some(128)]);
3358
3359 assert!(matches!(g.nodes[2].op, Op::Relu));
3360 assert_eq!(g.nodes[2].shape, vec![None, Some(128)]);
3361
3362 assert!(matches!(
3363 g.nodes[3].op,
3364 Op::Linear {
3365 in_features: 128,
3366 out_features: 10,
3367 ..
3368 }
3369 ));
3370 assert_eq!(g.nodes[3].shape, vec![None, Some(10)]);
3371
3372 assert!(matches!(g.nodes[4].op, Op::Softmax { dim: 1 }));
3373 assert_eq!(g.nodes[4].shape, vec![None, Some(10)]);
3374 }
3375
3376 #[test]
3377 fn test_topological_sort_linear_chain() {
3378 let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(784)]);
3379
3380 let model = sequential![
3381 Linear::<f32, SymTensor, SymTensor, 2>::new(784, 128, true),
3382 Relu::<f32, SymTensor, 2>::new(),
3383 Linear::<f32, SymTensor, SymTensor, 2>::new(128, 10, true),
3384 Softmax::<f32, SymTensor, 2>::new(1)
3385 ];
3386
3387 let _out = Layer::call(&model, input);
3388
3389 let g = graph.borrow();
3390 let order = g.topological_sort();
3391 assert_eq!(order.len(), g.nodes.len());
3392 for (pos, &id) in order.iter().enumerate() {
3393 for &input_id in &g.nodes[id].inputs {
3394 let input_pos = order.iter().position(|&x| x == input_id).unwrap();
3395 assert!(
3396 input_pos < pos,
3397 "producer {input_id} must come before consumer {id}"
3398 );
3399 }
3400 }
3401 }
3402
3403 #[test]
3404 fn test_residual_graph_extraction() {
3405 let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(64)]);
3406
3407 let main = Linear::<f32, SymTensor, SymTensor, 2>::new(64, 64, true).call(input.clone());
3408 let main = Relu::<f32, SymTensor, 2>::new().call(main);
3409 let skip = Linear::<f32, SymTensor, SymTensor, 2>::new(64, 64, false).call(input);
3410
3411 assert!(Rc::ptr_eq(&main.graph, &skip.graph));
3412
3413 let g = graph.borrow();
3414 assert_eq!(g.nodes.len(), 4);
3415 assert_eq!(g.nodes[1].inputs, vec![0]);
3416 assert_eq!(g.nodes[3].inputs, vec![0]);
3417 }
3418
3419 #[test]
3420 fn test_conv2d_graph_extraction() {
3421 let (input, graph) =
3422 SymTensor::input(DtypeRepr::F32, vec![None, Some(3), Some(32), Some(32)]);
3423
3424 let conv = Conv2d::<f32, SymTensor, SymTensor, 4>::new(3, 64, (3, 3), (1, 1), (1, 1), true);
3425 let _out = Layer::call(&conv, input);
3426
3427 let g = graph.borrow();
3428 assert_eq!(g.nodes.len(), 2);
3429 assert!(matches!(
3430 g.nodes[1].op,
3431 Op::Conv2d {
3432 in_channels: 3,
3433 out_channels: 64,
3434 kernel_h: 3,
3435 kernel_w: 3,
3436 stride_h: 1,
3437 stride_w: 1,
3438 padding_h: 1,
3439 padding_w: 1,
3440 has_bias: true,
3441 ..
3442 }
3443 ));
3444 assert_eq!(g.nodes[1].shape, vec![None, Some(64), Some(32), Some(32)]);
3445 }
3446
3447 #[test]
3448 fn test_lenet5_shapes() {
3449 let (input, graph) =
3450 SymTensor::input(DtypeRepr::F32, vec![None, Some(1), Some(28), Some(28)]);
3451
3452 use crate::{
3453 nn::{flatten::Flatten, pool::AvgPool2d},
3454 sequential,
3455 };
3456
3457 let model = sequential![
3458 Conv2d::<f32, SymTensor, SymTensor, 4>::new(1, 6, (5, 5), (1, 1), (2, 2), true),
3459 Relu::<f32, SymTensor, 4>::new(),
3460 AvgPool2d::<f32, SymTensor, SymTensor, 4>::new((2, 2), (2, 2)),
3461 Conv2d::<f32, SymTensor, SymTensor, 4>::new(6, 16, (5, 5), (1, 1), (0, 0), true),
3462 Relu::<f32, SymTensor, 4>::new(),
3463 AvgPool2d::<f32, SymTensor, SymTensor, 4>::new((2, 2), (2, 2)),
3464 Flatten::<f32, SymTensor, SymTensor>::new(),
3465 Linear::<f32, SymTensor, SymTensor, 2>::new(400, 120, true),
3466 Relu::<f32, SymTensor, 2>::new(),
3467 Linear::<f32, SymTensor, SymTensor, 2>::new(120, 84, true),
3468 Relu::<f32, SymTensor, 2>::new(),
3469 Linear::<f32, SymTensor, SymTensor, 2>::new(84, 10, true),
3470 Softmax::<f32, SymTensor, 2>::new(1)
3471 ];
3472
3473 let _out = Layer::call(&model, input);
3474
3475 let g = graph.borrow();
3476 assert_eq!(g.nodes.len(), 14);
3477 assert_eq!(g.nodes[0].shape, vec![None, Some(1), Some(28), Some(28)]);
3478 assert_eq!(g.nodes[1].shape, vec![None, Some(6), Some(28), Some(28)]);
3479 assert_eq!(g.nodes[2].shape, vec![None, Some(6), Some(28), Some(28)]);
3480 assert_eq!(g.nodes[3].shape, vec![None, Some(6), Some(14), Some(14)]);
3481 assert_eq!(g.nodes[4].shape, vec![None, Some(16), Some(10), Some(10)]);
3482 assert_eq!(g.nodes[5].shape, vec![None, Some(16), Some(10), Some(10)]);
3483 assert_eq!(g.nodes[6].shape, vec![None, Some(16), Some(5), Some(5)]);
3484 assert_eq!(g.nodes[7].shape, vec![None, Some(400)]);
3485 assert_eq!(g.nodes[8].shape, vec![None, Some(120)]);
3486 assert_eq!(g.nodes[9].shape, vec![None, Some(120)]);
3487 assert_eq!(g.nodes[10].shape, vec![None, Some(84)]);
3488 assert_eq!(g.nodes[11].shape, vec![None, Some(84)]);
3489 assert_eq!(g.nodes[12].shape, vec![None, Some(10)]);
3490 assert_eq!(g.nodes[13].shape, vec![None, Some(10)]);
3491 }
3492}