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

Rework extract functions. #51

Merged
merged 50 commits into from
Jul 1, 2024
Merged

Conversation

Joroks
Copy link
Collaborator

@Joroks Joroks commented Jun 27, 2024

I've reworked my previous PR with a more focused scope. It's still quite large so I've tried my best to make it as polished as possible to make reviewing it easier / reduce the amount of revisions needed for it to be merged.

I do feel a bit sorry for comming out of nowhere and suddendly putting this huge amount of extra workload on your table 😅. I'm really gratefull for the work you've put into this thus far! Please take as much time as you need to review this.

@Joroks
Copy link
Collaborator Author

Joroks commented Jun 28, 2024

I would almost consider not exporting them.

And then name then NONE instead of LAMMPS_NONE and STYLE_GLOBAL instead of LMP_STYLE_GLOBAL.
Then it's consitent in user code as LAMMPS.NONE and LAMMPS.STYLE_GLOBAL

@vchuravy

I've quickly implemented it in the PR to get a feel for how it would look like and I'm not quite sure if I like this change.

It works well enough for _LMP_DATATYPE but that's about it. The constants for the other enums get really long to type out. I think the better solution is to do this:

const LAMMPS_NONE = _LMP_DATATYPE{API.LAMMPS_NONE}()
...
const TYPE_SCALAR = _LMP_TYPE{API.LMP_TYPE_SCALAR}()
...
const VAR_EQUAL = _LMP_VARIABLE{API.LMP_VAR_EQUAL}()
...
const STYLE_GLOBAL = API.LMP_STYLE_GLOBAL

and export all of them. I originaly just kept the naming from API.jl for _LMP_STYLE_CONST because I wasn't wrapping them in TypeEnum. But removing the LMP_ at the begining would give us a nice and consistent naming scheme.

@vchuravy
Copy link
Member

the better solution is to do this

I think that's fine. Better than exporting the STRING

@Joroks Joroks requested a review from vchuravy June 30, 2024 11:50
src/LAMMPS.jl Outdated Show resolved Hide resolved
src/LAMMPS.jl Outdated Show resolved Hide resolved
Copy link
Member

@vchuravy vchuravy left a comment

Choose a reason for hiding this comment

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

Besides the two small nits, this looks great to me.

I would also like to invite you as a collaborator to this repository. The expectation would still be to use PRs and to wait/ask for review before merging large changes, but by now you rewritten most of the code here :)

@Joroks
Copy link
Collaborator Author

Joroks commented Jun 30, 2024

I would also like to invite you as a collaborator to this repository. The expectation would still be to use PRs and to wait/ask for review before merging large changes, but by now you rewritten most of the code here :)

I'll gladly accept this invitation!

src/LAMMPS.jl Outdated Show resolved Hide resolved
end
dtype = API._LMP_DATATYPE_CONST(dtype)
type = dtype2type(dtype)
function _extract(ptr::Ptr{<:Real}, shape::Integer; copy=false, own=false)
Copy link
Member

Choose a reason for hiding this comment

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

copy should imply own? We can almost never use own=true since we need to use lammps_free for the deallocation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As far as I can tell, we don't need to use lammps_free for deallocation. It just calls free internally without doing anything else. Thats the same thing that would happen If we take ownership of the memory if I'm not mistaken. I've taken this directly from the lammps source code:

void lammps_free(void *ptr)
{
  free(ptr);
}

The own keyword is used for the extract_variable method, specifically when it's a Atom style variable:

  if lmp_variable == VAR_ATOM
      ndata = extract_setting(lmp, "nlocal")
      ptr = _reinterpret(LAMMPS_DOUBLE, void_ptr)
      # lammps expects us to take ownership of the data
      return _extract(ptr, ndata; own=true)
  end

because the documentation states:

For atom-style variables, the return value is a pointer to an allocated block of storage of double of the length
atom->nlocal. Since the data returned are a copy, the location will persist, but its content will not be updated in case the variable is re-evaluated. To avoid a memory leak, this pointer needs to be freed after use in the calling program.

That means that the value is already a copy and copying it again would be superfluous.

I've also (crudely) verified, that this does indeed not cause a memory leak by running the following script:

lmp = LMP()

command(lmp, """
         atom_modify map yes
         region cell block 0 3 0 3 0 3
         create_box 1 cell
         lattice sc 1
         create_atoms 1 region cell
         mass 1 1
        
         variable var atom id
    """)

for _ in 1:10^7
    extract_variable(lmp, "var", VAR_ATOM)
end

