tensor.basic_opt – Tensor Optimizations

Tensor optimizations addressing the ops in basic.py.

class aesara.tensor.basic_opt.FusionOptimizer(local_optimizer)[source]

Graph optimizer that simply runs local fusion operations.

TODO: This is basically a EquilibriumOptimizer; we should just use that.

add_requirements(fgraph)[source]

Add Features and other requirements to a FunctionGraph.

apply(fgraph)[source]

Apply the optimization to a FunctionGraph.

It may use all the methods defined by the FunctionGraph. If the GlobalOptimizer needs to use a certain tool, such as an InstanceFinder, it can do so in its add_requirements method.

class aesara.tensor.basic_opt.InplaceElemwiseOptimizer(OP)[source]

This is parameterized so that it works for Elemwise Ops.

add_requirements(fgraph)[source]

Add Features and other requirements to a FunctionGraph.

apply(fgraph)[source]

Usage: InplaceElemwiseOptimizer(op).optimize(fgraph)

Attempts to replace all Broadcast ops by versions of them that operate inplace. It operates greedily: for each Broadcast Op that is encountered, for each output, tries each input to see if it can operate inplace on that input. If so, makes the change and go to the next output or Broadcast Op.

Examples

x + y + z -> x += y += z

(x + y) * (x * y) -> (x += y) *= (x * y) or (x + y) *= (x *= y)

print_summary(stream=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>, level=0, depth=-1)[source]

Print a single-line, indented representation of the rewriter.

class aesara.tensor.basic_opt.MakeVectorPrinter[source]
process(r, pstate)[source]

Construct a string representation for a Variable.

class aesara.tensor.basic_opt.ShapeFeature[source]

Graph optimizer for removing all calls to shape().

This optimizer replaces all Shapes and Subtensors of Shapes with Shape_i and MakeVector Ops.

This optimizer has several goals:

  1. to ‘lift’ Shapes to as close to the inputs as possible.
  2. to infer the shape of every node in the graph in terms of the input shapes.
  3. remove all fills (at.second, at.fill) from the graph

Lifting shapes as close to the inputs as possible is important for canonicalization because it is very bad form to have to compute something just to know how big it will be. Firstly, it is a waste of time to compute such outputs. But it is important to get rid of these outputs as early as possible in the compilation process because the extra computations make it appear as if many internal graph nodes have multiple clients. Many optimizations refuse to work on nodes with multiple clients.

Lifting is done by using an <Op>.infer_shape function if one is present, or else using a conservative default. An Op that supports shape-lifting should define a infer_shape(self, fgraph, node, input_shapes) function. The argument input_shapes is a tuple of tuples… there is an interior tuple for each input to the node. The tuple has as many elements as dimensions. The element in position i of tuple j represents the i’th shape component of the j’th input. The function should return a tuple of tuples. One output tuple for each node.output. Again, the i’th element of the j’th output tuple represents the output[j].shape[i] of the function. If an output is not a TensorType, then None should be returned instead of a tuple for that output.

For example the infer_shape for a matrix-matrix product would accept input_shapes=((x0,x1), (y0,y1)) and return ((x0, y1),).

Inferring the shape of internal nodes in the graph is important for doing size-driven optimizations. If we know how big various intermediate results will be, we can estimate the cost of many Ops accurately, and generate c-code that is specific [e.g. unrolled] to particular sizes.

In cases where you cannot figure out the shape, raise a ShapeError.

Notes

Right now there is only the ConvOp that could really take advantage of this shape inference, but it is worth it even just for the ConvOp. All that’s necessary to do shape inference is 1) to mark shared inputs as having a particular shape, either via a .tag or some similar hacking; and 2) to add an optional In() argument to promise that inputs will have a certain shape (or even to have certain shapes in certain dimensions). We can’t automatically infer the shape of shared variables as they can change of shape during the execution by default. (NOT IMPLEMENTED YET, BUT IS IN TRAC)

Using Shape information in Optimizations

To use this shape information in OPTIMIZATIONS, use the shape_of dictionary.

For example:

try:
    shape_of = fgraph.shape_feature.shape_of
except AttributeError:
    # This can happen when the mode doesn't include the ShapeFeature.
    return

shape_of_output_zero = shape_of[node.output[0]]

The shape_of_output_zero symbol will contain a tuple, whose elements are either integers or symbolic integers.

TODO: check to see if the symbols are necessarily non-constant… or are integer literals sometimes Aesara constants?? That would be confusing.

clone()[source]

Create a clone that can be attached to a new FunctionGraph.

This default implementation returns self, which carries the assumption that the Feature is essentially stateless. If a subclass has state of its own that is in any way relative to a given FunctionGraph, this method should be overridden with an implementation that actually creates a fresh copy.

default_infer_shape(fgraph, node, i_shapes)[source]

Return a list of shape tuple or None for the outputs of node.

This function is used for Ops that don’t implement infer_shape. Ops that do implement infer_shape should use the i_shapes parameter, but this default implementation ignores it.

get_shape(var, idx)[source]

Optimization can call this to get the current shape_i

It is better to call this then use directly shape_of[var][idx] as this method should update shape_of if needed.

TODO: Up to now, we don’t update it in all cases. Update in all cases.

init_r(r)[source]

Register r’s shape in the shape_of dictionary.

on_attach(fgraph)[source]

Called by FunctionGraph.attach_feature, the method that attaches the feature to the FunctionGraph. Since this is called after the FunctionGraph is initially populated, this is where you should run checks on the initial contents of the FunctionGraph.

The on_attach method may raise the AlreadyThere exception to cancel the attach operation if it detects that another Feature instance implementing the same functionality is already attached to the FunctionGraph.

