Hash join spills to disk even though there is plenty of memory granted for the query
The 17GB memory grant is across the entire query. That 17GB isn't given in its entirety to a single operator. Each operator gets a fraction of the overall grant, and that amount is not dynamically reallocated during query execution.

If you go into the query's XML and search for MemoryFractions (or hover your mouse over operators in Plan Explorer), you'll see more information about the fractions assigned to each operator.

It shows the input side of the join gets 52.48%, but not the output side. If you have more memory required for the data coming out of the operator, you'll spill to disk.

To avoid the spill, tune indexes, tune the query, use the new min memory grant hint in 2016, etc.