Skip to main content

tenferro_linalg/ad/
semantic.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use computegraph::traits::GraphOperation;
5use computegraph::types::{LocalValueId, OperationRole, ValueKey, ValueRef};
6use tenferro_ad::semantic_extension::{
7    AdValue, SemanticAdError, SemanticAdRuleRole, SemanticExtensionRegistryError,
8    SemanticExtensionRuleSet, SemanticLinearTransposeRequest, SemanticLinearTransposeRule,
9    SemanticLinearizeRequest, SemanticLinearizeResult, SemanticLinearizeRule,
10};
11use tenferro_ops::ad::PrimitiveRuleBuilder;
12use tenferro_ops::ad::PrimitiveTransposeInput;
13use tenferro_ops::dim_expr::DimExpr;
14use tenferro_ops::input_key::TensorInputKey;
15use tenferro_ops::shape_extent::ShapeExtent;
16use tenferro_ops::std_tensor_op::StdTensorOp;
17use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
18use tenferro_runtime::program::{
19    CoreSemanticOp, ProgramValue, ProgramValueMetadata, SemanticProgramBuilder,
20};
21
22use super::LinalgAdRule;
23use crate::extension::{LinalgExtensionOp, LinalgOp};
24use crate::LINALG_EXTENSION_FAMILY_ID;
25
26/// Return the linalg semantic-program AD rule set.
27///
28/// # Errors
29///
30/// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] if the linalg
31/// family identifier is invalid, or
32/// [`SemanticExtensionRegistryError::DuplicateRule`] if a semantic rule role
33/// is already registered.
34///
35/// # Examples
36///
37/// ```rust
38/// let rules = tenferro_linalg::semantic_ad_rules().unwrap();
39/// assert!(rules
40///     .lookup_linearize(tenferro_linalg::LINALG_EXTENSION_FAMILY_ID)
41///     .is_some());
42/// assert!(rules
43///     .lookup_linear_transpose(tenferro_linalg::LINALG_EXTENSION_FAMILY_ID)
44///     .is_some());
45/// assert!(rules
46///     .lookup_primal_vjp(tenferro_linalg::LINALG_EXTENSION_FAMILY_ID)
47///     .is_none());
48/// ```
49pub fn semantic_ad_rules() -> Result<SemanticExtensionRuleSet, SemanticExtensionRegistryError> {
50    SemanticExtensionRuleSet::new()
51        .with_linearize(Arc::new(LinalgAdRule))?
52        .with_linear_transpose(Arc::new(LinalgAdRule))
53}
54
55impl SemanticLinearizeRule for LinalgAdRule {
56    fn family_id(&self) -> &'static str {
57        LINALG_EXTENSION_FAMILY_ID
58    }
59
60    fn linearize(
61        &self,
62        request: SemanticLinearizeRequest<'_>,
63        builder: &mut SemanticProgramBuilder,
64    ) -> Result<SemanticLinearizeResult, SemanticAdError> {
65        let op = semantic_linalg_op(request.op(), SemanticAdRuleRole::Linearize)?;
66        if matches!(op.op(), LinalgOp::LuFactor | LinalgOp::SvdFull) {
67            // These value-only operations can appear inside a differentiable
68            // composite (for example, `solve` uses `LuFactor` outputs as
69            // prepared-solve residuals).  Returning absent tangents lets the
70            // composite rule differentiate through the primal inputs without
71            // pretending that the factorization outputs are differentiable.
72            // A caller requesting those outputs directly still observes that
73            // no derivative output was produced, while VJP rejects their
74            // unsupported transpose below.
75            return Ok(SemanticLinearizeResult::new(
76                std::iter::repeat_n(AdValue::Absent, request.primal_outputs().len()),
77                [],
78            ));
79        }
80        let legacy = LegacyInvocation::new(
81            request.primal_inputs(),
82            request.primal_outputs(),
83            request.active_outputs(),
84            builder,
85        )?;
86        let seed_values: Vec<_> = request
87            .tangent_inputs()
88            .iter()
89            .copied()
90            .map(AdValue::value)
91            .collect();
92        let tangent_inputs: Vec<_> = seed_values
93            .iter()
94            .enumerate()
95            .map(|(index, value)| value.map(|_| index))
96            .collect();
97        let mut emitted = SemanticRuleBuilder::with_seeds(
98            &seed_values,
99            &legacy.external_values,
100            &legacy.shape_sources,
101            builder,
102            SemanticAdRuleRole::Linearize,
103        );
104        let tangent_outputs = LinalgAdRule
105            .linearize(
106                op,
107                &mut emitted,
108                &legacy.input_keys,
109                &legacy.output_keys,
110                &tangent_inputs,
111                &mut legacy.context.clone(),
112            )
113            .map_err(|error| legacy_error(SemanticAdRuleRole::Linearize, error))?;
114        let locals = emitted.finish()?;
115        Ok(SemanticLinearizeResult::new(
116            tangent_outputs.into_iter().map(|value| {
117                value
118                    .and_then(|local| locals.get(local).copied().flatten())
119                    .map_or(AdValue::Absent, AdValue::Value)
120            }),
121            [],
122        ))
123    }
124}
125
126impl SemanticLinearTransposeRule for LinalgAdRule {
127    fn family_id(&self) -> &'static str {
128        LINALG_EXTENSION_FAMILY_ID
129    }
130
131    fn linear_transpose(
132        &self,
133        request: SemanticLinearTransposeRequest<'_>,
134        builder: &mut SemanticProgramBuilder,
135    ) -> Result<Box<[AdValue]>, SemanticAdError> {
136        let op = semantic_linalg_op(request.op(), SemanticAdRuleRole::LinearTranspose)?;
137        match op.op() {
138            LinalgOp::TriangularSolve {
139                left_side,
140                lower,
141                transpose_a,
142                unit_diagonal,
143            } => semantic_triangular_solve_transpose(
144                request.primal_inputs(),
145                request.primal_outputs(),
146                request.cotangent_outputs(),
147                request.active_inputs(),
148                builder,
149                left_side,
150                lower,
151                transpose_a,
152                unit_diagonal,
153            ),
154            LinalgOp::LuSolvePrepared { .. } => {
155                let active_inputs = lu_solve_prepared_transpose_active_inputs(
156                    request.active_inputs(),
157                    SemanticAdRuleRole::LinearTranspose,
158                )?;
159                semantic_custom_transpose(
160                    request.op(),
161                    request.primal_inputs(),
162                    request.primal_outputs(),
163                    request.cotangent_outputs(),
164                    &active_inputs,
165                    builder,
166                    SemanticAdRuleRole::LinearTranspose,
167                )
168            }
169            LinalgOp::FullPivLuSolve { .. } => semantic_custom_transpose(
170                request.op(),
171                request.primal_inputs(),
172                request.primal_outputs(),
173                request.cotangent_outputs(),
174                request.active_inputs(),
175                builder,
176                SemanticAdRuleRole::LinearTranspose,
177            ),
178            LinalgOp::LuFactor | LinalgOp::SvdFull => Err(SemanticAdError::Unsupported {
179                family_id: LINALG_EXTENSION_FAMILY_ID,
180                role: SemanticAdRuleRole::LinearTranspose,
181                message: format!("semantic linear transpose is unsupported for {:?}", op.op()),
182            }),
183            _ => semantic_linearized_transpose(
184                request.op(),
185                request.primal_inputs(),
186                request.primal_outputs(),
187                request.cotangent_outputs(),
188                request.active_inputs(),
189                builder,
190            ),
191        }
192    }
193}
194
195fn lu_solve_prepared_transpose_active_inputs(
196    active_inputs: &[bool],
197    role: SemanticAdRuleRole,
198) -> Result<[bool; 4], SemanticAdError> {
199    let active_inputs: [bool; 4] = active_inputs.try_into().map_err(|_| {
200        semantic_internal(
201            role,
202            format!(
203                "lu_solve_prepared semantic transpose expected 4 active inputs, got {}",
204                active_inputs.len()
205            ),
206        )
207    })?;
208    // Packed LU may be an active intermediate when `solve` lowers through
209    // factorization. Pivot and parity slots remain non-cotangent-producing
210    // residuals.
211    Ok([active_inputs[0], false, false, active_inputs[3]])
212}
213
214#[allow(clippy::too_many_arguments)]
215fn semantic_triangular_solve_transpose(
216    primal_inputs: &[ProgramValue],
217    primal_outputs: &[ProgramValue],
218    cotangent_outputs: &[AdValue],
219    active_inputs: &[bool],
220    builder: &mut SemanticProgramBuilder,
221    left_side: bool,
222    lower: bool,
223    transpose_a: bool,
224    unit_diagonal: bool,
225) -> Result<Box<[AdValue]>, SemanticAdError> {
226    let role = SemanticAdRuleRole::LinearTranspose;
227    if primal_inputs.len() != 2
228        || primal_outputs.len() != 1
229        || cotangent_outputs.len() != 1
230        || active_inputs.len() != 2
231    {
232        return Err(semantic_internal(
233            role,
234            "triangular_solve semantic transpose received malformed arity",
235        ));
236    }
237    let Some(ct) = cotangent_outputs.first().copied().and_then(AdValue::value) else {
238        return Ok(vec![AdValue::Absent; 2].into_boxed_slice());
239    };
240
241    let mut result = vec![AdValue::Absent; 2];
242    if !active_inputs[0] && !active_inputs[1] {
243        return Ok(result.into_boxed_slice());
244    }
245
246    let matrix_rank = builder.value_metadata(primal_inputs[0])?.shape().len();
247    let rhs_rank = builder.value_metadata(primal_inputs[1])?.shape().len();
248    if matrix_rank < 2 || rhs_rank < 2 {
249        return Err(semantic_internal(
250            role,
251            "triangular_solve semantic transpose expects matrix operands",
252        ));
253    }
254    if matrix_rank != rhs_rank {
255        return Err(semantic_internal(
256            role,
257            "triangular_solve semantic transpose expects equal-rank operands",
258        ));
259    }
260
261    let conjugated_a = conjugate_if_complex(builder, primal_inputs[0])?;
262    let rhs_cotangent = builder.add_extension(
263        Arc::new(LinalgExtensionOp::new(LinalgOp::TriangularSolve {
264            left_side,
265            lower,
266            transpose_a: !transpose_a,
267            unit_diagonal,
268        })),
269        &[conjugated_a, ct],
270    )?[0];
271
272    if active_inputs[1] {
273        result[1] = AdValue::Value(rhs_cotangent);
274    }
275    if active_inputs[0] {
276        let matrix_cotangent = semantic_solve_matrix_cotangent(
277            builder,
278            rhs_cotangent,
279            primal_outputs[0],
280            left_side,
281            transpose_a,
282            matrix_rank,
283        )?;
284        let k = if unit_diagonal {
285            if lower {
286                -1
287            } else {
288                1
289            }
290        } else {
291            0
292        };
293        let projected = if lower {
294            builder.add_op(CoreSemanticOp::Tril { k }, &[matrix_cotangent])?[0]
295        } else {
296            builder.add_op(CoreSemanticOp::Triu { k }, &[matrix_cotangent])?[0]
297        };
298        result[0] = AdValue::Value(projected);
299    }
300
301    Ok(result.into_boxed_slice())
302}
303
304fn semantic_linearized_transpose(
305    op: &dyn tenferro_ad::extension::ExtensionOp,
306    primal_inputs: &[ProgramValue],
307    primal_outputs: &[ProgramValue],
308    cotangent_outputs: &[AdValue],
309    active_inputs: &[bool],
310    builder: &mut SemanticProgramBuilder,
311) -> Result<Box<[AdValue]>, SemanticAdError> {
312    let legacy = LegacyInvocation::new(
313        primal_inputs,
314        primal_outputs,
315        &cotangent_outputs
316            .iter()
317            .map(|value| matches!(value, AdValue::Value(_)))
318            .collect::<Vec<_>>(),
319        builder,
320    )?;
321    let tangent_inputs: Vec<_> = active_inputs
322        .iter()
323        .copied()
324        .enumerate()
325        .map(|(index, active)| active.then_some(index))
326        .collect();
327    let mut fragment = SemanticLinearFragmentBuilder::with_seed_count(primal_inputs.len());
328    let tangent_outputs = LinalgAdRule
329        .linearize(
330            op,
331            &mut fragment,
332            &legacy.input_keys,
333            &legacy.output_keys,
334            &tangent_inputs,
335            &mut legacy.context.clone(),
336        )
337        .map_err(|error| legacy_error(SemanticAdRuleRole::LinearTranspose, error))?;
338    fragment.transpose_linear_fragment(
339        &tangent_outputs,
340        cotangent_outputs,
341        active_inputs,
342        &legacy.external_values,
343        &legacy.shape_sources,
344        builder,
345    )
346}
347
348fn semantic_custom_transpose(
349    op: &dyn tenferro_ad::extension::ExtensionOp,
350    primal_inputs: &[ProgramValue],
351    primal_outputs: &[ProgramValue],
352    cotangent_outputs: &[AdValue],
353    active_inputs: &[bool],
354    builder: &mut SemanticProgramBuilder,
355    role: SemanticAdRuleRole,
356) -> Result<Box<[AdValue]>, SemanticAdError> {
357    let legacy = LegacyInvocation::new(
358        primal_inputs,
359        primal_outputs,
360        &vec![true; primal_outputs.len()],
361        builder,
362    )?;
363    let seed_values: Vec<_> = cotangent_outputs
364        .iter()
365        .copied()
366        .map(AdValue::value)
367        .collect();
368    let cotangents: Vec<_> = seed_values
369        .iter()
370        .enumerate()
371        .map(|(index, value)| value.map(|_| index))
372        .collect();
373    let transpose_inputs: Vec<_> = legacy
374        .input_keys
375        .iter()
376        .cloned()
377        .map(PrimitiveTransposeInput::Residual)
378        .collect();
379    let mut emitted = SemanticRuleBuilder::with_seeds(
380        &seed_values,
381        &legacy.external_values,
382        &legacy.shape_sources,
383        builder,
384        role,
385    );
386    let cotangent_inputs = LinalgAdRule
387        .linear_transpose(
388            op,
389            &mut emitted,
390            &cotangents,
391            &transpose_inputs,
392            active_inputs,
393            &mut legacy.context.clone(),
394        )
395        .map_err(|error| legacy_error(role, error))?;
396    let locals = emitted.finish()?;
397    Ok(cotangent_inputs
398        .into_iter()
399        .map(|value| {
400            value
401                .and_then(|local| locals.get(local).copied().flatten())
402                .map_or(AdValue::Absent, AdValue::Value)
403        })
404        .collect())
405}
406
407struct LegacyInvocation {
408    context: ShapeGuardContext,
409    input_keys: Vec<ValueKey<StdTensorOp>>,
410    output_keys: Vec<ValueKey<StdTensorOp>>,
411    external_values: HashMap<ValueKey<StdTensorOp>, ProgramValue>,
412    shape_sources: Vec<ProgramValue>,
413}
414
415impl LegacyInvocation {
416    fn new(
417        primal_inputs: &[ProgramValue],
418        primal_outputs: &[ProgramValue],
419        active_outputs: &[bool],
420        builder: &SemanticProgramBuilder,
421    ) -> Result<Self, SemanticAdError> {
422        let values: Vec<_> = primal_inputs
423            .iter()
424            .chain(primal_outputs)
425            .copied()
426            .collect();
427        let metadata: Vec<_> = values
428            .iter()
429            .copied()
430            .map(|value| builder.value_metadata(value).cloned())
431            .collect::<Result<_, _>>()?;
432        let symbolic_inputs = synthetic_input_shapes(&metadata);
433        let symbolic_input_refs: Vec<_> = symbolic_inputs.iter().map(Vec::as_slice).collect();
434        let mut context = ShapeGuardContext::default();
435        let mut external_values = HashMap::new();
436        let keys: Vec<_> = values
437            .iter()
438            .copied()
439            .enumerate()
440            .map(|(index, value)| {
441                let key = ValueKey::Input(TensorInputKey::User {
442                    id: u64::try_from(index + 1).expect("small semantic AD invocation"),
443                });
444                context.insert_metadata(
445                    key.clone(),
446                    legacy_metadata(&metadata[index], &symbolic_input_refs),
447                );
448                external_values.insert(key.clone(), value);
449                key
450            })
451            .collect();
452        let input_count = primal_inputs.len();
453        let input_keys = keys[..input_count].to_vec();
454        let output_keys = keys[input_count..].to_vec();
455        let active_values: HashSet<_> = output_keys
456            .iter()
457            .zip(active_outputs)
458            .filter(|(_, active)| **active)
459            .map(|(key, _)| key.clone())
460            .collect();
461        context = context.with_linearize_active_values(Arc::new(active_values));
462        Ok(Self {
463            context,
464            input_keys,
465            output_keys,
466            external_values,
467            shape_sources: values,
468        })
469    }
470}
471
472fn legacy_metadata(metadata: &ProgramValueMetadata, input_shapes: &[&[SymDim]]) -> TensorMeta {
473    let extents = metadata
474        .shape()
475        .iter()
476        .cloned()
477        .map(|extent| extent.map(|dim| SymDim::from_dim_expr(&dim, input_shapes)))
478        .collect();
479    TensorMeta::with_extents(metadata.dtype(), extents)
480}
481
482fn synthetic_input_shapes(metadata: &[ProgramValueMetadata]) -> Vec<Vec<SymDim>> {
483    let mut ranks = Vec::<usize>::new();
484    for expression in metadata
485        .iter()
486        .flat_map(ProgramValueMetadata::shape)
487        .filter_map(ShapeExtent::bound_expr)
488    {
489        collect_input_ranks(expression, &mut ranks);
490    }
491    ranks
492        .into_iter()
493        .enumerate()
494        .map(|(input, rank)| {
495            (0..rank)
496                .map(|axis| {
497                    SymDim::tensor_axis(
498                        u64::try_from(input + 1).expect("small semantic input index"),
499                        axis,
500                    )
501                })
502                .collect()
503        })
504        .collect()
505}
506
507fn collect_input_ranks(expression: &DimExpr, ranks: &mut Vec<usize>) {
508    match expression {
509        DimExpr::Const(_) => {}
510        DimExpr::InputDim { input_idx, axis } => {
511            if ranks.len() <= *input_idx {
512                ranks.resize(*input_idx + 1, 0);
513            }
514            ranks[*input_idx] = ranks[*input_idx].max(*axis + 1);
515        }
516        DimExpr::Add(lhs, rhs)
517        | DimExpr::Sub(lhs, rhs)
518        | DimExpr::Mul(lhs, rhs)
519        | DimExpr::FloorDiv(lhs, rhs)
520        | DimExpr::Min(lhs, rhs)
521        | DimExpr::Max(lhs, rhs) => {
522            collect_input_ranks(lhs, ranks);
523            collect_input_ranks(rhs, ranks);
524        }
525    }
526}
527
528#[derive(Clone, Debug)]
529enum SemanticLinearFragmentOp {
530    Core(CoreSemanticOp),
531    Extension(Arc<dyn tenferro_ad::extension::ExtensionOp>),
532    Unsupported(String),
533}
534
535#[derive(Clone)]
536enum SemanticLinearFragmentInput {
537    External(ValueKey<StdTensorOp>),
538    Local(LocalValueId),
539}
540
541impl From<ValueRef<StdTensorOp>> for SemanticLinearFragmentInput {
542    fn from(value: ValueRef<StdTensorOp>) -> Self {
543        match value {
544            ValueRef::External(key) => Self::External(key),
545            ValueRef::Local(local) => Self::Local(local),
546        }
547    }
548}
549
550struct SemanticLinearFragmentOperation {
551    operation: SemanticLinearFragmentOp,
552    inputs: Vec<SemanticLinearFragmentInput>,
553    role: OperationRole,
554    outputs: Vec<LocalValueId>,
555}
556
557fn semantic_linear_fragment_op(operation: &StdTensorOp) -> SemanticLinearFragmentOp {
558    match operation {
559        StdTensorOp::Extension(extension) => {
560            SemanticLinearFragmentOp::Extension(Arc::clone(extension))
561        }
562        core => CoreSemanticOp::try_from(core).map_or_else(
563            |_| SemanticLinearFragmentOp::Unsupported(format!("{core:?}")),
564            SemanticLinearFragmentOp::Core,
565        ),
566    }
567}
568
569struct SemanticRuleBuilder<'a, 'builder> {
570    next_local: usize,
571    locals: Vec<Option<ProgramValue>>,
572    external_values: &'a HashMap<ValueKey<StdTensorOp>, ProgramValue>,
573    shape_sources: &'a [ProgramValue],
574    builder: &'builder mut SemanticProgramBuilder,
575    role: SemanticAdRuleRole,
576    error: Option<SemanticAdError>,
577}
578
579impl<'a, 'builder> SemanticRuleBuilder<'a, 'builder> {
580    fn with_seeds(
581        seeds: &[Option<ProgramValue>],
582        external_values: &'a HashMap<ValueKey<StdTensorOp>, ProgramValue>,
583        shape_sources: &'a [ProgramValue],
584        builder: &'builder mut SemanticProgramBuilder,
585        role: SemanticAdRuleRole,
586    ) -> Self {
587        Self {
588            next_local: seeds.len(),
589            locals: seeds.to_vec(),
590            external_values,
591            shape_sources,
592            builder,
593            role,
594            error: None,
595        }
596    }
597
598    fn finish(self) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
599        if let Some(error) = self.error {
600            Err(error)
601        } else {
602            Ok(self.locals)
603        }
604    }
605}
606
607impl PrimitiveRuleBuilder for SemanticRuleBuilder<'_, '_> {
608    fn add_operation(
609        &mut self,
610        operation: StdTensorOp,
611        inputs: Vec<ValueRef<StdTensorOp>>,
612        _role: OperationRole,
613    ) -> Vec<LocalValueId> {
614        let output_count = GraphOperation::output_count(&operation);
615        let outputs: Vec<_> = (self.next_local..self.next_local + output_count).collect();
616        self.next_local += output_count;
617        self.locals.resize(self.next_local, None);
618        if self.error.is_none() {
619            let fragment_op = semantic_linear_fragment_op(&operation);
620            let fragment_inputs: Vec<_> = inputs.into_iter().map(Into::into).collect();
621            let emitted = resolve_semantic_linear_fragment_inputs(
622                &fragment_inputs,
623                self.external_values,
624                &self.locals,
625                self.role,
626            )
627            .and_then(|resolved| {
628                emit_semantic_linear_fragment_operation(
629                    &fragment_op,
630                    &resolved,
631                    self.shape_sources,
632                    self.builder,
633                    self.role,
634                )
635            });
636            match emitted {
637                Ok(values) => {
638                    if values.len() != outputs.len() {
639                        self.error = Some(semantic_internal(
640                            self.role,
641                            format!(
642                                "semantic linalg AD operation emitted {} outputs for {} slots",
643                                values.len(),
644                                outputs.len()
645                            ),
646                        ));
647                    } else {
648                        for (local, value) in outputs.iter().copied().zip(values.iter().copied()) {
649                            self.locals[local] = Some(value);
650                        }
651                    }
652                }
653                Err(error) => {
654                    self.error = Some(error);
655                }
656            }
657        }
658        outputs
659    }
660}
661
662/// Op-local semantic linear fragment used by the manifest's
663/// `LinearizeThenTranspose` route. General linalg decomposition VJPs remain
664/// derived by transposing their emitted linearization, but the fragment stores
665/// semantic core ops or extension payloads instead of replaying legacy
666/// `StdTensorOp` graphs into the destination builder.
667struct SemanticLinearFragmentBuilder {
668    seed_count: usize,
669    next_local: usize,
670    operations: Vec<SemanticLinearFragmentOperation>,
671}
672
673impl SemanticLinearFragmentBuilder {
674    fn with_seed_count(seed_count: usize) -> Self {
675        Self {
676            seed_count,
677            next_local: seed_count,
678            operations: Vec::new(),
679        }
680    }
681
682    fn transpose_linear_fragment(
683        &self,
684        tangent_outputs: &[Option<LocalValueId>],
685        cotangent_outputs: &[AdValue],
686        active_inputs: &[bool],
687        external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
688        shape_sources: &[ProgramValue],
689        builder: &mut SemanticProgramBuilder,
690    ) -> Result<Box<[AdValue]>, SemanticAdError> {
691        let role = SemanticAdRuleRole::LinearTranspose;
692        let fixed_locals =
693            self.emit_fixed_primal_ops(external_values, shape_sources, builder, role)?;
694        let mut cotangents = HashMap::<LocalValueId, ProgramValue>::new();
695        for (tangent, cotangent) in tangent_outputs
696            .iter()
697            .copied()
698            .zip(cotangent_outputs.iter().copied())
699        {
700            if let (Some(tangent), AdValue::Value(cotangent)) = (tangent, cotangent) {
701                accumulate_local_cotangent(builder, &mut cotangents, tangent, cotangent)?;
702            }
703        }
704        for operation in self.operations.iter().rev() {
705            let Some(active_mask) = linear_active_mask(&operation.role) else {
706                continue;
707            };
708            if !active_mask.iter().any(|active| *active) {
709                continue;
710            }
711            let output_cotangents: Vec<_> = operation
712                .outputs
713                .iter()
714                .map(|output| cotangents.remove(output))
715                .collect();
716            if output_cotangents.iter().all(Option::is_none) {
717                continue;
718            }
719            let context = SemanticLinearFragmentTransposeContext {
720                fragment: self,
721                external_values,
722                fixed_locals: &fixed_locals,
723                shape_sources,
724                role,
725            };
726            let input_cotangents = transpose_semantic_linear_fragment_operation(
727                operation,
728                &output_cotangents,
729                active_mask,
730                &context,
731                builder,
732            )?;
733            for ((input, active), cotangent) in operation
734                .inputs
735                .iter()
736                .zip(active_mask)
737                .zip(input_cotangents)
738            {
739                if !active {
740                    continue;
741                }
742                let (SemanticLinearFragmentInput::Local(input), Some(cotangent)) =
743                    (input, cotangent)
744                else {
745                    return Err(semantic_internal(
746                        role,
747                        "linear linalg fragment has a non-local active input",
748                    ));
749                };
750                accumulate_local_cotangent(builder, &mut cotangents, *input, cotangent)?;
751            }
752        }
753        Ok(active_inputs
754            .iter()
755            .copied()
756            .enumerate()
757            .map(|(input, active)| {
758                if active {
759                    cotangents
760                        .remove(&input)
761                        .map_or(AdValue::Absent, AdValue::Value)
762                } else {
763                    AdValue::Absent
764                }
765            })
766            .collect())
767    }
768
769    fn emit_fixed_primal_ops(
770        &self,
771        external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
772        shape_sources: &[ProgramValue],
773        builder: &mut SemanticProgramBuilder,
774        role: SemanticAdRuleRole,
775    ) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
776        let mut locals = vec![None; self.next_local];
777        for operation in &self.operations {
778            if linear_active_mask(&operation.role)
779                .is_some_and(|mask| mask.iter().any(|active| *active))
780            {
781                continue;
782            }
783            let inputs = resolve_semantic_linear_fragment_inputs(
784                &operation.inputs,
785                external_values,
786                &locals,
787                role,
788            )?;
789            let outputs = emit_semantic_linear_fragment_operation(
790                &operation.operation,
791                &inputs,
792                shape_sources,
793                builder,
794                role,
795            )?;
796            for (local, value) in operation
797                .outputs
798                .iter()
799                .copied()
800                .zip(outputs.iter().copied())
801            {
802                locals[local] = Some(value);
803            }
804        }
805        Ok(locals)
806    }
807}
808
809impl PrimitiveRuleBuilder for SemanticLinearFragmentBuilder {
810    fn add_operation(
811        &mut self,
812        operation: StdTensorOp,
813        inputs: Vec<ValueRef<StdTensorOp>>,
814        role: OperationRole,
815    ) -> Vec<LocalValueId> {
816        let output_count = GraphOperation::output_count(&operation);
817        let outputs: Vec<_> = (self.next_local..self.next_local + output_count).collect();
818        self.next_local += output_count;
819        self.operations.push(SemanticLinearFragmentOperation {
820            operation: semantic_linear_fragment_op(&operation),
821            inputs: inputs.into_iter().map(Into::into).collect(),
822            role,
823            outputs: outputs.clone(),
824        });
825        outputs
826    }
827}
828
829fn linear_active_mask(role: &OperationRole) -> Option<&[bool]> {
830    match role {
831        OperationRole::Primary => None,
832        OperationRole::Linearized { active_mask } => Some(active_mask),
833    }
834}
835
836struct SemanticLinearFragmentTransposeContext<'a> {
837    fragment: &'a SemanticLinearFragmentBuilder,
838    external_values: &'a HashMap<ValueKey<StdTensorOp>, ProgramValue>,
839    fixed_locals: &'a [Option<ProgramValue>],
840    shape_sources: &'a [ProgramValue],
841    role: SemanticAdRuleRole,
842}
843
844fn resolve_semantic_linear_fragment_inputs(
845    inputs: &[SemanticLinearFragmentInput],
846    external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
847    locals: &[Option<ProgramValue>],
848    role: SemanticAdRuleRole,
849) -> Result<Vec<ProgramValue>, SemanticAdError> {
850    inputs
851        .iter()
852        .map(|input| match input {
853            SemanticLinearFragmentInput::External(key) => external_values.get(key).copied(),
854            SemanticLinearFragmentInput::Local(local) => locals.get(*local).copied().flatten(),
855        })
856        .collect::<Option<_>>()
857        .ok_or_else(|| {
858            semantic_internal(
859                role,
860                "semantic linalg linear fragment references an unavailable fixed value",
861            )
862        })
863}
864
865fn emit_semantic_linear_fragment_operation(
866    operation: &SemanticLinearFragmentOp,
867    inputs: &[ProgramValue],
868    shape_sources: &[ProgramValue],
869    builder: &mut SemanticProgramBuilder,
870    role: SemanticAdRuleRole,
871) -> Result<Box<[ProgramValue]>, SemanticAdError> {
872    match operation {
873        SemanticLinearFragmentOp::Extension(extension) => {
874            Ok(builder.add_extension(Arc::clone(extension), inputs)?)
875        }
876        SemanticLinearFragmentOp::Core(core) => {
877            let fragment_core = core.clone();
878            let (core, inputs) =
879                localize_shape_expressions(core.clone(), inputs, shape_sources, builder, role)
880                    .map_err(|error| match error {
881                        SemanticAdError::Invariant {
882                            family_id,
883                            role,
884                            message,
885                        } => SemanticAdError::Invariant {
886                            family_id,
887                            role,
888                            message: format!(
889                                "{message}; linear fragment operation {fragment_core:?}"
890                            ),
891                        },
892                        other => other,
893                    })?;
894            Ok(builder.add_op(core, &inputs)?)
895        }
896        SemanticLinearFragmentOp::Unsupported(operation) => Err(semantic_internal(
897            role,
898            format!("linalg AD emitted a non-semantic standard operation {operation}"),
899        )),
900    }
901}
902
903fn localize_shape_expressions(
904    operation: CoreSemanticOp,
905    data_inputs: &[ProgramValue],
906    shape_sources: &[ProgramValue],
907    builder: &SemanticProgramBuilder,
908    role: SemanticAdRuleRole,
909) -> Result<(CoreSemanticOp, Vec<ProgramValue>), SemanticAdError> {
910    let mut inputs = data_inputs.to_vec();
911    let operation = match operation {
912        CoreSemanticOp::Reshape { to_shape } => CoreSemanticOp::Reshape {
913            to_shape: localize_dims(
914                &to_shape,
915                data_inputs,
916                shape_sources,
917                1,
918                &mut inputs,
919                builder,
920                role,
921            )?,
922        },
923        CoreSemanticOp::BroadcastInDim { shape, dims } => CoreSemanticOp::BroadcastInDim {
924            shape: localize_dims(
925                &shape,
926                data_inputs,
927                shape_sources,
928                1,
929                &mut inputs,
930                builder,
931                role,
932            )?,
933            dims,
934        },
935        CoreSemanticOp::GatherDynamicSliceSizes {
936            offset_dims,
937            collapsed_slice_dims,
938            start_index_map,
939            index_vector_dim,
940            slice_sizes,
941        } => CoreSemanticOp::GatherDynamicSliceSizes {
942            offset_dims,
943            collapsed_slice_dims,
944            start_index_map,
945            index_vector_dim,
946            slice_sizes: localize_dims(
947                &slice_sizes,
948                data_inputs,
949                shape_sources,
950                2,
951                &mut inputs,
952                builder,
953                role,
954            )?,
955        },
956        other => other,
957    };
958    Ok((operation, inputs))
959}
960
961fn localize_dims(
962    dims: &[DimExpr],
963    data_inputs: &[ProgramValue],
964    shape_sources: &[ProgramValue],
965    fixed_data_arity: usize,
966    operation_inputs: &mut Vec<ProgramValue>,
967    builder: &SemanticProgramBuilder,
968    role: SemanticAdRuleRole,
969) -> Result<Vec<DimExpr>, SemanticAdError> {
970    dims.iter()
971        .map(|dim| {
972            localize_dim(
973                dim,
974                data_inputs,
975                shape_sources,
976                fixed_data_arity,
977                operation_inputs,
978                builder,
979                role,
980            )
981        })
982        .collect()
983}
984
985fn localize_dim(
986    dim: &DimExpr,
987    data_inputs: &[ProgramValue],
988    shape_sources: &[ProgramValue],
989    fixed_data_arity: usize,
990    operation_inputs: &mut Vec<ProgramValue>,
991    builder: &SemanticProgramBuilder,
992    role: SemanticAdRuleRole,
993) -> Result<DimExpr, SemanticAdError> {
994    let binary = |lhs: &DimExpr,
995                  rhs: &DimExpr,
996                  constructor: fn(Box<DimExpr>, Box<DimExpr>) -> DimExpr,
997                  operation_inputs: &mut Vec<ProgramValue>|
998     -> Result<DimExpr, SemanticAdError> {
999        Ok(constructor(
1000            Box::new(localize_dim(
1001                lhs,
1002                data_inputs,
1003                shape_sources,
1004                fixed_data_arity,
1005                operation_inputs,
1006                builder,
1007                role,
1008            )?),
1009            Box::new(localize_dim(
1010                rhs,
1011                data_inputs,
1012                shape_sources,
1013                fixed_data_arity,
1014                operation_inputs,
1015                builder,
1016                role,
1017            )?),
1018        ))
1019    };
1020    match dim {
1021        DimExpr::Const(value) => Ok(DimExpr::Const(*value)),
1022        DimExpr::InputDim { input_idx, axis } => {
1023            // Legacy linalg rules express shape dimensions in invocation
1024            // coordinates. Shape-aware primitive helpers append explicit
1025            // shape operands after the primitive's fixed data operands and
1026            // remap their dimensions into those local operand coordinates.
1027            // Keep those two coordinate spaces distinct; rank compatibility
1028            // cannot disambiguate them.
1029            let source = if *input_idx >= fixed_data_arity {
1030                data_inputs
1031                    .get(*input_idx)
1032                    .copied()
1033                    .or_else(|| shape_sources.get(*input_idx).copied())
1034            } else {
1035                shape_sources.get(*input_idx).copied()
1036            }
1037            .ok_or_else(|| {
1038                    semantic_internal(
1039                        role,
1040                        format!(
1041                            "linalg AD symbolic shape input {input_idx} is out of bounds for {} operation inputs and {} primal shape sources",
1042                            data_inputs.len(),
1043                            shape_sources.len()
1044                        ),
1045                    )
1046                })?;
1047            let rank = builder.value_metadata(source)?.shape().len();
1048            if *axis >= rank {
1049                return Err(semantic_internal(
1050                    role,
1051                    format!(
1052                        "linalg AD symbolic shape axis {axis} is out of bounds for source rank {rank}"
1053                    ),
1054                ));
1055            }
1056            let input_idx = operation_inputs
1057                .iter()
1058                .position(|value| *value == source)
1059                .unwrap_or_else(|| {
1060                    operation_inputs.push(source);
1061                    operation_inputs.len() - 1
1062                });
1063            debug_assert!(
1064                input_idx < data_inputs.len() + shape_sources.len(),
1065                "localized shape source must be an operation input"
1066            );
1067            Ok(DimExpr::InputDim {
1068                input_idx,
1069                axis: *axis,
1070            })
1071        }
1072        DimExpr::Add(lhs, rhs) => binary(lhs, rhs, DimExpr::Add, operation_inputs),
1073        DimExpr::Sub(lhs, rhs) => binary(lhs, rhs, DimExpr::Sub, operation_inputs),
1074        DimExpr::Mul(lhs, rhs) => binary(lhs, rhs, DimExpr::Mul, operation_inputs),
1075        DimExpr::FloorDiv(lhs, rhs) => binary(lhs, rhs, DimExpr::FloorDiv, operation_inputs),
1076        DimExpr::Min(lhs, rhs) => binary(lhs, rhs, DimExpr::Min, operation_inputs),
1077        DimExpr::Max(lhs, rhs) => binary(lhs, rhs, DimExpr::Max, operation_inputs),
1078    }
1079}
1080
1081fn transpose_semantic_linear_fragment_operation(
1082    operation: &SemanticLinearFragmentOperation,
1083    cotangent_outputs: &[Option<ProgramValue>],
1084    active_mask: &[bool],
1085    context: &SemanticLinearFragmentTransposeContext<'_>,
1086    builder: &mut SemanticProgramBuilder,
1087) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1088    let Some(cotangent) = cotangent_outputs.first().copied().flatten() else {
1089        return Ok(vec![None; operation.inputs.len()]);
1090    };
1091    let fixed = |index: usize| {
1092        if active_mask.get(index).copied().unwrap_or(false) {
1093            None
1094        } else {
1095            match operation.inputs.get(index) {
1096                Some(SemanticLinearFragmentInput::External(key)) => {
1097                    context.external_values.get(key).copied()
1098                }
1099                Some(SemanticLinearFragmentInput::Local(local)) => {
1100                    context.fixed_locals.get(*local).copied().flatten()
1101                }
1102                None => None,
1103            }
1104        }
1105    };
1106    let unary = |value| Ok(vec![Some(value)]);
1107    match &operation.operation {
1108        SemanticLinearFragmentOp::Core(CoreSemanticOp::Add) => Ok(active_mask
1109            .iter()
1110            .map(|active| active.then_some(cotangent))
1111            .collect()),
1112        SemanticLinearFragmentOp::Core(CoreSemanticOp::Sub) => {
1113            let rhs = builder.add_op(CoreSemanticOp::Neg, &[cotangent])?[0];
1114            Ok(vec![
1115                active_mask[0].then_some(cotangent),
1116                active_mask[1].then_some(rhs),
1117            ])
1118        }
1119        SemanticLinearFragmentOp::Core(CoreSemanticOp::Neg) => {
1120            let value = builder.add_op(CoreSemanticOp::Neg, &[cotangent])?[0];
1121            unary(value)
1122        }
1123        SemanticLinearFragmentOp::Core(CoreSemanticOp::Conj) => {
1124            let value = builder.add_op(CoreSemanticOp::Conj, &[cotangent])?[0];
1125            unary(value)
1126        }
1127        SemanticLinearFragmentOp::Core(CoreSemanticOp::Mul) => {
1128            transpose_mul(cotangent, active_mask, &fixed, builder, context.role)
1129        }
1130        SemanticLinearFragmentOp::Core(CoreSemanticOp::Div) => {
1131            transpose_div(cotangent, active_mask, &fixed, builder, context.role)
1132        }
1133        SemanticLinearFragmentOp::Core(CoreSemanticOp::DotGeneral { config }) => {
1134            transpose_matrix_dot(
1135                cotangent,
1136                config,
1137                active_mask,
1138                &fixed,
1139                builder,
1140                context.role,
1141            )
1142        }
1143        SemanticLinearFragmentOp::Core(CoreSemanticOp::ReduceSum { axes }) => {
1144            transpose_reduce_sum(context, operation, cotangent, axes, active_mask, builder)
1145        }
1146        SemanticLinearFragmentOp::Core(CoreSemanticOp::Transpose { perm }) => {
1147            let mut inverse = vec![0; perm.len()];
1148            for (output_axis, input_axis) in perm.iter().copied().enumerate() {
1149                inverse[input_axis] = output_axis;
1150            }
1151            let value =
1152                builder.add_op(CoreSemanticOp::Transpose { perm: inverse }, &[cotangent])?[0];
1153            unary(value)
1154        }
1155        SemanticLinearFragmentOp::Core(CoreSemanticOp::Convert { from, to }) => {
1156            let value = builder.add_op(
1157                CoreSemanticOp::Convert {
1158                    from: *to,
1159                    to: *from,
1160                },
1161                &[cotangent],
1162            )?[0];
1163            unary(value)
1164        }
1165        SemanticLinearFragmentOp::Core(CoreSemanticOp::ExtractDiag { axis_a, axis_b }) => {
1166            let value = builder.add_op(
1167                CoreSemanticOp::EmbedDiag {
1168                    axis_a: *axis_a,
1169                    axis_b: *axis_b,
1170                },
1171                &[cotangent],
1172            )?[0];
1173            unary(value)
1174        }
1175        SemanticLinearFragmentOp::Core(CoreSemanticOp::EmbedDiag { axis_a, axis_b }) => {
1176            let value = builder.add_op(
1177                CoreSemanticOp::ExtractDiag {
1178                    axis_a: *axis_a,
1179                    axis_b: *axis_b,
1180                },
1181                &[cotangent],
1182            )?[0];
1183            unary(value)
1184        }
1185        SemanticLinearFragmentOp::Core(CoreSemanticOp::Tril { k }) => {
1186            let value = builder.add_op(CoreSemanticOp::Tril { k: *k }, &[cotangent])?[0];
1187            unary(value)
1188        }
1189        SemanticLinearFragmentOp::Core(CoreSemanticOp::Triu { k }) => {
1190            let value = builder.add_op(CoreSemanticOp::Triu { k: *k }, &[cotangent])?[0];
1191            unary(value)
1192        }
1193        SemanticLinearFragmentOp::Extension(extension) => transpose_linalg_extension(
1194            extension.as_ref(),
1195            operation,
1196            cotangent,
1197            active_mask,
1198            context.external_values,
1199            context.fixed_locals,
1200            builder,
1201        ),
1202        SemanticLinearFragmentOp::Unsupported(operation) => Err(semantic_internal(
1203            context.role,
1204            format!("unsupported linear linalg fragment operation {operation}"),
1205        )),
1206        other => Err(semantic_internal(
1207            context.role,
1208            format!("unsupported linear linalg fragment operation {other:?}"),
1209        )),
1210    }
1211}
1212
1213fn transpose_reduce_sum(
1214    context: &SemanticLinearFragmentTransposeContext<'_>,
1215    operation: &SemanticLinearFragmentOperation,
1216    cotangent: ProgramValue,
1217    axes: &[usize],
1218    active_mask: &[bool],
1219    builder: &mut SemanticProgramBuilder,
1220) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1221    if operation.inputs.len() != 1 || active_mask.len() != 1 {
1222        return Err(semantic_internal(
1223            context.role,
1224            "linear reduce_sum fragment has malformed arity",
1225        ));
1226    }
1227    if !active_mask[0] {
1228        return Ok(vec![None]);
1229    }
1230    let mut cache = HashMap::new();
1231    let input_shape = semantic_linear_fragment_value_shape(
1232        context.fragment,
1233        &operation.inputs[0],
1234        context.external_values,
1235        context.shape_sources,
1236        builder,
1237        context.role,
1238        &mut cache,
1239    )?
1240    .ok_or_else(|| {
1241        semantic_internal(
1242            context.role,
1243            "linear reduce_sum fragment is missing input shape metadata",
1244        )
1245    })?;
1246    if axes.iter().any(|axis| *axis >= input_shape.len()) {
1247        return Err(semantic_internal(
1248            context.role,
1249            format!(
1250                "linear reduce_sum axis is out of bounds for input rank {}",
1251                input_shape.len()
1252            ),
1253        ));
1254    }
1255    let dims: Vec<_> = (0..input_shape.len())
1256        .filter(|axis| !axes.contains(axis))
1257        .collect();
1258    let mut inputs = vec![cotangent];
1259    let shape = localize_dims(
1260        &input_shape,
1261        &[cotangent],
1262        context.shape_sources,
1263        1,
1264        &mut inputs,
1265        builder,
1266        context.role,
1267    )?;
1268    Ok(vec![Some(
1269        builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &inputs)?[0],
1270    )])
1271}
1272
1273fn semantic_linear_fragment_value_shape(
1274    fragment: &SemanticLinearFragmentBuilder,
1275    value: &SemanticLinearFragmentInput,
1276    external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
1277    shape_sources: &[ProgramValue],
1278    builder: &SemanticProgramBuilder,
1279    role: SemanticAdRuleRole,
1280    cache: &mut HashMap<LocalValueId, Option<Vec<DimExpr>>>,
1281) -> Result<Option<Vec<DimExpr>>, SemanticAdError> {
1282    match value {
1283        SemanticLinearFragmentInput::External(key) => {
1284            let source = external_values.get(key).copied().ok_or_else(|| {
1285                semantic_internal(
1286                    role,
1287                    "semantic linalg linear-fragment shape references missing external value",
1288                )
1289            })?;
1290            source_shape(source, shape_sources, builder, role).map(Some)
1291        }
1292        SemanticLinearFragmentInput::Local(local) => semantic_linear_fragment_local_shape(
1293            fragment,
1294            *local,
1295            external_values,
1296            shape_sources,
1297            builder,
1298            role,
1299            cache,
1300        ),
1301    }
1302}
1303
1304fn semantic_linear_fragment_local_shape(
1305    fragment: &SemanticLinearFragmentBuilder,
1306    local: LocalValueId,
1307    external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
1308    shape_sources: &[ProgramValue],
1309    builder: &SemanticProgramBuilder,
1310    role: SemanticAdRuleRole,
1311    cache: &mut HashMap<LocalValueId, Option<Vec<DimExpr>>>,
1312) -> Result<Option<Vec<DimExpr>>, SemanticAdError> {
1313    if let Some(cached) = cache.get(&local) {
1314        return Ok(cached.clone());
1315    }
1316    let shape = if local < fragment.seed_count {
1317        let source = shape_sources.get(local).copied().ok_or_else(|| {
1318            semantic_internal(
1319                role,
1320                format!("semantic linalg linear-fragment seed local {local} has no shape source"),
1321            )
1322        })?;
1323        Some(source_shape(source, shape_sources, builder, role)?)
1324    } else {
1325        let (operation, output_index) = fragment
1326            .operations
1327            .iter()
1328            .find_map(|operation| {
1329                operation
1330                    .outputs
1331                    .iter()
1332                    .position(|output| *output == local)
1333                    .map(|index| (operation, index))
1334            })
1335            .ok_or_else(|| {
1336                semantic_internal(
1337                    role,
1338                    format!(
1339                        "semantic linalg linear-fragment local {local} has no producing operation"
1340                    ),
1341                )
1342            })?;
1343        semantic_linear_fragment_operation_output_shape(
1344            fragment,
1345            operation,
1346            output_index,
1347            external_values,
1348            shape_sources,
1349            builder,
1350            role,
1351            cache,
1352        )?
1353    };
1354    cache.insert(local, shape.clone());
1355    Ok(shape)
1356}
1357
1358fn source_shape(
1359    source: ProgramValue,
1360    shape_sources: &[ProgramValue],
1361    builder: &SemanticProgramBuilder,
1362    role: SemanticAdRuleRole,
1363) -> Result<Vec<DimExpr>, SemanticAdError> {
1364    let index = shape_sources
1365        .iter()
1366        .position(|candidate| *candidate == source)
1367        .ok_or_else(|| {
1368            semantic_internal(role, "shape source is not part of the linalg invocation")
1369        })?;
1370    let rank = builder.value_metadata(source)?.shape().len();
1371    Ok(DimExpr::input_shape(index, rank))
1372}
1373
1374#[allow(clippy::too_many_arguments)]
1375fn semantic_linear_fragment_operation_output_shape(
1376    fragment: &SemanticLinearFragmentBuilder,
1377    operation: &SemanticLinearFragmentOperation,
1378    output_index: usize,
1379    external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
1380    shape_sources: &[ProgramValue],
1381    builder: &SemanticProgramBuilder,
1382    role: SemanticAdRuleRole,
1383    cache: &mut HashMap<LocalValueId, Option<Vec<DimExpr>>>,
1384) -> Result<Option<Vec<DimExpr>>, SemanticAdError> {
1385    let input_shape = |input_index: usize,
1386                       cache: &mut HashMap<LocalValueId, Option<Vec<DimExpr>>>|
1387     -> Result<Option<Vec<DimExpr>>, SemanticAdError> {
1388        let input = operation.inputs.get(input_index).ok_or_else(|| {
1389            semantic_internal(
1390                role,
1391                "semantic linalg linear-fragment shape requested missing operation input",
1392            )
1393        })?;
1394        semantic_linear_fragment_value_shape(
1395            fragment,
1396            input,
1397            external_values,
1398            shape_sources,
1399            builder,
1400            role,
1401            cache,
1402        )
1403    };
1404    match &operation.operation {
1405        SemanticLinearFragmentOp::Extension(extension) => {
1406            let linalg = semantic_linalg_op(extension.as_ref(), role)?;
1407            match linalg.op() {
1408                LinalgOp::LuFactor => match output_index {
1409                    0 => input_shape(0, cache),
1410                    1 => Ok(input_shape(0, cache)?.map(|shape| {
1411                        let (rows, cols, batch) =
1412                            semantic_linear_fragment_matrix_shape_parts(&shape);
1413                        let mut pivots_shape =
1414                            vec![DimExpr::Min(Box::new(rows.clone()), Box::new(cols.clone()))];
1415                        pivots_shape.extend_from_slice(batch);
1416                        pivots_shape
1417                    })),
1418                    2 => Ok(input_shape(0, cache)?.map(|shape| shape[2..].to_vec())),
1419                    _ => Ok(None),
1420                },
1421                LinalgOp::LuSolvePrepared { .. } => input_shape(3, cache),
1422                _ => Ok(None),
1423            }
1424        }
1425        SemanticLinearFragmentOp::Core(CoreSemanticOp::ExtractDiag { axis_a, axis_b }) => {
1426            Ok(input_shape(0, cache)?
1427                .map(|shape| extract_diag_shape(&shape, *axis_a, *axis_b))
1428                .transpose()?)
1429        }
1430        SemanticLinearFragmentOp::Core(CoreSemanticOp::ReduceSum { axes }) => {
1431            Ok(input_shape(0, cache)?.map(|shape| {
1432                shape
1433                    .into_iter()
1434                    .enumerate()
1435                    .filter_map(|(axis, dim)| (!axes.contains(&axis)).then_some(dim))
1436                    .collect()
1437            }))
1438        }
1439        SemanticLinearFragmentOp::Core(
1440            CoreSemanticOp::Convert { .. }
1441            | CoreSemanticOp::Neg
1442            | CoreSemanticOp::Conj
1443            | CoreSemanticOp::Tril { .. }
1444            | CoreSemanticOp::Triu { .. },
1445        ) => input_shape(0, cache),
1446        SemanticLinearFragmentOp::Core(CoreSemanticOp::Transpose { perm }) => {
1447            Ok(input_shape(0, cache)?
1448                .map(|shape| perm.iter().map(|axis| shape[*axis].clone()).collect()))
1449        }
1450        _ => Ok(None),
1451    }
1452}
1453
1454fn semantic_linear_fragment_matrix_shape_parts(
1455    shape: &[DimExpr],
1456) -> (&DimExpr, &DimExpr, &[DimExpr]) {
1457    (&shape[0], &shape[1], &shape[2..])
1458}
1459
1460fn extract_diag_shape(
1461    shape: &[DimExpr],
1462    axis_a: usize,
1463    axis_b: usize,
1464) -> Result<Vec<DimExpr>, SemanticAdError> {
1465    if axis_a >= shape.len() || axis_b >= shape.len() || axis_a == axis_b {
1466        return Err(semantic_internal(
1467            SemanticAdRuleRole::LinearTranspose,
1468            "extract_diag shape derivation received invalid axes",
1469        ));
1470    }
1471    let diagonal = DimExpr::Min(
1472        Box::new(shape[axis_a].clone()),
1473        Box::new(shape[axis_b].clone()),
1474    );
1475    let mut output = Vec::with_capacity(shape.len() - 1);
1476    for (axis, dim) in shape.iter().enumerate() {
1477        if axis == axis_b {
1478            continue;
1479        }
1480        if axis == axis_a {
1481            output.push(diagonal.clone());
1482        } else {
1483            output.push(dim.clone());
1484        }
1485    }
1486    Ok(output)
1487}
1488
1489fn transpose_mul(
1490    cotangent: ProgramValue,
1491    active_mask: &[bool],
1492    fixed: &impl Fn(usize) -> Option<ProgramValue>,
1493    builder: &mut SemanticProgramBuilder,
1494    role: SemanticAdRuleRole,
1495) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1496    let mut result = vec![None; 2];
1497    for input in 0..2 {
1498        if !active_mask[input] {
1499            continue;
1500        }
1501        let coefficient = fixed(1 - input).ok_or_else(|| {
1502            semantic_internal(role, "linear multiply is missing its fixed coefficient")
1503        })?;
1504        let coefficient = conjugate_if_complex(builder, coefficient)?;
1505        result[input] = Some(builder.add_op(CoreSemanticOp::Mul, &[cotangent, coefficient])?[0]);
1506    }
1507    Ok(result)
1508}
1509
1510fn transpose_div(
1511    cotangent: ProgramValue,
1512    active_mask: &[bool],
1513    fixed: &impl Fn(usize) -> Option<ProgramValue>,
1514    builder: &mut SemanticProgramBuilder,
1515    role: SemanticAdRuleRole,
1516) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1517    let mut result = vec![None; 2];
1518    if active_mask[0] {
1519        let denominator = fixed(1).ok_or_else(|| {
1520            semantic_internal(role, "linear divide is missing its fixed denominator")
1521        })?;
1522        let denominator = conjugate_if_complex(builder, denominator)?;
1523        result[0] = Some(builder.add_op(CoreSemanticOp::Div, &[cotangent, denominator])?[0]);
1524    }
1525    if active_mask[1] {
1526        let numerator = fixed(0).ok_or_else(|| {
1527            semantic_internal(role, "linear divide is missing its fixed numerator")
1528        })?;
1529        let denominator = fixed(1).ok_or_else(|| {
1530            semantic_internal(role, "linear divide is missing its fixed denominator")
1531        })?;
1532        let square = builder.add_op(CoreSemanticOp::Mul, &[denominator, denominator])?[0];
1533        let coefficient = builder.add_op(CoreSemanticOp::Div, &[numerator, square])?[0];
1534        let coefficient = conjugate_if_complex(builder, coefficient)?;
1535        let value = builder.add_op(CoreSemanticOp::Mul, &[cotangent, coefficient])?[0];
1536        result[1] = Some(builder.add_op(CoreSemanticOp::Neg, &[value])?[0]);
1537    }
1538    Ok(result)
1539}
1540
1541fn transpose_matrix_dot(
1542    cotangent: ProgramValue,
1543    config: &tenferro_tensor::DotGeneralConfig,
1544    active_mask: &[bool],
1545    fixed: &impl Fn(usize) -> Option<ProgramValue>,
1546    builder: &mut SemanticProgramBuilder,
1547    role: SemanticAdRuleRole,
1548) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1549    let rank = 2 + config.lhs_batch_dims.len();
1550    let expected_batch: Vec<_> = (2..rank).collect();
1551    if config.lhs_contracting_dims != [1]
1552        || config.rhs_contracting_dims != [0]
1553        || config.lhs_batch_dims != expected_batch
1554        || config.rhs_batch_dims != expected_batch
1555    {
1556        return Err(semantic_internal(
1557            role,
1558            "linalg AD emitted an unsupported dot-general configuration",
1559        ));
1560    }
1561    let mut result = vec![None; 2];
1562    if active_mask[0] {
1563        let rhs = fixed(1).ok_or_else(|| {
1564            semantic_internal(role, "linear matrix product is missing its fixed rhs")
1565        })?;
1566        let rhs_h = matrix_adjoint(builder, rhs, rank)?;
1567        result[0] = Some(
1568            builder.add_op(
1569                CoreSemanticOp::DotGeneral {
1570                    config: config.clone(),
1571                },
1572                &[cotangent, rhs_h],
1573            )?[0],
1574        );
1575    }
1576    if active_mask[1] {
1577        let lhs = fixed(0).ok_or_else(|| {
1578            semantic_internal(role, "linear matrix product is missing its fixed lhs")
1579        })?;
1580        let lhs_h = matrix_adjoint(builder, lhs, rank)?;
1581        result[1] = Some(
1582            builder.add_op(
1583                CoreSemanticOp::DotGeneral {
1584                    config: config.clone(),
1585                },
1586                &[lhs_h, cotangent],
1587            )?[0],
1588        );
1589    }
1590    Ok(result)
1591}
1592
1593fn semantic_solve_matrix_cotangent(
1594    builder: &mut SemanticProgramBuilder,
1595    rhs_cotangent: ProgramValue,
1596    solution: ProgramValue,
1597    left_side: bool,
1598    transpose_a: bool,
1599    rank: usize,
1600) -> Result<ProgramValue, SemanticAdError> {
1601    let negative_rhs_cotangent = builder.add_op(CoreSemanticOp::Neg, &[rhs_cotangent])?[0];
1602    let solution_h = matrix_adjoint(builder, solution, rank)?;
1603    let config = semantic_matrix_multiply_config(rank)?;
1604    let matrix_cotangent = if left_side {
1605        builder.add_op(
1606            CoreSemanticOp::DotGeneral {
1607                config: config.clone(),
1608            },
1609            &[negative_rhs_cotangent, solution_h],
1610        )?[0]
1611    } else {
1612        builder.add_op(
1613            CoreSemanticOp::DotGeneral { config },
1614            &[solution_h, negative_rhs_cotangent],
1615        )?[0]
1616    };
1617    if transpose_a {
1618        semantic_matrix_transpose(builder, matrix_cotangent, rank)
1619    } else {
1620        Ok(matrix_cotangent)
1621    }
1622}
1623
1624fn semantic_matrix_multiply_config(
1625    rank: usize,
1626) -> Result<tenferro_tensor::DotGeneralConfig, SemanticAdError> {
1627    if rank < 2 {
1628        return Err(semantic_internal(
1629            SemanticAdRuleRole::LinearTranspose,
1630            "matrix multiply semantic helper expects rank >= 2",
1631        ));
1632    }
1633    let batch_dims: Vec<usize> = (2..rank).collect();
1634    Ok(tenferro_tensor::DotGeneralConfig {
1635        lhs_contracting_dims: vec![1],
1636        rhs_contracting_dims: vec![0],
1637        lhs_batch_dims: batch_dims.clone(),
1638        rhs_batch_dims: batch_dims,
1639    })
1640}
1641
1642fn semantic_matrix_transpose(
1643    builder: &mut SemanticProgramBuilder,
1644    value: ProgramValue,
1645    rank: usize,
1646) -> Result<ProgramValue, SemanticAdError> {
1647    if rank < 2 {
1648        return Err(semantic_internal(
1649            SemanticAdRuleRole::LinearTranspose,
1650            "matrix transpose semantic helper expects rank >= 2",
1651        ));
1652    }
1653    let mut perm: Vec<_> = (0..rank).collect();
1654    perm.swap(0, 1);
1655    Ok(builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0])
1656}
1657
1658fn matrix_adjoint(
1659    builder: &mut SemanticProgramBuilder,
1660    value: ProgramValue,
1661    rank: usize,
1662) -> Result<ProgramValue, SemanticAdError> {
1663    let value = conjugate_if_complex(builder, value)?;
1664    let mut perm: Vec<_> = (0..rank).collect();
1665    perm.swap(0, 1);
1666    Ok(builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0])
1667}
1668
1669fn conjugate_if_complex(
1670    builder: &mut SemanticProgramBuilder,
1671    value: ProgramValue,
1672) -> Result<ProgramValue, SemanticAdError> {
1673    if matches!(
1674        builder.value_metadata(value)?.dtype(),
1675        tenferro_tensor::DType::C32 | tenferro_tensor::DType::C64
1676    ) {
1677        Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
1678    } else {
1679        Ok(value)
1680    }
1681}
1682
1683fn transpose_linalg_extension(
1684    extension: &dyn tenferro_ad::extension::ExtensionOp,
1685    operation: &SemanticLinearFragmentOperation,
1686    cotangent: ProgramValue,
1687    active_mask: &[bool],
1688    external_values: &HashMap<ValueKey<StdTensorOp>, ProgramValue>,
1689    fixed_locals: &[Option<ProgramValue>],
1690    builder: &mut SemanticProgramBuilder,
1691) -> Result<Vec<Option<ProgramValue>>, SemanticAdError> {
1692    let role = SemanticAdRuleRole::LinearTranspose;
1693    let linalg = semantic_linalg_op(extension, role)?;
1694    if !matches!(
1695        linalg.op(),
1696        LinalgOp::TriangularSolve { .. }
1697            | LinalgOp::LuSolvePrepared { .. }
1698            | LinalgOp::FullPivLuSolve { .. }
1699    ) {
1700        return Err(semantic_internal(
1701            role,
1702            format!(
1703                "linear linalg fragment contains unsupported extension {:?}",
1704                linalg.op()
1705            ),
1706        ));
1707    }
1708    let mut context = ShapeGuardContext::default();
1709    let mut fixed_values = HashMap::new();
1710    let mut keys = Vec::with_capacity(operation.inputs.len());
1711    let mut shape_sources = Vec::with_capacity(operation.inputs.len());
1712    for (index, (input, active)) in operation.inputs.iter().zip(active_mask).enumerate() {
1713        let key = ValueKey::Input(TensorInputKey::User {
1714            id: 10_000 + u64::try_from(index).expect("small linalg extension arity"),
1715        });
1716        let value = if *active {
1717            cotangent
1718        } else {
1719            match input {
1720                SemanticLinearFragmentInput::External(key) => external_values.get(key).copied(),
1721                SemanticLinearFragmentInput::Local(local) => {
1722                    fixed_locals.get(*local).copied().flatten()
1723                }
1724            }
1725            .ok_or_else(|| {
1726                semantic_internal(role, "linear solve fragment is missing a fixed operand")
1727            })?
1728        };
1729        let metadata = builder.value_metadata(value)?.clone();
1730        let symbolic_shapes = synthetic_input_shapes(std::slice::from_ref(&metadata));
1731        let symbolic_shape_refs: Vec<_> = symbolic_shapes.iter().map(Vec::as_slice).collect();
1732        context.insert_metadata(
1733            key.clone(),
1734            legacy_metadata(&metadata, &symbolic_shape_refs),
1735        );
1736        if !active {
1737            fixed_values.insert(key.clone(), value);
1738        }
1739        shape_sources.push(value);
1740        keys.push(key);
1741    }
1742    let inputs: Vec<_> = keys
1743        .iter()
1744        .cloned()
1745        .map(PrimitiveTransposeInput::Residual)
1746        .collect();
1747    let mut emitted = SemanticRuleBuilder::with_seeds(
1748        &[Some(cotangent)],
1749        &fixed_values,
1750        &shape_sources,
1751        builder,
1752        role,
1753    );
1754    let outputs = LinalgAdRule
1755        .linear_transpose(
1756            extension,
1757            &mut emitted,
1758            &[Some(0)],
1759            &inputs,
1760            active_mask,
1761            &mut context,
1762        )
1763        .map_err(|error| legacy_error(role, error))?;
1764    let locals = emitted.finish()?;
1765    Ok(outputs
1766        .into_iter()
1767        .map(|output| output.and_then(|local| locals.get(local).copied().flatten()))
1768        .collect())
1769}
1770
1771fn accumulate_local_cotangent(
1772    builder: &mut SemanticProgramBuilder,
1773    cotangents: &mut HashMap<LocalValueId, ProgramValue>,
1774    local: LocalValueId,
1775    cotangent: ProgramValue,
1776) -> Result<(), SemanticAdError> {
1777    if let Some(existing) = cotangents.get_mut(&local) {
1778        *existing = builder.add_op(CoreSemanticOp::Add, &[*existing, cotangent])?[0];
1779    } else {
1780        cotangents.insert(local, cotangent);
1781    }
1782    Ok(())
1783}
1784
1785fn semantic_linalg_op(
1786    op: &dyn tenferro_ad::extension::ExtensionOp,
1787    role: SemanticAdRuleRole,
1788) -> Result<&LinalgExtensionOp, SemanticAdError> {
1789    op.as_any()
1790        .downcast_ref::<LinalgExtensionOp>()
1791        .ok_or_else(|| SemanticAdError::Unsupported {
1792            family_id: LINALG_EXTENSION_FAMILY_ID,
1793            role,
1794            message: "linalg semantic AD received an incompatible payload".into(),
1795        })
1796}
1797
1798fn legacy_error(role: SemanticAdRuleRole, error: tenferro_ops::ad::ADRuleError) -> SemanticAdError {
1799    SemanticAdError::Rule {
1800        family_id: LINALG_EXTENSION_FAMILY_ID,
1801        role,
1802        source: Box::new(error),
1803    }
1804}
1805
1806fn semantic_internal(role: SemanticAdRuleRole, message: impl Into<String>) -> SemanticAdError {
1807    SemanticAdError::Invariant {
1808        family_id: LINALG_EXTENSION_FAMILY_ID,
1809        role,
1810        message: message.into(),
1811    }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816    use super::*;
1817    use tenferro_runtime::program::ProgramInputSpec;
1818    use tenferro_tensor::DType;
1819
1820    #[test]
1821    fn recorded_broadcast_prefers_primal_shape_source_over_rank_compatible_data_input() {
1822        let mut builder = SemanticProgramBuilder::new();
1823        let _row_anchor = builder
1824            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(3)]))
1825            .unwrap();
1826        let _col_anchor = builder
1827            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1828            .unwrap();
1829        let matrix = builder
1830            .input(ProgramInputSpec::new(
1831                DType::F64,
1832                [
1833                    DimExpr::InputDim {
1834                        input_idx: 0,
1835                        axis: 0,
1836                    },
1837                    DimExpr::InputDim {
1838                        input_idx: 1,
1839                        axis: 0,
1840                    },
1841                ],
1842            ))
1843            .unwrap();
1844        let vector = builder
1845            .input(ProgramInputSpec::new(
1846                DType::F64,
1847                [DimExpr::Min(
1848                    Box::new(DimExpr::InputDim {
1849                        input_idx: 0,
1850                        axis: 0,
1851                    }),
1852                    Box::new(DimExpr::InputDim {
1853                        input_idx: 1,
1854                        axis: 0,
1855                    }),
1856                )],
1857            ))
1858            .unwrap();
1859
1860        let (operation, inputs) = localize_shape_expressions(
1861            CoreSemanticOp::BroadcastInDim {
1862                shape: vec![
1863                    DimExpr::InputDim {
1864                        input_idx: 0,
1865                        axis: 0,
1866                    },
1867                    DimExpr::InputDim {
1868                        input_idx: 0,
1869                        axis: 1,
1870                    },
1871                ],
1872                dims: vec![1],
1873            },
1874            &[vector],
1875            &[matrix],
1876            &builder,
1877            SemanticAdRuleRole::Linearize,
1878        )
1879        .unwrap();
1880
1881        assert_eq!(inputs, vec![vector, matrix]);
1882        assert_eq!(
1883            operation,
1884            CoreSemanticOp::BroadcastInDim {
1885                shape: vec![
1886                    DimExpr::InputDim {
1887                        input_idx: 1,
1888                        axis: 0,
1889                    },
1890                    DimExpr::InputDim {
1891                        input_idx: 1,
1892                        axis: 1,
1893                    },
1894                ],
1895                dims: vec![1],
1896            }
1897        );
1898    }
1899}