The Branching Path¶
optimal path is guaranteed to find the smallest estimate FLOP
cost, it spends a lot of time exploring paths which are not likely to result in
an optimal path. For instance, outer products are usually not advantageous
unless absolutely necessary. Additionally, by trying a ‘good’ path first, it
should be possible to quickly establish a threshold FLOP cost which can then be
used to prune many bad paths.
The branching strategy (provided by
this by taking the recursive, depth-first approach of
optimal(), whilst also sorting potential contractions
based on a heuristic cost, as in
There are two main flavours:
optimize='branch-all': explore all inner products, starting with those that look best according to the cost heuristic.
optimize='branch-2': similar, but at each step only explore the estimated best two possible contractions, leading to a maximum of 2^N paths assessed.
In both cases,
branch() takes an active approach to
pruning paths well before they hit the best total FLOP count, by comparing
them to the FLOP count (times some factor) achieved by the best path at the
same point in the contraction.
There is also
'branch-1', which, since it only explores a single path at
each step does not really ‘branch’ - this is essentially the approach of
'branch-1' will be slower for large expressions, but for
small to medium expressions it might find slightly higher quality contractions
due to considering individual flop costs at each step.
optimize='auto' mode of
opt_einsum will use
'branch-all' for 5 or 6 tensors, though it should be able to handle
12-13 tensors in a matter or seconds. Likewise,
'branch-2' will be used for
7 or 8 tensors, though it should be able to handle 20-22 tensors in a matter of
'branch-1' will be used by
'auto' for expressions of
up to 14 tensors.
Customizing the Branching Path¶
The ‘branch and bound’ path can be customized by creating a custom
BranchBound instance. For example:
optimizer = oe.BranchBound(nbranch=3, minimize='size', cutoff_flops_factor=None) path, path_info = oe.contract_path(eq, *arrays, optimize=optimizer)
You could then tweak the settings (e.g.
optimizer.nbranch = 4) and the best
bound found so far will persist and be used to prune paths on the next call:
optimizer.nbranch = 4 path, path_info = oe.contract_path(eq, *arrays, optimize=optimizer)