If I set own to false, my memory usage rises by about 2GB. If I set own to true the memory consumption stays stable.
So I'd say its safe to let the GC handle freeing the memory if we're supposed to take ownership.

I have also verified that the data stays allocated even if we close the lammps instance. I've even written this as a test:

lmp = LMP(["-screen", "none"])
command(lmp, """
    atom_modify map yes
    region cell block 0 3 0 3 0 3
    create_box 1 cell
    lattice sc 1
    create_atoms 1 region cell
    mass 1 1

    variable var atom id
""")

var = extract_variable(lmp, "var", VAR_ATOM)
var_copy = copy(var)
LAMMPS.close!(lmp)

@test var == var_copy

In summary, we use copy=true, if the ownership stays with LAMMPS but we want a copy and own=true if the ownership is handed to us, which is usually because LAMMPS has made a copy for us

Copy link
Member

Choose a reason for hiding this comment

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

t just calls free internally without doing anything else. Thats the same thing that would happen If we take ownership of the memory if I'm not mistaken.

This is sadly not true. In shared libraries the malloc and free maybe be resolved to different implementations. So you can never rely that the functions are the same in two shared libraries.

if we're supposed to take ownership.

So that's actually a good question. When are we supposed to take ownership?

As far as I understood extract we should never take ownershi, but if the underlying API is allocating memory for us, then we will need to do a copy + lammps_free. Which sucks.

Copy link
Member

Choose a reason for hiding this comment

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

Okay according to the docs https://docs.lammps.org/Library_objects.html
atom style requires us to call lammps_free.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've thought about registering lammps_free as a finalizer on our array. I think that could work?

Copy link
Collaborator Author

@Joroks Joroks Jun 30, 2024

Choose a reason for hiding this comment

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

I've done:

finalizer(result) do r
    API.lammps_free(r)
    @async println("finializer has run!")
end

and then the following test script:

lmp = LMP()

command(lmp, """
   atom_modify map yes
   region cell block 0 3 0 3 0 3
   create_box 1 cell
   lattice sc 1
   create_atoms 1 region cell
   mass 1 1

   variable var atom id
""")

var1 = extract_variable(lmp, "var", VAR_ATOM)
var2 = reshape(var1, 1, 27)

var1 = nothing
GC.gc() # finallizer is not run

var2 = nothing
GC.gc() # finallizer is run

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the finalizer is the best solution. Should we merge this PR?
Then as soon as 1.11 gets released, we should revisit this. It might become necessary to force 1.11 or higher if we need to register the finalizer on the Memory?

Copy link
Member

@vchuravy vchuravy Jul 1, 2024

Choose a reason for hiding this comment

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

So on 1.11

julia> var2 = reshape(var1, 1, 27)
1×27 Matrix{Float64}:
 1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0  10.0  11.0  12.0  13.0  14.0  …  17.0  18.0  19.0  20.0  21.0  22.0  23.0  24.0  25.0  26.0  27.0

julia> var1 = nothing

julia> GC.gc() # finallizer is run
Finalizer run

julia> var2
1×27 Matrix{Float64}:
 1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0  10.0  11.0  12.0  13.0  14.0  …  18.0  19.0  20.0  21.0  22.0  23.0  24.0  25.0  26.0  1.265e-321

Note the data flipping.

Copy link
Member

@vchuravy vchuravy Jul 1, 2024

Choose a reason for hiding this comment

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

I opened an upstream issue, but I don't expect this behavior to material change before 1.11 gets released

JuliaLang/julia#54985

So if we want to use this finalizer version, we would need to use @static if VERSION >= v1.11-dev and attach the finalizer to result.ref.mem, or unsafe_wrap it as a Memory and then create an Array from that, that is probably clearer.

Copy link
Collaborator Author

@Joroks Joroks Jul 1, 2024

Choose a reason for hiding this comment

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

this now puts a lot of stress on the GC (in 1.11)

grafik

after running my test loop (with 10^8 iterations) i have to manually trigger GC twice for the memory to be freed completely.

grafik

this doesn't happen if I attach the finalizer to the array instead of the memory

Co-authored-by: Valentin Churavy <[email protected]>
@Joroks Joroks assigned Joroks and unassigned Joroks Jun 30, 2024
src/LAMMPS.jl Show resolved Hide resolved
@vchuravy vchuravy merged commit c8beaa5 into cesmix-mit:main Jul 1, 2024
14 of 16 checks passed
@vchuravy vchuravy mentioned this pull request Jul 1, 2024
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.

3 participants