Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve manybodyoperator performance once more #128

Merged
merged 12 commits into from
Sep 27, 2023

Conversation

aryavorskiy
Copy link
Contributor

@aryavorskiy aryavorskiy commented Aug 3, 2023

  • state_transition! instead of coefficient allows to loop over occupations only once. The state index can be found by searching in the occupations list - but it is still way faster.
  • The occupation vectors are usually sorted lexicographically (and we can sort them if not). We can make use of it to make searching in the occupations list more performant.
    These two tricks yield yet another ~300x performance boost :)

The OccupationsIterator interface can make it even faster, because for fermion states one can avoid storing all occupation vectors by calculating the state index using a formula - but I'd better leave it for another pull request.

Before (36 modes, 3 fermions):

BenchmarkTools.Trial: 2 samples with 1 evaluation.
 Range (min … max):  3.925 s …   3.952 s  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     3.939 s              ┊ GC (median):    0.00%
 Time  (mean ± σ):   3.939 s ± 19.174 ms  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █                                                       █  
  █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ ▁
  3.93 s         Histogram: frequency by time        3.95 s <

 Memory estimate: 2.63 MiB, allocs estimate: 69.

After:

BenchmarkTools.Trial: 341 samples with 1 evaluation.
 Range (min … max):  12.618 ms … 48.976 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     14.262 ms              ┊ GC (median):    0.00%
 Time  (mean ± σ):   14.672 ms ±  2.320 ms  ┊ GC (mean ± σ):  1.43% ± 4.52%

       ▁    █ ▄▁▃ ▂▁
  ▅▂▄▅▇█▇█▇█████████▇▆▅▄▄▆▃▃▂▃▃▂▄▃▁▃▃▄▂▃▃▃▃▂▂▂▁▁▂▂▂▂▁▁▁▁▁▂▁▃▂ ▃
  12.6 ms         Histogram: frequency by time        19.7 ms <

 Memory estimate: 2.98 MiB, allocs estimate: 14377.

UPD: Important detail is that the default state ordering in manybody bases is now slightly changed if Nparticles is a vector. Hope that does not break anything.

@codecov
Copy link

codecov bot commented Aug 3, 2023

Codecov Report

Merging #128 (ac60054) into master (06809f7) will decrease coverage by 0.18%.
The diff coverage is 97.51%.

@@            Coverage Diff             @@
##           master     #128      +/-   ##
==========================================
- Coverage   93.55%   93.37%   -0.18%     
==========================================
  Files          24       24              
  Lines        3055     3018      -37     
==========================================
- Hits         2858     2818      -40     
- Misses        197      200       +3     
Files Coverage Δ
src/manybody.jl 98.40% <97.51%> (-1.25%) ⬇️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@Krastanov
Copy link
Collaborator

This looks great, thank you as always!

I am not too worried about the change of ordering in the returned vector. However, I think this has at least one change that probably should be considered breaking. Previously the return type for many of these functions was Vector, but now it is Occupations, and Occupations does not permit operations that users might already be relying on.

master:

julia> fermionstates(2,2)
1-element Vector{Vector{Int64}}:
 [1, 1]

julia> fermionstates(2,2)[1]
2-element Vector{Int64}:
 1
 1

this pr:

julia> fermionstates(2,2)
QuantumOpticsBase.Occupations{Vector{Int64}}([[1, 1]])

julia> fermionstates(2,2)[1]
ERROR: MethodError: no method matching getindex(::QuantumOpticsBase.Occupations{Vector{Int64}}, ::Int64)
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

I think we need to support a bit more of the Vector API before being comfortable merging this.

I also pushed an empty commit to this branch to rerun the CI. Feel free to overwrite it.

@aryavorskiy
Copy link
Contributor Author

Done. The best way to support the vector API is implementing the AbstractVector interface :)
I removed the OccupationsIterator interface completely - one can extend this functionality by subtyping AbstractVector directly.
The ManyBodyBasis also accepts any type of containers now.

