chainweb-node icon indicating copy to clipboard operation
chainweb-node copied to clipboard

Improve FloydWarshall

Open mercadoa opened this issue 6 years ago • 11 comments

We use the Floyd-Warshall algorithm to calculate the diameter of chain graphs. This is mostly useful for testing, since in production, we would use known, fixed-diameter graphs.

massiv is employed in implementing the Data.DiGraph.FloydWarshall module, and its usage can be improved in a few ways.

Maximize Laziness

type DenseAdjMatrix = Array U Ix2 Int

fromAdjacencySets :: AdjacencySets -> DenseAdjMatrix
fromAdjacencySets g = makeArray Seq (n :. n) go
  where go = ...

As much as possible, we want our Array types to have the internal representation of D or DW. This avoids memory allocation until the last possible moment. When we do force them, asking for U, P, or S are generally equivalent (at least for our purposes).

floydWarshall :: Array U Ix2 Double -> Array U Ix2 Double

shortestPaths :: Array U Ix2 Int -> Array U Ix2 Double
shortestPaths = floydWarshall . computeAs U . distMatrix

Since everything is being forced as U here, a lot of extra memory is being allocated. computeAs should only be used:

  1. At the very end of a composition of matrix operations, or;
  2. Before doing a Stencil-based operation (like convolution, etc.)

Generally, for non-Stencil ops, there should always be a way to do everything lazily (via D) until the very end.

Parallelism

At least in test environments, we can assume graph sizes of > 1000 nodes. This means a Matrix of at least 1000x1000. In my experience with Massiv, for any Matrix over 256x256, using the Par evaluation strategy quickly becomes worthwhile.

fromAdjacencySets :: AdjacencySets -> DenseAdjMatrix
fromAdjacencySets g = makeArray Seq (n :. n) go  -- should be `Par`!

Then, when compiled/ran with the correct RTS ops, we get performance boosts for free.

Stencils

The implementation of the floydWarshall function in particular allocates a new Array for each interation, which means n unnecessary allocations are occuring, where n is the width/length of the Array. Glancing at the code, it seems like this could just be rewritten as a Stencil operation and done in a single pass.

IntMap / IntSet

type AdjacencySets = HM.HashMap Int (HS.HashSet Int)

Could this be rewritten in terms of IntMap and IntSet? fromAdjacencySets uses a lot of lookup and member calls, and the Int-based structures can perform these operations about twice as fast as the hash-based ones.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Lars Kuhtz commented:

When we have time it would be nice to do some benchmarks and see the effect of the different changes. I am particularly curious about the effect of adding laziness. It's not immediately obvious to me that it always pays off the delay forcing computations as much as possible, but I don't know enough about the inner workings of massiv.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Lars Kuhtz commented:

👍 for using IntMap IntSet in place of HM.HashMap Int (HS.HashSet Int).

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Colin Woodbury commented:

I am particularly curious about the effect of adding laziness.

It's less GHC laziness and more Massiv's internal "Delayed" representation. Using D properly improves performance drastically, especially when computed with parallelism.

IntMap IntSet

This would be fine, except in how an AdjacencySets type is come upon in Data.DiGraph:

newtype DiGraph a = DiGraph { unGraph :: HM.HashMap a (HS.HashSet a) }

diameter ::Eq a => Hashable a => DiGraph a -> Maybe Natural diameter g = FW.diameter $ FW.fromAdjacencySets (unGraph ig) where vmap = HM.fromList $ zip (HS.toList $ vertices g) [0..] ig = mapVertices (vmap HM.!) gNotice the call to unGraph. The DiGraph newtype would have to be reworked (perhaps with a type family?) to allow for a different internal representation, or we'd have to eat the cost of converting the preexisting HashMap/HashSets into their Int forms.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Lars Kuhtz commented:

type AdjacencySets = HM.HashMap Int (HS.HashSet Int)

This type is actually only used to convert between DiGraph and the matrix representation. So, it has to be precisely this type. It's not used for any computation.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Colin Woodbury commented:

It does seem to be used:

fromAdjacencySets :: AdjacencySets -> DenseAdjMatrix fromAdjacencySets g = makeArray Seq (n :. n) go where n = HM.size g go (i :. j) | isEdge (i, j) = 1 | isEdge (j, i) = 1 | otherwise = 0 isEdge (a, b) = maybe False (HS.member b) $ HM.lookup a gIn isEdge.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Lars Kuhtz commented:

fromAdjacencySets :: AdjacencySets -> DenseAdjMatrix

That's just the conversion function between the DiGraph representation and the the representation that is used in the Floyd-Warshall algorithm.

I don't think that first converting to IntMap IntSet would speed up things.

mercadoa avatar Apr 12 '19 15:04 mercadoa

➤ Colin Woodbury commented:

Exactly, if DiGraph needs the signature that it currently has, then we'd waste time converting to IntMap IntSet anyway.

mercadoa avatar Apr 12 '19 15:04 mercadoa

Need Lars to opine on whether this is still needed

mercadoa avatar Apr 12 '19 15:04 mercadoa

As per discussion with Lars - keep issue, but not a high priority / technical debt

mercadoa avatar Apr 12 '19 15:04 mercadoa

Moved issue over to chainweb-node (112)

mercadoa avatar Apr 16 '19 15:04 mercadoa

I accidentally stumbled upon this issue and though I could give a suggestion with regards to:

The implementation of the floydWarshall function in particular allocates a new Array for each interation, which means n unnecessary allocations are occuring, where n is the width/length of the Array.

There is a fairly new function in massiv that solves exactly this problem: iterateUntil Let me know if you have any questions on its usage.

lehins avatar Jul 29 '19 18:07 lehins