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