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