1use serde::Serialize;
7
8pub const AUDITED_CANDLE_VERSION: &str = "0.11.0";
10
11pub fn is_audited_candle_version(version: &str) -> bool {
12 version == AUDITED_CANDLE_VERSION
13}
14
15pub fn matched_candle_version<'a>(
20 candle_core_version: Option<&'a str>,
21 candle_nn_version: Option<&'a str>,
22) -> Option<&'a str> {
23 match (candle_core_version, candle_nn_version) {
24 (Some(core), Some(nn)) if core == nn && is_audited_candle_version(nn) => Some(nn),
25 _ => None,
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum AbstractDtype {
33 F64,
34 F32,
35 F16,
36 Bf16,
37 I16,
38 I32,
39 I64,
40 U32,
41 U8,
42 F8E4M3,
43 F6E2M3,
44 F6E3M2,
45 F4,
46 F8E8M0,
47 Unknown,
49}
50
51impl AbstractDtype {
52 pub fn parse(text: &str) -> Self {
53 let t = text.trim();
54 let t = t.rsplit("::").next().unwrap_or(t);
55 match t {
56 "F64" | "f64" => Self::F64,
57 "F32" | "f32" => Self::F32,
58 "F16" | "f16" => Self::F16,
59 "BF16" | "Bf16" | "bf16" => Self::Bf16,
60 "I16" | "i16" => Self::I16,
61 "I32" | "i32" => Self::I32,
62 "I64" | "i64" => Self::I64,
63 "U32" | "u32" => Self::U32,
64 "U8" | "u8" => Self::U8,
65 "F8E4M3" => Self::F8E4M3,
66 "F6E2M3" => Self::F6E2M3,
67 "F6E3M2" => Self::F6E3M2,
68 "F4" => Self::F4,
69 "F8E8M0" => Self::F8E8M0,
70 _ => Self::Unknown,
71 }
72 }
73
74 pub fn is_known(self) -> bool {
75 !matches!(self, Self::Unknown)
76 }
77}
78
79impl std::fmt::Display for AbstractDtype {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::F64 => write!(f, "F64"),
83 Self::F32 => write!(f, "F32"),
84 Self::F16 => write!(f, "F16"),
85 Self::Bf16 => write!(f, "BF16"),
86 Self::I16 => write!(f, "I16"),
87 Self::I32 => write!(f, "I32"),
88 Self::I64 => write!(f, "I64"),
89 Self::U32 => write!(f, "U32"),
90 Self::U8 => write!(f, "U8"),
91 Self::F8E4M3 => write!(f, "F8E4M3"),
92 Self::F6E2M3 => write!(f, "F6E2M3"),
93 Self::F6E3M2 => write!(f, "F6E3M2"),
94 Self::F4 => write!(f, "F4"),
95 Self::F8E8M0 => write!(f, "F8E8M0"),
96 Self::Unknown => write!(f, "Unknown"),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
103#[serde(rename_all = "snake_case")]
104pub enum GradFlow {
105 Propagates,
107 Severs,
109 LayoutDependent,
111 Unknown,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
117#[serde(rename_all = "snake_case")]
118pub enum DtypeRule {
119 Preserve,
121 SameAsInputs,
123 Explicit,
125 Fixed(AbstractDtype),
127 Unknown,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
133#[serde(rename_all = "snake_case")]
134pub enum NumericDomain {
135 Real,
136 NonNegative,
137 SaturatingUnit,
139 StrictlyPositive,
141 Unknown,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
146#[serde(rename_all = "snake_case")]
147pub enum DomainRequirement {
148 StrictlyPositive,
149 NonZero,
150 NonNegative,
151 None,
152}
153
154pub const SIGMOID_F32_UPPER_SATURATION: f32 = 16.6355;
157pub const SIGMOID_F32_LOWER_SATURATION: f32 = -88.7228;
158
159#[derive(Debug, Clone, Copy, PartialEq)]
161pub enum BodyAtom {
162 Arg(usize),
164 Assume {
166 src: u16,
167 domain: NumericDomain,
168 },
169 Unary {
170 op: &'static str,
171 src: u16,
172 },
173 Binary {
174 op: &'static str,
175 left: u16,
176 right: u16,
177 },
178 Affine {
180 src: u16,
181 mul: f64,
182 add: f64,
183 },
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
188pub struct LibraryBody {
189 pub cite: &'static str,
190 pub steps: &'static [BodyAtom],
191}
192
193const BCE_WITH_LOGITS_011: LibraryBody = LibraryBody {
196 cite: "candle-nn-0.11.0/src/loss.rs:64-74",
197 steps: &[
198 BodyAtom::Arg(0), BodyAtom::Arg(1), BodyAtom::Assume {
202 src: 1,
203 domain: NumericDomain::SaturatingUnit,
204 }, BodyAtom::Unary {
206 op: "sigmoid",
207 src: 0,
208 }, BodyAtom::Unary { op: "log", src: 3 }, BodyAtom::Binary {
211 op: "mul",
212 left: 2,
213 right: 4,
214 }, BodyAtom::Affine {
216 src: 3,
217 mul: -1.0,
218 add: 1.0,
219 }, BodyAtom::Unary { op: "log", src: 6 }, BodyAtom::Affine {
222 src: 2,
223 mul: -1.0,
224 add: 1.0,
225 }, BodyAtom::Binary {
227 op: "mul",
228 left: 8,
229 right: 7,
230 }, BodyAtom::Binary {
232 op: "add",
233 left: 5,
234 right: 9,
235 }, ],
237};
238
239pub fn library_body(op: &str, candle_nn_version: Option<&str>) -> Option<&'static LibraryBody> {
244 if !candle_nn_version.is_some_and(is_audited_candle_version) {
245 return None;
246 }
247 let name = op.rsplit("::").next().unwrap_or(op);
248 match name {
249 "binary_cross_entropy_with_logit" => Some(&BCE_WITH_LOGITS_011),
250 _ => None,
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
255pub struct OpEffect {
256 pub name: String,
257 pub dtype: DtypeRule,
258 pub grad: GradFlow,
259 pub is_loss: bool,
261 pub note: Option<&'static str>,
263 pub domain: NumericDomain,
265 pub requires: DomainRequirement,
267}
268
269impl OpEffect {
270 fn known(
271 name: &str,
272 dtype: DtypeRule,
273 grad: GradFlow,
274 is_loss: bool,
275 note: Option<&'static str>,
276 ) -> Self {
277 Self {
278 name: name.to_string(),
279 dtype,
280 grad,
281 is_loss,
282 note,
283 domain: NumericDomain::Real,
284 requires: DomainRequirement::None,
285 }
286 }
287
288 fn with_domain(mut self, domain: NumericDomain) -> Self {
289 self.domain = domain;
290 self
291 }
292
293 fn with_requires(mut self, requires: DomainRequirement) -> Self {
294 self.requires = requires;
295 self
296 }
297
298 pub fn unknown(name: &str) -> Self {
299 Self {
300 name: name.to_string(),
301 dtype: DtypeRule::Unknown,
302 grad: GradFlow::Unknown,
303 is_loss: false,
304 note: Some("no transfer rule; left Unknown"),
305 domain: NumericDomain::Unknown,
306 requires: DomainRequirement::None,
307 }
308 }
309
310 pub fn domain_rule_label(&self) -> String {
311 match self.domain {
312 NumericDomain::Real => "real".into(),
313 NumericDomain::NonNegative => "non_negative".into(),
314 NumericDomain::SaturatingUnit => format!(
315 "saturating_unit(f32_upper={SIGMOID_F32_UPPER_SATURATION},f32_lower={SIGMOID_F32_LOWER_SATURATION})"
316 ),
317 NumericDomain::StrictlyPositive => "strictly_positive".into(),
318 NumericDomain::Unknown => "unknown".into(),
319 }
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum DomainViolationConfidence {
326 Proven,
327 Unknown,
328}
329
330pub fn domain_violation(
335 requires: DomainRequirement,
336 domain: NumericDomain,
337) -> Option<DomainViolationConfidence> {
338 match requires {
339 DomainRequirement::None => None,
340 DomainRequirement::StrictlyPositive => match domain {
341 NumericDomain::StrictlyPositive => None,
342 NumericDomain::Unknown => Some(DomainViolationConfidence::Unknown),
343 NumericDomain::Real | NumericDomain::NonNegative | NumericDomain::SaturatingUnit => {
344 Some(DomainViolationConfidence::Proven)
345 }
346 },
347 DomainRequirement::NonZero => match domain {
348 NumericDomain::StrictlyPositive => None,
349 NumericDomain::Unknown => Some(DomainViolationConfidence::Unknown),
350 NumericDomain::Real | NumericDomain::NonNegative | NumericDomain::SaturatingUnit => {
351 Some(DomainViolationConfidence::Proven)
352 }
353 },
354 DomainRequirement::NonNegative => match domain {
355 NumericDomain::NonNegative
356 | NumericDomain::StrictlyPositive
357 | NumericDomain::SaturatingUnit => None,
358 NumericDomain::Unknown => Some(DomainViolationConfidence::Unknown),
359 NumericDomain::Real => Some(DomainViolationConfidence::Proven),
360 },
361 }
362}
363
364pub fn join_domain(left: NumericDomain, right: NumericDomain) -> NumericDomain {
366 use NumericDomain::*;
367 match (left, right) {
368 (Unknown, _) | (_, Unknown) => Unknown,
369 (StrictlyPositive, StrictlyPositive) => StrictlyPositive,
370 (StrictlyPositive, NonNegative) | (NonNegative, StrictlyPositive) => NonNegative,
371 (NonNegative, NonNegative) => NonNegative,
372 (SaturatingUnit, SaturatingUnit) => SaturatingUnit,
373 (SaturatingUnit, NonNegative) | (NonNegative, SaturatingUnit) => NonNegative,
374 (SaturatingUnit, StrictlyPositive) | (StrictlyPositive, SaturatingUnit) => NonNegative,
375 _ => Real,
376 }
377}
378
379pub fn affine_domain(operand: NumericDomain, mul: Option<f64>, add: Option<f64>) -> NumericDomain {
385 match (mul, add) {
386 (Some(m), Some(a)) if m > 0.0 && a > 0.0 => match operand {
387 NumericDomain::SaturatingUnit
388 | NumericDomain::NonNegative
389 | NumericDomain::StrictlyPositive => NumericDomain::StrictlyPositive,
390 NumericDomain::Unknown => NumericDomain::Unknown,
391 NumericDomain::Real => NumericDomain::Real,
392 },
393 _ => match operand {
394 NumericDomain::SaturatingUnit => NumericDomain::SaturatingUnit,
396 NumericDomain::StrictlyPositive => NumericDomain::Real,
397 NumericDomain::NonNegative => NumericDomain::Real,
398 NumericDomain::Unknown => NumericDomain::Unknown,
399 NumericDomain::Real => NumericDomain::Real,
400 },
401 }
402}
403
404pub fn domain_includes_zero(domain: NumericDomain) -> bool {
405 matches!(
406 domain,
407 NumericDomain::NonNegative | NumericDomain::SaturatingUnit | NumericDomain::Real
408 )
409}
410
411pub fn lookup(op: &str) -> OpEffect {
413 lookup_for(op, None)
416}
417
418pub fn lookup_for(op: &str, candle_nn_version: Option<&str>) -> OpEffect {
424 let name = op.rsplit("::").next().unwrap_or(op);
425 let audited_no_bwd = matches!(
426 name,
427 "softmax_last_dim"
428 | "rms_norm"
429 | "layer_norm"
430 | "sdpa"
431 | "rope"
432 | "rope_i"
433 | "rope_thd"
434 | "flash_attn"
435 | "flash_attn_varlen_cpu"
436 | "flash_attn_varlen_unfused"
437 | "run_flash_attn_cpu"
438 );
439 if audited_no_bwd && !candle_nn_version.is_some_and(is_audited_candle_version) {
440 return OpEffect::unknown(name);
441 }
442 match name {
443 "to_dtype" => OpEffect::known(
445 name,
446 DtypeRule::Explicit,
447 GradFlow::Propagates,
448 false,
449 Some("candle Tensor::to_dtype"),
450 ),
451
452 "add" | "sub" | "mul" | "maximum" | "minimum" => OpEffect::known(
454 name,
455 DtypeRule::SameAsInputs,
456 GradFlow::Propagates,
457 false,
458 Some("candle binary op; operands must share dtype"),
459 ),
460 "div" => OpEffect::known(
461 name,
462 DtypeRule::SameAsInputs,
463 GradFlow::Propagates,
464 false,
465 Some("candle binary op; operands must share dtype"),
466 )
467 .with_requires(DomainRequirement::NonZero),
468 "broadcast_add" | "broadcast_sub" | "broadcast_mul" | "broadcast_maximum"
469 | "broadcast_minimum" | "broadcast_pow" => OpEffect::known(
470 name,
471 DtypeRule::SameAsInputs,
472 GradFlow::Propagates,
473 false,
474 Some("candle broadcast binary; operands must share dtype"),
475 ),
476 "broadcast_div" => OpEffect::known(
477 name,
478 DtypeRule::SameAsInputs,
479 GradFlow::Propagates,
480 false,
481 Some("candle broadcast binary; operands must share dtype"),
482 )
483 .with_requires(DomainRequirement::NonZero),
484 "matmul" | "broadcast_matmul" => OpEffect::known(
485 name,
486 DtypeRule::SameAsInputs,
487 GradFlow::Propagates,
488 false,
489 Some("candle matmul; operands must share dtype"),
490 ),
491
492 "neg" | "sin" | "cos" | "tanh" | "gelu" | "silu" | "erf" => OpEffect::known(
494 name,
495 DtypeRule::Preserve,
496 GradFlow::Propagates,
497 false,
498 Some("dtype-preserving unary"),
499 ),
500 "abs" | "sqr" | "relu" => OpEffect::known(
501 name,
502 DtypeRule::Preserve,
503 GradFlow::Propagates,
504 false,
505 Some("dtype-preserving unary"),
506 )
507 .with_domain(NumericDomain::NonNegative),
508 "exp" => OpEffect::known(
509 name,
510 DtypeRule::Preserve,
511 GradFlow::Propagates,
512 false,
513 Some("dtype-preserving unary; f32 underflows to exactly 0.0"),
514 )
515 .with_domain(NumericDomain::NonNegative),
516 "sqrt" => OpEffect::known(
517 name,
518 DtypeRule::Preserve,
519 GradFlow::Propagates,
520 false,
521 Some("dtype-preserving unary"),
522 )
523 .with_domain(NumericDomain::NonNegative)
524 .with_requires(DomainRequirement::NonNegative),
525 "log" => OpEffect::known(
526 name,
527 DtypeRule::Preserve,
528 GradFlow::Propagates,
529 false,
530 Some("dtype-preserving unary; undefined at 0"),
531 )
532 .with_requires(DomainRequirement::StrictlyPositive),
533 "recip" => OpEffect::known(
534 name,
535 DtypeRule::Preserve,
536 GradFlow::Propagates,
537 false,
538 Some("dtype-preserving unary; undefined at 0"),
539 )
540 .with_requires(DomainRequirement::NonZero),
541 "floor" | "round" | "sign" => OpEffect::known(
542 name,
543 DtypeRule::Preserve,
544 GradFlow::Severs,
545 false,
546 Some("candle operation is created without a backward rule"),
547 ),
548 "ceil" => OpEffect::known(
549 name,
550 DtypeRule::Preserve,
551 GradFlow::Unknown,
552 false,
553 Some("candle backward reports this operation as unsupported"),
554 ),
555 "cmp" | "eq" | "ne" | "lt" | "gt" | "ge" | "le" => OpEffect::known(
556 name,
557 DtypeRule::Fixed(AbstractDtype::U8),
558 GradFlow::Severs,
559 false,
560 Some("candle-core 0.11.0 comparisons return U8 and do not propagate gradients"),
561 ),
562 "argmin" | "argmin_keepdim" | "argmax" | "argmax_keepdim" => OpEffect::known(
563 name,
564 DtypeRule::Fixed(AbstractDtype::U32),
565 GradFlow::Severs,
566 false,
567 Some("candle-core 0.11.0 index reductions return U32 without a backward op"),
568 ),
569
570 "sum" | "sum_keepdim" | "sum_all" | "mean" | "mean_keepdim" | "mean_all" | "max"
572 | "max_keepdim" | "min" | "min_keepdim" | "log_sum_exp" | "powf" | "elu" | "clamp" => {
573 OpEffect::known(
574 name,
575 DtypeRule::Preserve,
576 GradFlow::Propagates,
577 false,
578 Some("dtype-preserving reduction/affine"),
579 )
580 }
581 "affine" => OpEffect::known(
583 name,
584 DtypeRule::Preserve,
585 GradFlow::Propagates,
586 false,
587 Some("dtype-preserving affine; domain transfers through positive add"),
588 )
589 .with_domain(NumericDomain::Unknown),
590
591 "reshape" | "flatten_all" | "flatten_to" | "flatten_from" | "squeeze" | "unsqueeze"
593 | "transpose" | "permute" | "narrow" | "contiguous" | "broadcast_as" | "broadcast_left"
594 | "expand" | "t" => OpEffect::known(
595 name,
596 DtypeRule::Preserve,
597 GradFlow::Propagates,
598 false,
599 Some("layout/view operation with an explicit backward rule"),
600 ),
601
602 "detach" | "as_detached_tensor" => OpEffect::known(
604 name,
605 DtypeRule::Preserve,
606 GradFlow::Severs,
607 false,
608 Some("explicit detach"),
609 ),
610
611 "mse" => OpEffect::known(
613 name,
614 DtypeRule::Preserve,
615 GradFlow::Propagates,
616 true,
617 Some("candle_nn::loss::mse"),
618 ),
619 "nll" => OpEffect::known(
620 name,
621 DtypeRule::Preserve,
622 GradFlow::Propagates,
623 true,
624 Some("candle_nn::loss::nll expects log-probabilities"),
625 ),
626 "cross_entropy" => OpEffect::known(
627 name,
628 DtypeRule::Preserve,
629 GradFlow::Propagates,
630 true,
631 Some("candle_nn::loss::cross_entropy via log_softmax + nll"),
632 ),
633 "huber" => OpEffect::known(
634 name,
635 DtypeRule::Preserve,
636 GradFlow::Propagates,
637 true,
638 Some("candle_nn::loss::huber"),
639 ),
640 "binary_cross_entropy_with_logit" => OpEffect::known(
641 name,
642 DtypeRule::Preserve,
643 GradFlow::Propagates,
644 true,
645 Some(
646 "candle_nn::loss::binary_cross_entropy_with_logit; body expanded when \
647 Candle 0.11.0 is resolved — see library_body()",
648 ),
649 ),
650
651 "softmax_last_dim" => OpEffect::known(
653 name,
654 DtypeRule::Preserve,
655 GradFlow::Severs,
656 false,
657 Some("candle_nn::ops::softmax_last_dim uses apply_op1_no_bwd"),
658 )
659 .with_domain(NumericDomain::SaturatingUnit),
660 "rms_norm" => OpEffect::known(
661 name,
662 DtypeRule::Preserve,
663 GradFlow::Severs,
664 false,
665 Some("candle_nn::ops::rms_norm uses apply_op2_no_bwd"),
666 ),
667 "layer_norm" => OpEffect::known(
668 name,
669 DtypeRule::Preserve,
670 GradFlow::Severs,
671 false,
672 Some("candle_nn::ops::layer_norm uses apply_op3_no_bwd"),
673 ),
674 "sdpa" => OpEffect::known(
675 name,
676 DtypeRule::Preserve,
677 GradFlow::Severs,
678 false,
679 Some("candle_nn::ops::sdpa uses apply_op3_no_bwd"),
680 ),
681 "rope" | "rope_i" | "rope_thd" => OpEffect::known(
682 name,
683 DtypeRule::Preserve,
684 GradFlow::Severs,
685 false,
686 Some("candle_nn::rotary_emb::* uses apply_op3_no_bwd"),
687 ),
688 "flash_attn"
689 | "flash_attn_varlen_cpu"
690 | "flash_attn_varlen_unfused"
691 | "run_flash_attn_cpu" => OpEffect::known(
692 name,
693 DtypeRule::Preserve,
694 GradFlow::Severs,
695 false,
696 Some("candle-nn 0.11.0 CPU/varlen attention returns a detached output"),
697 ),
698
699 "rms_norm_slow" | "layer_norm_slow" | "rope_slow" | "rope_i_slow" => OpEffect::known(
701 name,
702 DtypeRule::Preserve,
703 GradFlow::Propagates,
704 false,
705 Some("candle_nn differentiable helper"),
706 ),
707 "sigmoid" => OpEffect::known(
708 name,
709 DtypeRule::Preserve,
710 GradFlow::Propagates,
711 false,
712 Some(
713 "candle_nn sigmoid saturates to exactly 0/1 in f32 \
714 (candle-nn-0.11.0/src/ops.rs:59-60)",
715 ),
716 )
717 .with_domain(NumericDomain::SaturatingUnit),
718 "softmax" | "log_softmax" => OpEffect::known(
719 name,
720 DtypeRule::Preserve,
721 GradFlow::Propagates,
722 false,
723 Some("candle_nn softmax family; probabilities attain 0/1 after f32 rounding"),
724 )
725 .with_domain(NumericDomain::SaturatingUnit),
726
727 "to_device" | "clone" => {
729 OpEffect::known(name, DtypeRule::Preserve, GradFlow::Propagates, false, None)
730 }
731
732 _ => OpEffect::unknown(name),
733 }
734}
735
736pub fn lookup_method(
743 receiver_type: &str,
744 method: &str,
745 candle_nn_version: Option<&str>,
746) -> OpEffect {
747 let receiver = receiver_type
748 .split('<')
749 .next()
750 .unwrap_or(receiver_type)
751 .rsplit("::")
752 .next()
753 .unwrap_or(receiver_type);
754 match (receiver, method, candle_nn_version) {
755 ("RmsNorm", "forward_diff", Some(AUDITED_CANDLE_VERSION)) => OpEffect::known(
756 "RmsNorm::forward_diff",
757 DtypeRule::Preserve,
758 GradFlow::Propagates,
759 false,
760 Some("audited candle-nn RmsNorm::forward_diff uses differentiable LayerNorm::forward"),
761 ),
762 ("RmsNorm", "forward", Some(AUDITED_CANDLE_VERSION)) => OpEffect::known(
763 "RmsNorm::forward",
764 DtypeRule::Preserve,
765 GradFlow::LayoutDependent,
766 false,
767 Some("audited candle-nn RmsNorm::forward uses a no-bwd kernel for contiguous input"),
768 ),
769 (
770 "Linear" | "Embedding" | "Conv1d" | "Conv2d" | "ConvTranspose1d" | "ConvTranspose2d"
771 | "PReLU" | "GroupNorm" | "BatchNorm",
772 "forward" | "forward_t",
773 Some(AUDITED_CANDLE_VERSION),
774 ) => OpEffect::known(
775 &format!("{receiver}::{method}"),
776 DtypeRule::Preserve,
777 GradFlow::Propagates,
778 false,
779 Some("known candle-nn parameterized module forward"),
780 ),
781 _ => OpEffect::unknown(&format!("{receiver}::{method}")),
782 }
783}
784
785pub fn lookup_resolved(op: &str, candle_nn_version: Option<&str>) -> OpEffect {
790 if let Some((receiver, method)) = op.rsplit_once("::") {
791 let method_effect = lookup_method(receiver, method, candle_nn_version);
792 if !matches!(method_effect.dtype, DtypeRule::Unknown)
793 || !matches!(method_effect.grad, GradFlow::Unknown)
794 {
795 return method_effect;
796 }
797 }
798 lookup_for(op, candle_nn_version)
799}
800
801#[allow(dead_code)]
803pub fn requires_same_dtype(op: &str) -> bool {
804 matches!(lookup(op).dtype, DtypeRule::SameAsInputs)
805}
806
807#[allow(dead_code)]
809pub fn is_no_backward(op: &str) -> bool {
810 matches!(lookup(op).grad, GradFlow::Severs)
811}