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

Switch the consensus algorithms interface to using function objects. #13272

Merged
merged 1 commit into from Jan 31, 2022

Conversation

bangerth
Copy link
Member

This is a proposal to fix #13208. The idea is to switch the CA run() function to function objects instead of the Process object. I've managed to keep the existing functions around, and the patch currently does not convert any of the existing CA users. In other words, things are backward compatible. I will change the users to the new interface in a later patch or commit.

Here is what I was setting out for:

  • Ultimately, I would like to address Specialize consensus algorithms for the case where no answer is required #13084, where we need to figure out whether an answer is actually required (or would just be empty) and if not, would use a more efficient algorithm. But for this, I need an interface where one can indicate this, which is most easily done by calling a variant of the run() function that simply doesn't take answer_request() and process_answer() (renamed from read_answer()) functions.
  • The interface essentially makes the AnonymousProcess class unnecessary: Instead of storing the lambda functions in AnonymousProcess, one can just pass them to run() directly. This is what I would have liked to do in FETools::extrapolate(), for example.
  • Retain the possibility of using the Process class -- I keep a Interface::run() function that simply unpacks the Process object and calls the one that takes function objects.
  • While there, I also changed the type of the function objects users have to provide. Previously, these were all of the form
  void create_request(const unsigned int other_rank, std::vector<T1> &  send_buffer)

and put their output into the last function argument. But in all cases, these output buffers are not reused, and so there is no good reason to not just write the function as

   std::vector<T1> create_request(const unsigned int other_rank)

where the object is simply returned. In times of move constructors, this should not have any adverse effect, but is more "functional" and easier to read. In some cases, it also avoids having to use named variables.

Overall, this patch should have no actual functional difference, and it should be backward compatible.

Thoughts about the approach?

/rebuild

Comment on lines -771 to +901
consensus_algo.reset(new NBX<T1, T2>(process, comm));
consensus_algo.reset(new NBX<T1, T2>());
else
# endif
#endif
if (n_procs > 1)
consensus_algo.reset(new PEX<T1, T2>(process, comm));
consensus_algo.reset(new PEX<T1, T2>());
else
consensus_algo.reset(new Serial<T1, T2>(process, comm));
}


consensus_algo.reset(new Serial<T1, T2>());
Copy link
Member Author

Choose a reason for hiding this comment

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

In a side-by-side, you would see that this resetting of the member variable happened in the constructor. I had to move this into the run() function because we now have two constructors, only one of which knows how to choose the algorithm. That also means that the consensus_algorithm variable is actually only used in this one run() function any more. I could move it from a member variable to a local variable. I'll do so in a follow-up patch, but thought I'd try to keep things minimal for this PR.

@bangerth bangerth force-pushed the ca-interface branch 3 times, most recently from 9ea0865 to b3b7487 Compare January 20, 2022 22:05
Comment on lines +839 to +833
Assert((Utilities::MPI::job_supports_mpi() == false) ||
(Utilities::MPI::n_mpi_processes(comm) == 1),
ExcMessage("You shouldn't use the 'Serial' class on "
"communicators that have more than one process "
"associated with it."));
Copy link
Member Author

Choose a reason for hiding this comment

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

This is strictly speaking an unrelated addition, but I thought it was a good idea while I was already working on this function.

Copy link
Member

@peterrum peterrum left a comment

Choose a reason for hiding this comment

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

Looks overall good.

But to be honest, I am not that fond of that additional parameters need to be passed to the internal functions and it does not help the reading (the NBX algorithms used to be 7 locs). The logical next step would be to move all internal variables into the run function and at that time one could make the function static and probably one would not even need a class but only a free function. The helper functions would have even longer argument lists...

@bangerth
Copy link
Member Author

Yes, it is not crazy to suggest that a free function would have been the way to go.

To me as a software designer, here is the difference in variables:

  • Member variables describe the state of the object.
  • Function arguments describe the inputs of the operation to be performed by a function.

This is the separation I have chosen in this patch: I pass the inputs around as function arguments, and describe the state of the object via member variables. I think I did that consistently.

Now, oftentimes, member variables would describe and keep the state of the object between one function call and the next. Here, this is not the case: At the end of the run() function, all state becomes irrelevant and it is created from scratch during the next call to run(). This might suggest that we implement (the public interface of) these algorithms not as a member function, but as a free function ConsensusAlgorithms::nbx(...) (and similarly for PEX), which would then simply create an object of a class declared in some anonymous namespace that keeps the state of the algorithm; when CA::nbx() terminates, it also destroys the object -- in other words, the object only exists only during the run time of run(...) and keeps the state of the object between the different phases of the algorithm. This is probably how I would have implemented the algorithm if I had to start from scratch, but it seemed like a step too far given the existing design. I'm willing to be convinced otherwise.

(By way of already arguing the opposite direction: One could of course say that the operations that are encoded in the Process object are also part of the state of object, and so it makes sense to store them in a member variable. This would, for example, make sense if one were to pass a Process object to the constructor, which would then do some setting up of communication data structures, and then repeated run() calls would use all of this. If so, storing the Process object as a member would make great sense. But this is neither what the implementation of the algorithms currently do, nor how they are used in practice: As far as I can see, there is no place in the library where we actually store a ConsensusAlgorithms object; to the best of my knowledge, they are always created in one place, then one calls run(), and then the object goes out of scope again. In other words, empirically these objects are treated like a free function -- in the sense that there is no conceptual difference between { Class c; c.run(); } or even Class().run() and function_name() -- , and in the places where the CA algorithms are currently use all seem to treat the Process objects as a convenient way to package up function arguments, rather than as setting the initial state of an object that will then be re-used many times over.)

All this to say that I don't think the current design of these classes is bad; I just wanted to provide a software-design-theoretic explanation for why the patch is written this way :-)

@bangerth
Copy link
Member Author

Maybe to bring this into one sentence:

Function arguments describe what to do on the member variables of a class.

Copy link
Member

@kronbichler kronbichler left a comment

Choose a reason for hiding this comment

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

I think this is a reasonable step. I am not particularly happy with the increasing number of function arguments, given that the state is not passed along between the member functions. Also, I generally prefer functions with a named signature (like member functions rather than lambdas) as they are much easier to track down when you are in some lower-level tool as I keep using them, but it seems a problem not shared by many others. On the other hand, I feel that being able to eventually call ConsensusAlgorithms::nbx(...) would be great.

@bangerth
Copy link
Member Author

I commit to making ConsensusAlgorithms::nbx() available as a follow-up.

@kronbichler
Copy link
Member

@peterrum any final comments here? I think we should go on with this topic now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ConsensusAlgorithms interface
3 participants