@Krastanov
Copy link
Collaborator

I do not think that Occupations{T} <: AbstractVector{Vector{T}} actually helps here. Subtyping in julia does not guarantee that any particular interface is being implemented or followed. I think this is an object-oriented idiom that does not really apply to Julia. Unless you are using these for something, may we just do struct Occupations{T} without any supertypes? Actually, if ManyBodyBasis can take any container, what is the reason to create Occupations?

Naturally, I might be wrong about these suggestions of mine, thus please run me through your reasoning to make sure I am not missing something.

Implementing formal interfaces in Julia is itself a very interesting topic with many threads on Discourse and experiments in Github repos, but no proper solution yet.

@aryavorskiy
Copy link
Contributor Author

I'm sorry I disappeared for a while :)
Implementing the AbstractVector interface may be useful for non-trivial use cases like applying map or find to an Occupations object.
As for the Occupations structure itself, its main and only feature is that it wraps a sorted array which makes searching much faster. Is's probably better to call it in some other way (maybe SortedContainer?), but I think the structure itself should be kept as it is now.
I can attach some benchmarks to prove there is a significant difference in performance

@Krastanov
Copy link
Collaborator

Krastanov commented Sep 25, 2023

No worries!

I agree that implementing the AbstractVector interface would be useful, but subclassing AbstractVector does not contribute to that goal, unless there are some specific methods that are already defined for AbstractVector and that rely on methods defined for Occupations.

Are you saying that your definitions of size and getindex are enough for "deriving" map, find, length, collect, etc, thanks to subtyping AbstractVector? If that is the case, I would agree with the subtyping (but I would avoid calling it an interface as I understand "interface" to have a different more formal meaning).

If these indeed work now, could you add a few "interface" unit tests? Checking that getting an index, getting a length, collecting, etc all work? collect is probably the main one as it would be the way to fix broken user code (if it turns out that we actually broke something)

@aryavorskiy
Copy link
Contributor Author

aryavorskiy commented Sep 25, 2023

Sure, I'll rename Occupations to SortedVector and add some unit tests. The old name feels kinda bad because there is no functionality designed specially for occupation vectors.

Speaking of interfaces, I used this manual page which says that redefining size and getindex along with setting IndexStyle to Linear() implements the default read-only vector semantics.

@Krastanov
Copy link
Collaborator

Speaking of interfaces, I used [https://docs.julialang.org/en/v1/manual/interfaces/](this manual page) which says that redefining size and getindex along with setting IndexStyle to Linear() implements the default read-only vector semantics.

Oh, I understand now! I am all on board with this. I do not envision other points to discuss and would be eager to merge this after the addition of a couple of interface tests. Thank you so much for this contribution!!!

There are a couple of "correctness of interface implementation testing" packages in the ecosystem. I will add an issue to track the possible use of such a package in the future (not specifically for this contribution, rather in general). It can be a good google summer of code onboarding tasks too.

@aryavorskiy
Copy link
Contributor Author

Done.
By the way, this PR also seems to cover the issue #147 - I added a test to ensure.

"""
Calculate the matrix element <{m}|at_1...at_n a_1...a_n|{n}>.
"""
Base.@propagate_inbounds function coefficient(occ_m, occ_n, at_indices, a_indices)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given #147 , it seems some folks use this function even though it is internal. Do you mind keeping a version of it (a wrapper around your implementation) so that we do not break user code? I guess Hyrum's law strikes again https://www.hyrumslaw.com/

We should probably start keeping a changelog to make it easier for folks to notice when stuff like this happens.

Also, please go ahead and bump the version number to 0.4.18 - I will make a release immediately after merging this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also using @deprecate to warn people they should not be using this (they should never have started, but too late for that)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me backtrack on this, I misunderstood the report in #147 . No need to add anything or many any changes. I will reread and merge this soon.

@Krastanov Krastanov merged commit d2e4c8e into qojulia:master Sep 27, 2023
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants