tenferro_runtime/graph/lowering_view.rs
1use std::fmt;
2
3use tenferro_ops::ext_op::ExtensionOp;
4use tenferro_ops::ShapeExtent;
5use tenferro_tensor::{DType, DotGeneralConfig};
6
7use crate::exec::{ExecInstruction, ExecOp, ExecProgram};
8
9/// Read-only lowering view over a compiled graph program.
10///
11/// This view is for peer executor crates that need to translate a
12/// [`GraphProgram`](super::GraphProgram) without mutating the runtime-owned
13/// execution program.
14///
15/// # Examples
16///
17/// ```
18/// use tenferro_runtime::{GraphCompiler, TracedTensor};
19///
20/// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
21/// let mut compiler = GraphCompiler::new();
22/// let y = x.neg().unwrap();
23/// let program = compiler.compile(&y).unwrap();
24/// let view = program.lowering_view();
25/// assert_eq!(view.output_slots().len(), 1);
26/// ```
27#[derive(Clone, Copy)]
28pub struct GraphProgramLoweringView<'a> {
29 exec: &'a ExecProgram,
30}
31
32impl fmt::Debug for GraphProgramLoweringView<'_> {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.debug_struct("GraphProgramLoweringView")
35 .field("slot_count", &self.slot_count())
36 .field("input_count", &self.input_slots().len())
37 .field("output_count", &self.output_slots().len())
38 .field("instruction_count", &self.exec.instructions.len())
39 .finish()
40 }
41}
42
43impl<'a> GraphProgramLoweringView<'a> {
44 pub(crate) fn new(exec: &'a ExecProgram) -> Self {
45 Self { exec }
46 }
47
48 /// Return the number of execution slots used by the program.
49 ///
50 /// # Examples
51 ///
52 /// ```
53 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
54 ///
55 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
56 /// let mut compiler = GraphCompiler::new();
57 /// let y = x.neg().unwrap();
58 /// let program = compiler.compile(&y).unwrap();
59 /// assert!(program.lowering_view().slot_count() >= 1);
60 /// ```
61 pub fn slot_count(&self) -> usize {
62 self.exec.n_slots
63 }
64
65 /// Return the execution slots populated by graph inputs.
66 ///
67 /// # Examples
68 ///
69 /// ```
70 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
71 ///
72 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
73 /// let mut compiler = GraphCompiler::new();
74 /// let y = x.neg().unwrap();
75 /// let program = compiler.compile(&y).unwrap();
76 /// assert_eq!(program.lowering_view().input_slots().len(), 1);
77 /// ```
78 pub fn input_slots(&self) -> &'a [usize] {
79 &self.exec.input_slots
80 }
81
82 /// Return the execution slots used as program outputs.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
88 ///
89 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
90 /// let mut compiler = GraphCompiler::new();
91 /// let y = x.neg().unwrap();
92 /// let program = compiler.compile(&y).unwrap();
93 /// assert_eq!(program.lowering_view().output_slots().len(), 1);
94 /// ```
95 pub fn output_slots(&self) -> &'a [usize] {
96 &self.exec.output_slots
97 }
98
99 /// Iterate over read-only instruction views in execution order.
100 ///
101 /// # Examples
102 ///
103 /// ```
104 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
105 ///
106 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
107 /// let mut compiler = GraphCompiler::new();
108 /// let y = x.neg().unwrap();
109 /// let program = compiler.compile(&y).unwrap();
110 /// assert!(program.lowering_view().instructions().count() >= 1);
111 /// ```
112 pub fn instructions(&self) -> impl ExactSizeIterator<Item = GraphInstructionView<'a>> + '_ {
113 self.exec.instructions.iter().map(GraphInstructionView::new)
114 }
115}
116
117/// Read-only lowering view over one execution instruction.
118///
119/// # Examples
120///
121/// ```
122/// use tenferro_runtime::{GraphCompiler, TracedTensor};
123///
124/// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
125/// let mut compiler = GraphCompiler::new();
126/// let y = x.neg().unwrap();
127/// let program = compiler.compile(&y).unwrap();
128/// let inst = program.lowering_view().instructions().next().unwrap();
129/// assert_eq!(inst.output_slots().len(), 1);
130/// ```
131#[derive(Clone, Copy)]
132pub struct GraphInstructionView<'a> {
133 inst: &'a ExecInstruction,
134}
135
136impl fmt::Debug for GraphInstructionView<'_> {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.debug_struct("GraphInstructionView")
139 .field("op", &self.op_name())
140 .field("input_count", &self.input_slots().len())
141 .field("output_count", &self.output_slots().len())
142 .field("dtype", &self.dtype())
143 .finish()
144 }
145}
146
147impl<'a> GraphInstructionView<'a> {
148 fn new(inst: &'a ExecInstruction) -> Self {
149 Self { inst }
150 }
151
152 /// Return the operation view for this instruction.
153 ///
154 /// # Examples
155 ///
156 /// ```
157 /// use tenferro_runtime::{GraphCompiler, GraphOpView, TracedTensor};
158 ///
159 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
160 /// let mut compiler = GraphCompiler::new();
161 /// let y = x.neg().unwrap();
162 /// let program = compiler.compile(&y).unwrap();
163 /// let inst = program.lowering_view().instructions().next().unwrap();
164 /// assert!(matches!(inst.op(), GraphOpView::Negate));
165 /// ```
166 pub fn op(&self) -> GraphOpView<'a> {
167 match &self.inst.op {
168 ExecOp::Constant { dtype, bytes } => GraphOpView::Constant {
169 dtype: *dtype,
170 bytes,
171 },
172 ExecOp::Add => GraphOpView::Add,
173 ExecOp::Multiply => GraphOpView::Multiply,
174 ExecOp::Negate => GraphOpView::Negate,
175 ExecOp::Divide => GraphOpView::Divide,
176 ExecOp::Abs => GraphOpView::Abs,
177 ExecOp::Exp => GraphOpView::Exp,
178 ExecOp::Log => GraphOpView::Log,
179 ExecOp::Sin => GraphOpView::Sin,
180 ExecOp::Cos => GraphOpView::Cos,
181 ExecOp::Tanh => GraphOpView::Tanh,
182 ExecOp::Sqrt => GraphOpView::Sqrt,
183 ExecOp::Rsqrt => GraphOpView::Rsqrt,
184 ExecOp::Pow => GraphOpView::Pow,
185 ExecOp::Expm1 => GraphOpView::Expm1,
186 ExecOp::Log1p => GraphOpView::Log1p,
187 ExecOp::Convert { to } => GraphOpView::Convert { to: *to },
188 ExecOp::Reshape { .. } => GraphOpView::Reshape,
189 ExecOp::BroadcastInDim { dims, .. } => GraphOpView::BroadcastInDim { dims },
190 ExecOp::Transpose { perm } => GraphOpView::Transpose { perm },
191 ExecOp::ReduceSum { axes } => GraphOpView::ReduceSum { axes },
192 ExecOp::DotGeneral(config) => GraphOpView::DotGeneral { config },
193 ExecOp::Extension(op) => GraphOpView::Extension { op: op.as_ref() },
194 other => GraphOpView::Unsupported {
195 name: exec_op_name(other),
196 },
197 }
198 }
199
200 /// Return a stable operation name for diagnostics.
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
206 ///
207 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
208 /// let mut compiler = GraphCompiler::new();
209 /// let y = x.neg().unwrap();
210 /// let program = compiler.compile(&y).unwrap();
211 /// let inst = program.lowering_view().instructions().next().unwrap();
212 /// assert_eq!(inst.op_name(), "Negate");
213 /// ```
214 pub fn op_name(&self) -> &'static str {
215 self.op().name()
216 }
217
218 /// Return the input slots consumed by this instruction.
219 ///
220 /// # Examples
221 ///
222 /// ```
223 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
224 ///
225 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
226 /// let y = (&x + &x).unwrap();
227 /// let mut compiler = GraphCompiler::new();
228 /// let program = compiler.compile(&y).unwrap();
229 /// let inst = program.lowering_view().instructions().next().unwrap();
230 /// assert_eq!(inst.input_slots().len(), 2);
231 /// ```
232 pub fn input_slots(&self) -> &'a [usize] {
233 &self.inst.input_slots
234 }
235
236 /// Return the output slots written by this instruction.
237 ///
238 /// # Examples
239 ///
240 /// ```
241 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
242 ///
243 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
244 /// let mut compiler = GraphCompiler::new();
245 /// let y = x.neg().unwrap();
246 /// let program = compiler.compile(&y).unwrap();
247 /// let inst = program.lowering_view().instructions().next().unwrap();
248 /// assert_eq!(inst.output_slots().len(), 1);
249 /// ```
250 pub fn output_slots(&self) -> &'a [usize] {
251 &self.inst.output_slots
252 }
253
254 /// Return the dtype of this instruction's output.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use tenferro_runtime::{DType, GraphCompiler, TracedTensor};
260 ///
261 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
262 /// let mut compiler = GraphCompiler::new();
263 /// let y = x.neg().unwrap();
264 /// let program = compiler.compile(&y).unwrap();
265 /// let inst = program.lowering_view().instructions().next().unwrap();
266 /// assert_eq!(inst.dtype(), DType::F64);
267 /// ```
268 pub fn dtype(&self) -> DType {
269 self.inst.dtype
270 }
271
272 /// Resolve an exact static output shape for this instruction.
273 ///
274 /// `input_shapes` must be ordered the same way as [`Self::input_slots`].
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
280 ///
281 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
282 /// let mut compiler = GraphCompiler::new();
283 /// let y = x.neg().unwrap();
284 /// let program = compiler.compile(&y).unwrap();
285 /// let inst = program.lowering_view().instructions().next().unwrap();
286 /// assert_eq!(inst.static_output_shape(0, &[&[1]]).unwrap(), vec![1]);
287 /// ```
288 pub fn static_output_shape(
289 &self,
290 output_index: usize,
291 input_shapes: &[&[usize]],
292 ) -> std::result::Result<Vec<usize>, GraphProgramLoweringShapeError> {
293 let extents = self.inst.output_extents.get(output_index).ok_or(
294 GraphProgramLoweringShapeError::MissingOutput {
295 op: self.op_name(),
296 output_index,
297 },
298 )?;
299 let mut shape = Vec::with_capacity(extents.len());
300 for (axis, extent) in extents.iter().enumerate() {
301 match extent {
302 ShapeExtent::Exact(dim) => shape.push(dim.eval(input_shapes).map_err(|err| {
303 GraphProgramLoweringShapeError::InvalidDimExpr {
304 op: self.op_name(),
305 output_index,
306 axis,
307 source: err,
308 }
309 })?),
310 ShapeExtent::UpperBound(_) => {
311 return Err(GraphProgramLoweringShapeError::NonStatic {
312 op: self.op_name(),
313 output_index,
314 axis,
315 kind: "an upper bound",
316 });
317 }
318 ShapeExtent::Unknown => {
319 return Err(GraphProgramLoweringShapeError::NonStatic {
320 op: self.op_name(),
321 output_index,
322 axis,
323 kind: "unknown",
324 });
325 }
326 }
327 }
328 Ok(shape)
329 }
330}
331
332/// Read-only operation view for graph lowering integrations.
333///
334/// Unsupported operation families are represented as [`GraphOpView::Unsupported`]
335/// so peer executors can emit precise diagnostics without depending on the raw
336/// execution IR.
337///
338/// # Examples
339///
340/// ```
341/// use tenferro_runtime::{GraphCompiler, GraphOpView, TracedTensor};
342///
343/// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
344/// let mut compiler = GraphCompiler::new();
345/// let y = x.neg().unwrap();
346/// let program = compiler.compile(&y).unwrap();
347/// let op = program.lowering_view().instructions().next().unwrap().op();
348/// assert!(matches!(op, GraphOpView::Negate));
349/// ```
350#[derive(Clone, Copy)]
351pub enum GraphOpView<'a> {
352 /// Scalar constant payload.
353 Constant { dtype: DType, bytes: &'a [u8] },
354 /// Elementwise addition.
355 Add,
356 /// Elementwise multiplication.
357 Multiply,
358 /// Elementwise negation.
359 Negate,
360 /// Elementwise division.
361 Divide,
362 /// Elementwise absolute value.
363 Abs,
364 /// Elementwise exponential.
365 Exp,
366 /// Elementwise natural logarithm.
367 Log,
368 /// Elementwise sine.
369 Sin,
370 /// Elementwise cosine.
371 Cos,
372 /// Elementwise hyperbolic tangent.
373 Tanh,
374 /// Elementwise square root.
375 Sqrt,
376 /// Elementwise reciprocal square root.
377 Rsqrt,
378 /// Elementwise power.
379 Pow,
380 /// Elementwise exponential minus one.
381 Expm1,
382 /// Elementwise natural logarithm of one plus input.
383 Log1p,
384 /// Dtype conversion.
385 Convert { to: DType },
386 /// Shape-only reshape.
387 Reshape,
388 /// Broadcast with output-to-input dimension mapping.
389 BroadcastInDim { dims: &'a [usize] },
390 /// Transpose with output dimension permutation.
391 Transpose { perm: &'a [usize] },
392 /// Sum reduction.
393 ReduceSum { axes: &'a [usize] },
394 /// General dot/contraction.
395 DotGeneral { config: &'a DotGeneralConfig },
396 /// Extension operation with an owner-provided optional standard-op lowering.
397 Extension { op: &'a dyn ExtensionOp },
398 /// Operation outside the stable public lowering view.
399 Unsupported { name: &'static str },
400}
401
402impl fmt::Debug for GraphOpView<'_> {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 match self {
405 Self::Constant { dtype, bytes } => f
406 .debug_struct("Constant")
407 .field("dtype", dtype)
408 .field("byte_len", &bytes.len())
409 .finish(),
410 Self::Add => f.write_str("Add"),
411 Self::Multiply => f.write_str("Multiply"),
412 Self::Negate => f.write_str("Negate"),
413 Self::Divide => f.write_str("Divide"),
414 Self::Abs => f.write_str("Abs"),
415 Self::Exp => f.write_str("Exp"),
416 Self::Log => f.write_str("Log"),
417 Self::Sin => f.write_str("Sin"),
418 Self::Cos => f.write_str("Cos"),
419 Self::Tanh => f.write_str("Tanh"),
420 Self::Sqrt => f.write_str("Sqrt"),
421 Self::Rsqrt => f.write_str("Rsqrt"),
422 Self::Pow => f.write_str("Pow"),
423 Self::Expm1 => f.write_str("Expm1"),
424 Self::Log1p => f.write_str("Log1p"),
425 Self::Convert { to } => f.debug_struct("Convert").field("to", to).finish(),
426 Self::Reshape => f.write_str("Reshape"),
427 Self::BroadcastInDim { dims } => f
428 .debug_struct("BroadcastInDim")
429 .field("dims", dims)
430 .finish(),
431 Self::Transpose { perm } => f.debug_struct("Transpose").field("perm", perm).finish(),
432 Self::ReduceSum { axes } => f.debug_struct("ReduceSum").field("axes", axes).finish(),
433 Self::DotGeneral { config } => f
434 .debug_struct("DotGeneral")
435 .field("config", config)
436 .finish(),
437 Self::Extension { op } => f
438 .debug_struct("Extension")
439 .field("family_id", &op.family_id())
440 .finish(),
441 Self::Unsupported { name } => {
442 f.debug_struct("Unsupported").field("name", name).finish()
443 }
444 }
445 }
446}
447
448impl GraphOpView<'_> {
449 /// Return the stable operation name used in diagnostics.
450 ///
451 /// # Examples
452 ///
453 /// ```
454 /// use tenferro_runtime::{GraphCompiler, TracedTensor};
455 ///
456 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
457 /// let mut compiler = GraphCompiler::new();
458 /// let y = x.neg().unwrap();
459 /// let program = compiler.compile(&y).unwrap();
460 /// let op = program.lowering_view().instructions().next().unwrap().op();
461 /// assert_eq!(op.name(), "Negate");
462 /// ```
463 pub fn name(&self) -> &'static str {
464 match self {
465 Self::Constant { .. } => "Constant",
466 Self::Add => "Add",
467 Self::Multiply => "Multiply",
468 Self::Negate => "Negate",
469 Self::Divide => "Divide",
470 Self::Abs => "Abs",
471 Self::Exp => "Exp",
472 Self::Log => "Log",
473 Self::Sin => "Sin",
474 Self::Cos => "Cos",
475 Self::Tanh => "Tanh",
476 Self::Sqrt => "Sqrt",
477 Self::Rsqrt => "Rsqrt",
478 Self::Pow => "Pow",
479 Self::Expm1 => "Expm1",
480 Self::Log1p => "Log1p",
481 Self::Convert { .. } => "Convert",
482 Self::Reshape => "Reshape",
483 Self::BroadcastInDim { .. } => "BroadcastInDim",
484 Self::Transpose { .. } => "Transpose",
485 Self::ReduceSum { .. } => "ReduceSum",
486 Self::DotGeneral { .. } => "DotGeneral",
487 Self::Extension { .. } => "Extension",
488 Self::Unsupported { name } => name,
489 }
490 }
491}
492
493/// Error returned when a lowering view cannot resolve an exact output shape.
494///
495/// # Examples
496///
497/// ```
498/// use tenferro_runtime::GraphProgramLoweringShapeError;
499///
500/// let err = GraphProgramLoweringShapeError::MissingOutput {
501/// op: "Example",
502/// output_index: 0,
503/// };
504/// assert!(err.to_string().contains("Example"));
505/// ```
506#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
507pub enum GraphProgramLoweringShapeError {
508 /// The instruction has no metadata for the requested output.
509 #[error("ExecOp::{op} missing output_extents for output {output_index}")]
510 MissingOutput {
511 op: &'static str,
512 output_index: usize,
513 },
514 /// The instruction output has dynamic or unknown extent metadata.
515 #[error("ExecOp::{op} output {output_index} axis {axis} has non-static extent: {kind}")]
516 NonStatic {
517 op: &'static str,
518 output_index: usize,
519 axis: usize,
520 kind: &'static str,
521 },
522 /// Static shape evaluation failed for an exact dimension expression.
523 #[error(
524 "ExecOp::{op} output {output_index} axis {axis} has invalid dimension expression: {source}"
525 )]
526 InvalidDimExpr {
527 op: &'static str,
528 output_index: usize,
529 axis: usize,
530 source: tenferro_ops::dim_expr::DimExprEvalError,
531 },
532}
533
534fn exec_op_name(op: &ExecOp) -> &'static str {
535 match op {
536 ExecOp::Transpose { .. } => "Transpose",
537 ExecOp::Reshape { .. } => "Reshape",
538 ExecOp::BroadcastInDim { .. } => "BroadcastInDim",
539 ExecOp::Convert { .. } => "Convert",
540 ExecOp::Constant { .. } => "Constant",
541 ExecOp::DotGeneral(_) => "DotGeneral",
542 ExecOp::DotGeneralWithConj { .. } => "DotGeneralWithConj",
543 ExecOp::ReduceSum { .. } => "ReduceSum",
544 ExecOp::ExtractDiag { .. } => "ExtractDiag",
545 ExecOp::EmbedDiag { .. } => "EmbedDiag",
546 ExecOp::Tril { .. } => "Tril",
547 ExecOp::Triu { .. } => "Triu",
548 ExecOp::Add => "Add",
549 ExecOp::Subtract => "Subtract",
550 ExecOp::Multiply => "Multiply",
551 ExecOp::Negate => "Negate",
552 ExecOp::Conj => "Conj",
553 ExecOp::Divide => "Divide",
554 ExecOp::Remainder => "Remainder",
555 ExecOp::Abs => "Abs",
556 ExecOp::Sign => "Sign",
557 ExecOp::Maximum => "Maximum",
558 ExecOp::Minimum => "Minimum",
559 ExecOp::Compare(_) => "Compare",
560 ExecOp::Select => "Select",
561 ExecOp::Clamp => "Clamp",
562 ExecOp::Exp => "Exp",
563 ExecOp::Log => "Log",
564 ExecOp::Sin => "Sin",
565 ExecOp::Cos => "Cos",
566 ExecOp::Tanh => "Tanh",
567 ExecOp::Sqrt => "Sqrt",
568 ExecOp::Rsqrt => "Rsqrt",
569 ExecOp::Pow => "Pow",
570 ExecOp::Expm1 => "Expm1",
571 ExecOp::Log1p => "Log1p",
572 ExecOp::Gather(_) => "Gather",
573 ExecOp::GatherDynamicSliceSizes { .. } => "GatherDynamicSliceSizes",
574 ExecOp::Scatter(_) => "Scatter",
575 ExecOp::Slice(_) => "Slice",
576 ExecOp::DynamicSlice { .. } => "DynamicSlice",
577 ExecOp::DynamicUpdateSlice => "DynamicUpdateSlice",
578 ExecOp::Pad(_) => "Pad",
579 ExecOp::Concatenate { .. } => "Concatenate",
580 ExecOp::Reverse { .. } => "Reverse",
581 ExecOp::ShapeOf { .. } => "ShapeOf",
582 ExecOp::DynamicTruncate { .. } => "DynamicTruncate",
583 ExecOp::PadToMatch { .. } => "PadToMatch",
584 ExecOp::ReduceProd { .. } => "ReduceProd",
585 ExecOp::ReduceMax { .. } => "ReduceMax",
586 ExecOp::ReduceMin { .. } => "ReduceMin",
587 ExecOp::Extension(_) => "Extension",
588 }
589}
590
591#[cfg(test)]
592mod tests;