MRC-CSO-SPHSU / LoneParentsModel.jl

An initial implementation of an ABM for social and child care
0 stars 4 forks source link

Simulation speed of MA-dependent Main #79

Closed AtiyahElsheikh closed 1 year ago

AtiyahElsheikh commented 2 years ago

The simulation speed of MA-based main including doDeaths & doBirths is slow ca. 10 secs (40 secs when assumptions are being checked) & memory allocation is more than 1.5 GB (5 GB). So I will attempt (some of) the following

AtiyahElsheikh commented 2 years ago

I have tried to figure out the impact of deleting agents via the doDeath! function. After ignoring dead removal, it turned out that the runtime performance, particularly the amount of memory allocation did not really improve.

After over-viewing performance tips from juliadoc, have some initial guesses what need to be improved, type stability etc. It seems, I should have read this before starting the coding.

AtiyahElsheikh commented 2 years ago

I have created a new release V0.5. Regardless it has been a lot of new stuffs now, it is good to have a repository tag for future performance comparisons hopefully if simulation runtime / memory allocation get improved.

AtiyahElsheikh commented 2 years ago

First experiments, the following steps have slightly improved the performance:

    • Re-declaring age within BasicInfo from Rational to Rational{Int}
    • Within death.jl, static declaration of the parameters argument for the most-inner function, i.e. deathProbability
mhinsch commented 2 years ago

What do you mean by 2.? Do you mean adding type declarations to the parameter "parameters" of deathProbability?

mhinsch commented 2 years ago

Ok, I can reproduce 1., but not 2. Adding explicit type information in deathProbability has no effect for me (as it shouldn't).

mhinsch commented 2 years ago

I tried your Performance branch and found the same - type information in deathProbability has no effect. Edit: I'm on 1.7, though, I'll try the same with 1.8. Edit2: Nope, same as with 1.7.

AtiyahElsheikh commented 2 years ago

The static declaration of parameters in the innermost internal function deathProbabilityto parameters::UKPopulationPars parameters have reduced the number of allocations from ~ 31M to 24M for MainLPM.jl and from 4.5 M to 3.5 M for Main.jl.

This was before declaration of age as a concrete type.

Yes indeed the static declaration of other arguments were unnecessary and I only forgot to remove them. Apparently, the compiler can deduce such information alone, but benefit from the static declaration of the parameters argument only in the innermost function.

Yes, the performance with 1.8 did not improve for me as well.

mhinsch commented 2 years ago

Hmm, I don't get any difference for either 1.7. or 1.8 between generic and typed parameters. If there were, that would probably even count as a bug in Julia as the type of the parameters argument is known to the compiler at the time of call.

mhinsch commented 2 years ago

BTW, I only get 2M and 1.54M allocations (with julia 1.8 and 1.7, respectively) for both versions. That's when running Main.jl from your Performance branch (shell or repl doesn't make a difference).

AtiyahElsheikh commented 2 years ago

For me both version 1.7 and 1.8 were identical or hopefully it was a clean upgrade.

My idea after what I read, is to isolate the most-intensive computations into an internal function where every thing is statically declared and see what happens. Parameters did influence. I can check that again with my private labtop.

AtiyahElsheikh commented 2 years ago

I have re-checked again.

Yes the static declaration of parameters did not influence. It was the static declaration of baseRate argument in the function deathProbability, i.e.

function deathProbability(baseRate::Float64,person,parameters)

When age is not a concrete type,

AtiyahElsheikh commented 2 years ago

If age is defined as a concrete type, then

function deathProbability(baseRate::Float64,person,parameters)

So it is something else that propagates till this function.

AtiyahElsheikh commented 2 years ago

For records, Main.jl requires ~ 503 K allocations / 15.485 Mb (when assumption checking is disabled)

AtiyahElsheikh commented 2 years ago

Removal of dead people does not significantly influence the current performance. So most of the memory consumption is somewhere else.

Currently it is enabled within the Performance branch as follows:

lpmDemographySim = MABMSimulation(ukDemography,simProperties, 
                                  # example=LPMUKDemography())
                                  example=LPMUKDemographyOpt()) 
AtiyahElsheikh commented 2 years ago

I am enabling the usage of iterators within MALPM.Simulate as follows:

alivePeople(population::ABM{Person},example::LPMUKDemographyOpt) = 
               # Iterators.filter(person->alive(person),allagents(population)) # remove comment to enable iterators 
                [ person for person in allagents(population)  if alive(person) ]

It did not change that much, though I guess I need to reduce the instabilities before jumping to conclusions. Is that sentence above enough to say that Iterators are used?

mhinsch commented 2 years ago

A few things I found in MultiAgents.jl that might slow down the MALPM version:

AtiyahElsheikh commented 2 years ago

Thanks .. I started today with MA.jl. I will try to simplify more in this direction, eliminate abstract-typed fields and separate model parameters, propoerties etc. from simulation parameters, git rid / reduce usage of dictionaries{Symbol,Any} in general.

What I was wondering as well is Function-Type arguments. They are not concrete types. But I still guess that would not matter.

mhinsch commented 2 years ago

I don't know how well "function pointers" can be optimised. When I did the latest changes in my transitions branch I tested the version with static code against the one where the predicate and the transition are parameters of the applyTransition function and it didn't seem to make a difference. IIRC the julia compiler is quite good at doing constant propagation across function calls and maybe functions themselves are constants in this context.

mhinsch commented 2 years ago

Concerning the get/setproperty in MA - you can probably speed that up considerably by using a generated function to unroll the 'name in fieldnames' loop. If you generate a long if, ifelse,... expression the compiler will be able to translate that into a jump table which should be a lot faster.

mhinsch commented 2 years ago

I looked it up - I was wrong on point 3. Abstract types as function parameters are totally fine and don't prevent any optimisation (as long as the type is known at the call site of course). It's so easy to fall back into C++ thinking...

AtiyahElsheikh commented 2 years ago

From https://docs.julialang.org/en/v1/manual/performance-tips/

As a heuristic, Julia avoids automatically specializing on argument type parameters in three specific cases: Type, Function, and Vararg. Julia will always specialize when the argument is used within the method, but not if the argument is just passed through to another function.

May be a hint why it might be not perfect sometimes with function arguments

I was also thinking that it is better to replace

foo(n::Number) with

foo(f::Float64)
foo(i::Int)

Also wondering about functions that return Union{Something,Nothing}

AtiyahElsheikh commented 2 years ago

and that it is kind of strategy to blindly examine memory with/without the lowest-level functions having static arguments

AtiyahElsheikh commented 2 years ago

and that it is kind of strategy to blindly examine memory with/without the lowest-level functions having static arguments

So after getting rid of abstract-type field of the ABM type, this did not improve any more.

AtiyahElsheikh commented 2 years ago

Making ABM a fixed-size concrete type has lead performance improvement. More to be progressed in this direction: issue no 12. MA.jl towards version 0.3

AtiyahElsheikh commented 1 year ago

resolved via V0.3 MultiAgents.jl