Skip to content

Conversation

@Technici4n
Copy link
Collaborator

@Technici4n Technici4n commented Apr 30, 2025

Still super super WIP.

The problem lies with how the P matrices are stored in the nonlocal term. Each column of the P matrix corresponds to a projector (and each psp usually has multiple projectors). Multiple atoms with the same psp have the same projectors except for a structure factor $e^{-(G+k)\cdot r_{\text{atom}}}$.

The basic idea in this PR is to store the projectors without the phase factor, and apply the structure factor on-the-fly. The cleanest way to do it that I could find is to use a custom matrix-like type for P such that from the outside P still looks like a dense matrix.

Not sure yet what the best way to apply the structure factor on-the-fly would be. Either we use a temporary vector to hold ψ[:,iband] .* structure_factor or we write a loop but then the question is whether it will work on the GPU.

Partially fixes #1032.

TODO:

  • Actually fix the issue and make the code work.
  • Check that the CPU performance and memory allocations are still fine.
  • Check that the memory usage went down.
  • Check that the GPU performance is still fine.

@Technici4n
Copy link
Collaborator Author

Here is an example showing the problem:

If we have oxygen from PseudoDojo there are 13 projectors. Now assume we perform a computation with roughly $10^4$ plane waves per $k$-point, and with $10^3$ $k$-points.

Each P matrix would have $13 \cdot 10^4$ entries. With $10^3$ such matrices (one per $k$-point) and considering that each entry is a complex number which requires 16 bytes, this gives a total memory usage of roughly 2 gigabytes.

If we have the same setting but 8 oxygen atoms the memory usage goes to 16 gigabytes. With this PR, the goal is that it should remain around 2 gigabytes.

@antoine-levitt
Copy link
Member

So this is a performance memory tradeoff. I did it this way because then you use full blas3 operations. I remember something like this gave a huge boost on abinit, but possibly it was blas3ifying the operation on all the bands at once that gave the performance boost (as opposed to band by band as it was done before). So I'm not opposed to switching but keep in mind the performance implications and benchmark it.

@antoine-levitt
Copy link
Member

@Technici4n
Copy link
Collaborator Author

Nice reference, maybe that lets me find how abinit does it. We also need to be careful not to destroy GPU performance. 😓

@antoine-levitt
Copy link
Member

If you want to look, it's in the opernla routine (I worked on that code a long time ago, it still haunts my dreams)

Copy link
Member

@mfherbst mfherbst left a comment

Choose a reason for hiding this comment

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

Nice. Yeah the BLAS 2 versus BLAS 3 tradeoff is something to be understood a little better by benchmarks before we commit on one way to do this.

for proj in eachcol(p.projectors)
# TODO: what allocates here? and does it use BLAS?
ψ_scratch .= p.structure_factors .* proj
C[iproj, :] .= dropdims(ψ_scratch' * ψk; dims=1)
Copy link
Member

Choose a reason for hiding this comment

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

The dropdims is weird. Is this not just a dot product ?

ops::Vector{NonlocalOperator}
end

struct AtomProjectors{T <: Real,
Copy link
Member

Choose a reason for hiding this comment

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

Let's see first to what extend things show up in benchmarking, but we should perhaps rethink these structs (where to put scratch arrays, where to store them etc., how they can be reused across various datastructures and tasks etc.)

for iband in size(B, 2)
C[:, iband] .+= ψ_scratch .** B[iproj, iband])
for iband in axes(B, 2)
@views C[:, iband] .+= ψ_scratch .** B[iproj, iband])
Copy link
Member

Choose a reason for hiding this comment

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

5-argument mul! ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's not obvious but this is actually an axpy-type call. (level 1 :( )

test/PspUpf.jl Outdated
end
end

@testitem "Test nonlocal term operations" tags=[:psp] setup=[mPspUpf] begin
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure about this test. The goal was just for me to be able to try my changes with <5s waiting time.

D = build_projection_coefficients(basis, psp_groups)
P = build_projection_vectors(basis, kpt, psp_groups, positions)
P_minus_q = build_projection_vectors(basis, kpt_minus_q, psp_groups, positions)
# TODO: probably needs an extra parenthesis to first compute P'ψ
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So... I noticed that Julia has custom * overloads for matrix multiplication with more than 2 operands to select the chain of operations that will minimize the total cost. Presumably it will almost always compute P_minus_q' * ψk first. But if it doesn't we are in trouble, so this should probably be changed to (P_minus_q' * ψk).

ST <: AbstractVector{Complex{T}},
PT <: AtomProjectors,
} <: AbstractMatrix{Complex{T}}
# TODO: this is a real problem wrt. thread-safety, no?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How bad is this? DftHamiltonianBlock should handle it fine, but GenericHamiltonianBlock seems to be parallelizing over bands which will cause problems!

@Technici4n Technici4n marked this pull request as ready for review July 10, 2025 09:34
@Technici4n
Copy link
Collaborator Author

Cleaned up this PR quite a bit. Benchmarks are still missing of course. 😄

Copy link
Member

@mfherbst mfherbst left a comment

Choose a reason for hiding this comment

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

I've only done a quick review for now.

We should look a bit more at performance (also on GPU) and if there are a few small things here and there we can do to make code a little cleaner.

Comment on lines +207 to +216
# Add a level of indirection here to avoid ambiguity with the mul! method provided by Julia.
LinearAlgebra.mul!(C::AbstractVector, A::Adjoint{<:Any, <:NonlocalProjectors},
ψk::AbstractVector) = _mul!(C, A, ψk)
LinearAlgebra.mul!(C::AbstractMatrix, A::Adjoint{<:Any, <:NonlocalProjectors},
ψk::AbstractMatrix) = _mul!(C, A, ψk)

LinearAlgebra.mul!(C::AbstractVector, A::NonlocalProjectors, B::AbstractVector,
α::Number, β::Number) = _mul!(C, A, B, α, β)
LinearAlgebra.mul!(C::AbstractMatrix, A::NonlocalProjectors, B::AbstractMatrix,
α::Number, β::Number) = _mul!(C, A, B, α, β)
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed ? Surprises me a bit.

Comment on lines +225 to +226
for at in A.parent.atoms
for proj in eachcol(at.projectors)
Copy link
Member

Choose a reason for hiding this comment

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

join these loops to avoid too deep nesting.

else
@view(B[iproj:iproj+nproj-1, :])
end
mul!(C, Pwork, Bwork, α, 1)
Copy link
Member

Choose a reason for hiding this comment

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

why not put β here ?

Comment on lines +256 to +260
Bwork = if BT <: AbstractVector
@view(B[iproj:iproj+nproj-1])
else
@view(B[iproj:iproj+nproj-1, :])
end
Copy link
Member

Choose a reason for hiding this comment

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

this looks weird.

@Technici4n
Copy link
Collaborator Author

Yes, I am not sure of the current implementation, it's quite complex.

We can additionally save quite a bit of RAM if we allow ourselves to recompute the spherical harmonics on-the-fly. In that case we will only need to store one form factor per angular moment component l instead of 2l+1. For PD Silicon this would bring the number of projector form factors that need to be stored from 18 down to 6.

@mfherbst
Copy link
Member

We can additionally save quite a bit of RAM if we allow ourselves to recompute the spherical harmonics on-the-fly. In that case we will only need to store one form factor per angular moment component l instead of 2l+1. For PD Silicon this would bring the number of projector form factors that need to be stored from 18 down to 6.

I think what we are getting to here is the usual issue with balancing memory and compute. I think this can be reasonable, but maybe we also want some way to have both options ? Or is this too much of a maintainance burden at the moment ?

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.

Stress computation can use a lot of RAM

3 participants