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