Speed up getindex and findindex for ProductSector SectorValues - #106
borisdevos wants to merge 3 commits into
Conversation
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
lkdvos
left a comment
There was a problem hiding this comment.
This is definitely cool! Do you have an idea which part is affecting the speed the most, the tabulation or the short circuit?
I also don't really know how much this actually affects performance of downstream algorithms, it seems like the affect on the functions that end up being called isn't enormous, and these seem to be very small numbers, but I don't actually know how often they are called.
For dim for example, it seems like there might be a much larger speedup from just changing the implementation here:
to something like:
function dim(V::GradedSpace{I, <:AbstractDict})
init = 0 * dim(first(allunits(sectortype(V))))
return sum((c, d) -> dim(c) * d, pairs(V.dims); init)
end
function dim(V::GradedSpace{I, <:Tuple})
init = 0 * dim(first(allunits(sectortype(V))))
return sum(((c, d),) -> dim(c) * d, zip(values(V). V.dims)); init)
endIn other words, completely bypassing the hash lookup that was taking place in the dim(V, c) call.
The main reason I'm saying this is that I'm a bit scared of adding more caches, since people tend to complain about this eating up space and this being nontransparent 🙃
| return binomial(d + N - 1, N - 1) | ||
| catch e | ||
| e isa OverflowError || rethrow() | ||
| return nothing # fallback to recursion |
There was a problem hiding this comment.
Did you hit this case somewhere specifically? I did not look at this in detail, but I somehow would have expected that if the binomial function overflows also the recursive function would?
There was a problem hiding this comment.
Yeah you're right, which really means that previously you could've potentially summed up incorrectly in the recursion, and now I'm catching it in a subset of cases. Although I think realistically this can't be reached, as this requires a crazy amount of product sectors, so maybe I can just remove this?
Btw I didn't hit this myself, it's something I read in the docstring of factorial so I thought I should catch it, but clearly I didn't think enough about this being useful or not.
| const TABLES = IdDict{DataType, Any}() | ||
| const TABLE_LOCK = ReentrantLock() # dictionaries aren't thread-safe for concurrent mutation |
There was a problem hiding this comment.
I wonder if it might be worth it to try something similar to TensorKit's caches for this, using an LRU which is both threadsafe as well as avoids keeping too many entries around.
| multi::Vector{NTuple{N, Int}} # Manhattan index -> multi-index | ||
| lin::Array{Int, N} # multi-index -> Manhattan index |
There was a problem hiding this comment.
Do you need both here? Isn't the manhattan index 1:n, so as long as you store the multi index in sorted order by their linear index you could fold both into a single array, using the array index as the key.
There was a problem hiding this comment.
I guess the tradeoffs occurring here as that you save a bit of memory removing lin (not much compared to multi, though), but looking for a multi-index given the Manhattan index requires a small binary search (which google tells me is log(n)). Is that worth it?
There was a problem hiding this comment.
I also realized that I wasn't really following what was going on here anyways, and I think my suggestion is wrong and that would defeat the entire point, the ordering is important 🙃
It's the tabulation, once a sector type is tabulated you never call on the auxiliary functions again.
That's true, and is something I was also looking into; whether there were multiple commonly called points in TensorKit which treated ntuple and dictionary storage equally and perhaps inefficiently. However, the speedup here is already gotten purely through this tabulation. Your suggestion to
Fair argument, and I think the suggestion you made concerning LRU caching is a valid approach. |
|
Closing this as the effects of QuantumKitHub/TensorKit.jl#511, particularly the new tuple-to-sectordict cutoff, make the proposed ideas here (among others I was playing around with) completely redundant. |
TLDR: it's faster.
The current Manhattan-distance enumeration was implemented just recursively, which scaled like distance^(N-1) for N sector types in the product sector. When it comes to finite product sectors within TensorKit,
getindexandfindindexare used frequently in constructing graded spaces.Here, I propose a speedup that exists of two parts.
num_manhattan_pointsin these cases. Otherwise, still calculate recursively. I also limited this behavior to a certain amount of product sector types just based on complexity, but it might be an unnecessary complexity (other definition) of the code.islessof product sectors, and cache this in a table. Thenfindindexandgetindexare just array lookups instead of recursive calculations.There's also a fix related to checking bounds. This is because, as of now, when getting an index outside of the length of sector values, it would get stuck in a while loop. I added
checksectorindexto check the bounds. Everything's done in a way that functions can be inlined, but I don't really know how that works. I think it's worth it in this case, but please be critical about it. This infinite loop is now unreachable through the API.A comment before the benchmarks: As of now I set the cutoff to tabulating to some value large enough that finite product sector types will most likely tabulate. Whether this tabulation matters in practice depends on
Vect[I]usingNTupleorSectorDictstorage. Currently, every finite sectors falls within the former, but I'm looking into introducing a cutoff even in the finite case, which I hope to share soon. This is based on performance within TensorKit's functions which frequently access this storage.Benchmarks: I checked decoding/
getindexand encoding/findindex, as well as 4 functions within TensorKit which access the sector values in some way:dim,fuse,sectorsandblocksectorson product spaces (not cached!). I ran this for 2 finite product sector types under the tabulation limit, an infinite product sector type, and a finite one above the limit (just the decoding/encoding).Benchmark results
Current main
Changes
Speedups
Standard errors (old / new)
Case D: Z8^⊠6 (n=262144), after execution
Important observations:
build_tablenow enumerates the grid directly instead of this recursive decoder, which is O(n logn) due to sorting.Vect[I]hasNTuplestorage. So this regression is only within TensorKitSectors. The last 4 columns here should really be ignored, as they don't pass the Manhattan code anyway.I want to stress again this tabulation cutoff being arbitrary, and it really just mattering within TensorKitSectors. My preliminary benchmarks on
NTuplevsSectorDictstorage hint at the former being efficient from the order of ~16-32 sectors, which is way below 2^20. So really, if I end up drawing as conclusion that the cutoff should be around 16-32, then this PR is practically useful for people studying product sector types ≤ 16 sectors 🤣 Thanks for listening to my TED talk.