The feature has great freedom in what it can do with the fgraph: it may, for example, add methods to it dynamically.

on_change_input(fgraph, node, i, r, new_r, reason)[source]

Called whenever node.inputs[i] is changed from var to new_var. At the moment the callback is done, the change has already taken place.

If you raise an exception in this function, the state of the graph might be broken for all intents and purposes.

on_detach(fgraph)[source]

Called by FunctionGraph.remove_feature. Should remove any dynamically-added functionality that it installed into the fgraph.

on_import(fgraph, node, reason)[source]

Called whenever a node is imported into fgraph, which is just before the node is actually connected to the graph.

Note: this is not called when the graph is created. If you want to detect the first nodes to be implemented to the graph, you should do this by implementing on_attach.

same_shape(x: aesara.graph.basic.Variable, y: aesara.graph.basic.Variable, dim_x: Optional[int] = None, dim_y: Optional[int] = None) bool[source]

Return True if x and y have the same shape.

Parameters:
  • x – The Variable for which its shape is to be compared with y’s shape.
  • y – The Variable for which its shape is to be compared with x’s shape.
  • dim_x – If non None, compare only the dimension of x equal to dim_x.
  • dim_y – If non None, compare only the dimension of y equal to dim_y.
set_shape(r, s, override=False)[source]

Assign the shape s to previously un-shaped variable r.

Parameters:
  • r (a variable) –
  • s (None or a tuple of symbolic integers) –
  • override (If False, it mean r is a new object in the fgraph.) – If True, it mean r is already in the fgraph and we want to override its shape.
set_shape_i(r, i, s_i)[source]

Replace element i of shape_of[r] by s_i

shape_ir(i, r)[source]

Return symbolic r.shape[i] for tensor variable r, int i.

shape_tuple(r)[source]

Return a tuple of symbolic shape vars for tensor variable r.

unpack(s_i, var)[source]

Return a symbolic integer scalar for the shape element s_i.

The s_i argument was produced by the infer_shape() of an Op subclass.

var: the variable that correspond to s_i. This is just for error reporting.

update_shape(r, other_r)[source]

Replace shape of r by shape of other_r.

If, on some dimensions, the shape of other_r is not informative, keep the shape of r on those dimensions.

class aesara.tensor.basic_opt.ShapeOptimizer[source]

Optimizer that adds ShapeFeature as a feature.

add_requirements(fgraph)[source]

Add Features and other requirements to a FunctionGraph.

apply(fgraph)[source]

Apply the optimization to a FunctionGraph.

It may use all the methods defined by the FunctionGraph. If the GlobalOptimizer needs to use a certain tool, such as an InstanceFinder, it can do so in its add_requirements method.

class aesara.tensor.basic_opt.UnShapeOptimizer[source]

Optimizer that removes ShapeFeature as a feature.

apply(fgraph)[source]

Apply the optimization to a FunctionGraph.

It may use all the methods defined by the FunctionGraph. If the GlobalOptimizer needs to use a certain tool, such as an InstanceFinder, it can do so in its add_requirements method.

aesara.tensor.basic_opt.apply_local_dimshuffle_lift(fgraph, var)[source]

lift recursively

aesara.tensor.basic_opt.apply_rebroadcast_opt(rval)[source]

Apply as many times as required the optimization local_useless_rebroadcast and local_rebroadcast_lift.

Parameters:rval (a Variable) –
Return type:A Variable (the same if no optimization can be applied)
aesara.tensor.basic_opt.broadcast_like(value, template, fgraph, dtype=None)[source]

Return a Variable with the same shape and dtype as the template, filled by broadcasting value through it. value will be cast as necessary.

aesara.tensor.basic_opt.encompasses_broadcastable(b1, b2)[source]
Parameters:
  • b1 – The broadcastable attribute of a tensor type.
  • b2 – The broadcastable attribute of a tensor type.
Returns:

True if the broadcastable patterns b1 and b2 are such that b2 is broadcasted to b1’s shape and not the opposite.

Return type:

bool

aesara.tensor.basic_opt.is_an_upcast(type1, type2)[source]

Given two data types (as strings), check if converting to type2 from type1 constitutes an upcast. Differs from aesara.scalar.upcast

aesara.tensor.basic_opt.is_dimshuffle_useless(new_order, input)[source]
Checks for two types of useless dimshuffles:
1 - dimshuffle all dimensions in order. 2 - dimshuffle a broadcastable dimension.
aesara.tensor.basic_opt.local_elemwise_fusion(fgraph, node)[source]

Fuse Elemwise Ops in a node.

As part of specialization, we fuse two consecutive Elemwise Ops of the same shape.

For mixed dtype, we let the Composite Op do the cast. It lets the C compiler do the cast.

The number of dimensions is validated at call time by Aesara itself.

aesara.tensor.basic_opt.local_elemwise_fusion_op(op_class, max_input_fct=<function <lambda>>, maker=None)[source]

Create a recursive function that fuses Elemwise Ops.

The basic idea is that we loop through an Elemwise node’s inputs, find other Elemwise nodes, determine the scalars input types for all of the Elemwise Ops, construct a new scalar Op using the scalar input types and each Elemwise’s scalar Op, and use the composite scalar Op in a new “fused” Elemwise.

It’s parameterized in order to work for Elemwise Ops.

Parameters:
  • op_class (type) – Elemwise class (the one that we want to fuse)
  • max_input_fct (callable) – A function that returns the maximum number of inputs that this Elemwise can take. On the CPU we limit to 32 input variables since that is the maximum NumPy support.
  • maker (callable) – A function with the signature (node, *args) that constructs an op_class instance (e.g. op_class(*args)).