tract_gpu/ops/
fused_output.rs1use tract_core::internal::*;
15
16use crate::fact::DeviceTypedFactExt;
17use crate::rule_ensure;
18use crate::tensor::DeviceTensor;
19use crate::turn_handler::make_tensor_for_node;
20
21use super::change_axes::GpuAxisOp;
22
23pub trait FusedOutputLayout {
26 fn out_axis_ops(&self) -> &[GpuAxisOp];
27 fn with_out_axis_ops(&self, ops: TVec<GpuAxisOp>) -> Box<dyn TypedOp>;
28}
29
30pub fn as_fused_output(op: &dyn TypedOp) -> Option<&dyn FusedOutputLayout> {
32 if let Some(op) = op.downcast_ref::<super::pulse::GpuDelay>() {
33 return Some(op);
34 }
35 None
36}
37
38fn eval_axis_op(op: &AxisOp, symbols: &SymbolValues) -> AxisOp {
39 match op {
40 AxisOp::Reshape(skip, from, to) => AxisOp::Reshape(
41 *skip,
42 from.iter().map(|d| d.eval(symbols)).collect(),
43 to.iter().map(|d| d.eval(symbols)).collect(),
44 ),
45 op => op.clone(),
46 }
47}
48
49pub fn chain_shape(
51 shape: &[usize],
52 chain: &[GpuAxisOp],
53 symbols: &SymbolValues,
54) -> TractResult<TVec<usize>> {
55 let mut shape: TVec<usize> = shape.into();
56 for op in chain {
57 eval_axis_op(&op.inner, symbols).change_shape_array(&mut shape, false)?;
58 }
59 Ok(shape)
60}
61
62fn concrete(dims: &[TDim]) -> TractResult<TVec<usize>> {
63 dims.iter().map(|d| Ok(d.to_usize()?)).collect()
64}
65
66pub fn unchain_strides(
70 shape: &[usize],
71 strides: &[isize],
72 chain: &[GpuAxisOp],
73 symbols: &SymbolValues,
74) -> TractResult<(TVec<usize>, TVec<isize>)> {
75 let mut shape: TVec<usize> = shape.into();
76 let mut strides: TVec<isize> = strides.into();
77 for op in chain.iter().rev() {
78 match eval_axis_op(&op.inner, symbols) {
79 AxisOp::Add(ix) => {
80 ensure!(shape[ix] == 1, "Add({ix}) carries extent {}", shape[ix]);
81 shape.remove(ix);
82 strides.remove(ix);
83 }
84 AxisOp::Rm(ix) => {
85 let stride = strides.get(ix).copied().unwrap_or(1);
86 shape.insert(ix, 1);
87 strides.insert(ix, stride);
88 }
89 AxisOp::Move(from, to) => {
90 let dim = shape.remove(to);
91 shape.insert(from, dim);
92 let stride = strides.remove(to);
93 strides.insert(from, stride);
94 }
95 AxisOp::Reshape(skip, from, to) => {
96 let (from, to) = (concrete(&from)?, concrete(&to)?);
97 let group = &strides[skip..skip + to.len()];
98 let inner = *group.last().context("Reshape to no axis at all")?;
99 let mut nested = inner;
100 for (dim, stride) in to.iter().zip(group).rev() {
101 ensure!(
102 *stride == nested,
103 "Reshaped group is not contiguous, so its split has no strides"
104 );
105 nested *= *dim as isize;
106 }
107 let mut split: TVec<isize> = tvec!();
108 let mut stride = inner;
109 for dim in from.iter().rev() {
110 split.insert(0, stride);
111 stride *= *dim as isize;
112 }
113 let tail: TVec<usize> = shape[skip + to.len()..].into();
114 shape.truncate(skip);
115 shape.extend(from.iter().copied().chain(tail));
116 let tail: TVec<isize> = strides[skip + to.len()..].into();
117 strides.truncate(skip);
118 strides.extend(split.into_iter().chain(tail));
119 }
120 }
121 }
122 Ok((shape, strides))
123}
124
125pub fn make_fused_output_for_node(
129 ctx: &EvalContext,
130 dt: DatumType,
131 shape: &[usize],
132 chain: &[GpuAxisOp],
133) -> TractResult<(DeviceTensor, DeviceTensor)> {
134 if chain.is_empty() {
135 let output = make_tensor_for_node(ctx, dt, shape)?;
136 return Ok((output.clone(), output));
137 }
138 let chained = chain_shape(shape, chain, ctx.symbols)?;
139 let published = make_tensor_for_node(ctx, dt, &chained)?;
140 let (unchained, strides) =
141 unchain_strides(published.shape(), published.strides(), chain, ctx.symbols)?;
142 ensure!(unchained.as_slice() == shape, "Output chain leads to {unchained:?}, not {shape:?}");
143 let view = published.reshaped(shape.into())?.restrided(strides)?;
144 Ok((view, published))
145}
146
147fn keeps_lane_axis(op: &GpuAxisOp) -> bool {
151 match op.inner {
152 AxisOp::Add(ix) | AxisOp::Rm(ix) => ix > 0,
153 AxisOp::Move(from, to) => from > 0 && to > 0,
154 AxisOp::Reshape(skip, _, _) => skip > 0,
155 }
156}
157
158fn keeps_runs(rank: usize, chain: &[GpuAxisOp]) -> bool {
162 let mut axis = rank - 1;
163 let mut rank = rank;
164 for op in chain {
165 let Some(next) = op.inner.transform_axis(axis) else { return false };
166 axis = next;
167 rank = match &op.inner {
168 AxisOp::Add(_) => rank + 1,
169 AxisOp::Rm(_) => rank - 1,
170 AxisOp::Move(_, _) => rank,
171 AxisOp::Reshape(_, from, to) => rank + to.len() - from.len(),
172 };
173 }
174 axis + 1 == rank
175}
176
177pub fn fuse_output_axis_op(
180 _ctx: &(),
181 model: &TypedModel,
182 node: &TypedNode,
183 _node_name: &str,
184 axis_op: &GpuAxisOp,
185) -> TractResult<Option<TypedModelPatch>> {
186 rule_ensure!(keeps_lane_axis(axis_op));
187 rule_if_some!(producer = model.single_prec(node.id)?);
188 rule_if_some!(fused = as_fused_output(producer.op.as_ref()));
189 rule_ensure!(fused.out_axis_ops().is_empty());
190 rule_ensure!(producer.op.forwards_input().is_none());
193 rule_ensure!(model.single_succ(producer.id)?.is_some());
194
195 let mut chain = tvec!(axis_op.clone());
196 let mut cursor = node;
197 while let Some(next) = model.single_succ(cursor.id)? {
198 let Some(next_op) = next.op_as::<GpuAxisOp>().filter(|o| keeps_lane_axis(o)) else {
199 break;
200 };
201 chain.push(next_op.clone());
202 cursor = next;
203 }
204
205 rule_ensure!(chain.iter().any(|op| matches!(op.inner, AxisOp::Move(..))));
209
210 let rank = model.node_output_facts(producer.id)?[0]
211 .as_device_fact()
212 .map(|f| f.shape.rank())
213 .unwrap_or_else(|| model.node_output_facts(producer.id).unwrap()[0].rank());
214 rule_ensure!(keeps_runs(rank, &chain));
215
216 let mut patch = TypedModelPatch::default();
217 let inputs = patch.taps(model, &producer.inputs)?;
218 let out = patch.wire_node(producer.name.clone(), fused.with_out_axis_ops(chain), &inputs)?;
219 patch.shunt_outside(model, cursor.id.into(), out[0])?;
220 Ok(Some(patch))
221}