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