Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions math/minuit2/inc/Minuit2/FCNBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ class FCNBase {
return Gradient(parameters);
};

/// Variant of GradientWithPrevResult() that additionally receives the
/// already-known function value at \p parameters (e.g. from the line search
/// that preceded the gradient request), so implementations can avoid
/// re-evaluating the function at the central point.
///
/// \warning Not meant to be overridden! This is a requirement for an
/// internal optimization in RooFit that might go away with any refactoring.
virtual std::vector<double> GradientWithPrevResult(std::vector<double> const &parameters, double *previous_grad,
double *previous_g2, double *previous_gstep,
double /*fValAtParameters*/) const
{
return GradientWithPrevResult(parameters, previous_grad, previous_g2, previous_gstep);
};

/// \warning Not meant to be overridden! This is a requirement for an
/// internal optimization in RooFit that might go away with any refactoring.
virtual GradientParameterSpace gradParameterSpace() const { return GradientParameterSpace::External; };
Expand Down
12 changes: 12 additions & 0 deletions math/minuit2/inc/Minuit2/NumericalDerivator.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ class NumericalDerivator {

void SetupDifferentiate(unsigned int nDim, const FCNBase *function, const double *cx,
std::span<const ROOT::Fit::ParameterSettings> parameters);

/// Pre-seed the cache of the function value at the central point, so that a
/// subsequent SetupDifferentiate() at the same point \p cx (in Minuit-internal
/// coordinates) can skip its function evaluation. Callers that already know
/// the function value at the gradient point (e.g. from the line search that
/// Minuit just completed) can use this to avoid one full function call.
void PreseedFVal(double fval, std::span<const double> cx)
{
fVxFValCache.assign(cx.begin(), cx.end());
fVal = fval;
}

std::vector<DerivatorElement> Differentiate(unsigned int nDim, const FCNBase *function, const double *x,
std::span<const ROOT::Fit::ParameterSettings> parameters,
std::span<const DerivatorElement> previous_gradient);
Expand Down
3 changes: 2 additions & 1 deletion math/minuit2/src/ExternalInternalGradientCalculator.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ ExternalInternalGradientCalculator::operator()(const MinimumParameters &par, con
std::vector<double> previous_g2(functionGradient.G2().Data(), functionGradient.G2().Data() + functionGradient.G2().size());
std::vector<double> previous_gstep(functionGradient.Gstep().Data(), functionGradient.Gstep().Data() + functionGradient.Gstep().size());

std::vector<double> grad = fGradFunc.GradientWithPrevResult(par_vec, previous_grad.data(), previous_g2.data(), previous_gstep.data());
std::vector<double> grad = fGradFunc.GradientWithPrevResult(par_vec, previous_grad.data(), previous_g2.data(),
previous_gstep.data(), par.Fval());
assert(grad.size() == fTransformation.Parameters().size());

MnAlgebraicVector v(par.Vec().size());
Expand Down
12 changes: 12 additions & 0 deletions roofit/multiprocess/inc/RooFit/MultiProcess/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ class Config {
static std::size_t defaultNComponentTasks;
};

struct LikelihoodGradientJob {
// magic value to indicate that the number of tasks will be set automatically
constexpr static std::size_t automaticNParamTasks = 0;

/// Number of tasks to split a gradient calculation into. Each task
/// covers a contiguous block of parameters, so fewer tasks mean less
/// scheduling overhead per gradient, but also less opportunity for load
/// balancing between the workers. The automatic default uses a small
/// multiple of the number of workers.
static std::size_t defaultNParamTasks;
};

struct Queue {
enum class QueueType {FIFO, Priority};
static bool setQueueType(QueueType queueType);
Expand Down
1 change: 1 addition & 0 deletions roofit/multiprocess/src/Config.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ void Config::Queue::suggestTaskOrder(std::size_t job_id, const std::vector<Task>
unsigned int Config::defaultNWorkers_ = std::thread::hardware_concurrency();
std::size_t Config::LikelihoodJob::defaultNEventTasks = Config::LikelihoodJob::automaticNEventTasks;
std::size_t Config::LikelihoodJob::defaultNComponentTasks = Config::LikelihoodJob::automaticNComponentTasks;
std::size_t Config::LikelihoodGradientJob::defaultNParamTasks = Config::LikelihoodGradientJob::automaticNParamTasks;
Config::Queue::QueueType Config::Queue::queueType_ = Config::Queue::QueueType::FIFO;
bool Config::timingAnalysis_ = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ class LikelihoodGradientWrapper {
SharedOffset offset);

virtual void fillGradient(double *grad) = 0;
virtual void
fillGradientWithPrevResult(double *grad, double *previous_grad, double *previous_g2, double *previous_gstep) = 0;
/// \param[in] fValAtX The function value at the current parameter point, if known by the
/// caller (e.g. from the preceding line search); NaN when unknown. Implementations
/// can use it to avoid re-evaluating the function at the central point.
virtual void fillGradientWithPrevResult(double *grad, double *previous_grad, double *previous_g2,
double *previous_gstep, double fValAtX) = 0;

/// Synchronize minimizer settings with calculators in child classes.
virtual void synchronizeWithMinimizer(const ROOT::Math::MinimizerOptions &options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class RooUnbinnedL : public RooAbsL {
mutable ROOT::Math::KahanSum<double> cachedResult_{0.};
std::shared_ptr<RooFit::Evaluator> evaluator_; ///<! For batched evaluation
std::stack<std::vector<double>> _vectorBuffers; // used for preserving resources in batched evaluation
std::vector<double> _unitWeights; ///<! all-ones weights for unweighted data in batched evaluation
};

} // namespace TestStatistics
Expand Down
3 changes: 2 additions & 1 deletion roofit/roofitcore/src/FitHelpers.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,8 @@ std::unique_ptr<RooAbsReal> createNLL(RooAbsPdf &pdf, RooAbsData &data, const Ro
.ConstrainedParameters(cParsSet)
.ExternalConstraints(extConsSet)
.GlobalObservables(glObsSet)
.GlobalObservablesTag(rangeName.c_str());
.GlobalObservablesTag(rangeName.c_str())
.EvalBackend(RooFit::EvalBackend(static_cast<RooFit::EvalBackend::Value>(pc.getInt("EvalBackend"))));

return std::make_unique<RooFit::TestStatistics::RooRealL>("likelihood", "", builder.build());
}
Expand Down
71 changes: 61 additions & 10 deletions roofit/roofitcore/src/TestStatistics/LikelihoodGradientJob.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include "Minuit2/Minuit2Minimizer.h"
#include "Minuit2/MnStrategy.h"

#include <algorithm>
#include <cmath>

namespace RooFit {
namespace TestStatistics {

Expand All @@ -32,13 +35,33 @@ LikelihoodGradientJob::LikelihoodGradientJob(std::shared_ptr<RooAbsL> likelihood
std::size_t N_dim, RooMinimizer *minimizer, SharedOffset offset)
: LikelihoodGradientWrapper(std::move(likelihood), std::move(calculation_is_clean), N_dim, minimizer,
std::move(offset)),
grad_(N_dim),
N_tasks_(N_dim)
grad_(N_dim)
{
// Each task covers multiple parameters to reduce the scheduling overhead
// per gradient: every task costs a dequeue round trip through the queue
// process plus a result message to the master, which for cheap partial
// derivatives can otherwise dominate over the actual calculation. A few
// tasks per worker still leave the queue room for load balancing. Parameters
// are assigned to tasks in strides, so parameters with expensive partial
// derivatives (that often sit next to each other in the parameter order,
// e.g. a block of correlated systematics) spread evenly over the tasks.
N_tasks_ = MultiProcess::Config::LikelihoodGradientJob::defaultNParamTasks;
if (N_tasks_ == MultiProcess::Config::LikelihoodGradientJob::automaticNParamTasks) {
N_tasks_ = 4 * MultiProcess::Config::getDefaultNWorkers();
}
N_tasks_ = std::min(N_tasks_, N_dim);

minuit_internal_x_.reserve(N_dim);
offsets_previous_ = shared_offset_.offsets();
}

/// Number of parameters assigned to task \p task (parameter indices congruent
/// to \p task modulo N_tasks_).
std::size_t LikelihoodGradientJob::taskSize(std::size_t task) const
{
return grad_.size() / N_tasks_ + (task < grad_.size() % N_tasks_ ? 1 : 0);
}

void LikelihoodGradientJob::synchronizeParameterSettingsImpl(
const std::vector<ROOT::Fit::ParameterSettings> &parameter_settings)
{
Expand Down Expand Up @@ -86,23 +109,33 @@ void LikelihoodGradientJob::setErrorLevel(double error_level) const

void LikelihoodGradientJob::evaluate_task(std::size_t task)
{
run_derivator(task);
for (std::size_t ix = task; ix < grad_.size(); ix += N_tasks_) {
run_derivator(ix);
}
}

// SYNCHRONIZATION FROM WORKERS TO MASTER

void LikelihoodGradientJob::send_back_task_result_from_worker(std::size_t task)
{
task_result_t task_result{id_, task, grad_[task]};
zmq::message_t message(sizeof(task_result_t));
task_result_t task_result{id_, task};
zmq::message_t message(sizeof(task_result_t) + taskSize(task) * sizeof(ROOT::Minuit2::DerivatorElement));
memcpy(message.data(), &task_result, sizeof(task_result_t));
auto elements = reinterpret_cast<ROOT::Minuit2::DerivatorElement *>(message.data<char>() + sizeof(task_result_t));
for (std::size_t ix = task; ix < grad_.size(); ix += N_tasks_) {
*elements++ = grad_[ix];
}
get_manager()->messenger().send_from_worker_to_master(std::move(message));
}

bool LikelihoodGradientJob::receive_task_result_on_master(const zmq::message_t &message)
{
auto result = message.data<task_result_t>();
grad_[result->task_id] = result->grad;
auto elements =
reinterpret_cast<ROOT::Minuit2::DerivatorElement const *>(message.data<char>() + sizeof(task_result_t));
for (std::size_t ix = result->task_id; ix < grad_.size(); ix += N_tasks_) {
grad_[ix] = *elements++;
}
--N_tasks_at_workers_;
bool job_completed = (N_tasks_at_workers_ == 0);
return job_completed;
Expand All @@ -122,14 +155,18 @@ void LikelihoodGradientJob::update_workers_state()
++state_id_;

if (shared_offset_.offsets() != offsets_previous_) {
// The function value known by the master corresponds to the previous
// offsets, so it cannot be used to skip the central-point evaluation on
// the workers in this update.
double fValAtX = std::numeric_limits<double>::quiet_NaN();
zmq::message_t offsets_message(shared_offset_.offsets().begin(), shared_offset_.offsets().end());
get_manager()->messenger().publish_from_master_to_workers(
id_, state_id_, isCalculating_, maxFCN, fcnOffset, std::move(gradient_message),
id_, state_id_, isCalculating_, maxFCN, fcnOffset, fValAtX, std::move(gradient_message),
std::move(minuit_internal_x_message), std::move(offsets_message));
offsets_previous_ = shared_offset_.offsets();
} else {
get_manager()->messenger().publish_from_master_to_workers(id_, state_id_, isCalculating_, maxFCN, fcnOffset,
std::move(gradient_message),
fval_at_x_, std::move(gradient_message),
std::move(minuit_internal_x_message));
}
}
Expand Down Expand Up @@ -157,6 +194,9 @@ void LikelihoodGradientJob::update_state()
minimizer_->fcnOffset() = fcnOffset;
assert(more);

auto fValAtX = get_manager()->messenger().receive_from_master_on_worker<double>(&more);
assert(more);

auto gradient_message = get_manager()->messenger().receive_from_master_on_worker<zmq::message_t>(&more);
assert(more);
auto gradient_message_begin = gradient_message.data<ROOT::Minuit2::DerivatorElement>();
Expand Down Expand Up @@ -184,6 +224,14 @@ void LikelihoodGradientJob::update_state()
// Since the gradient parallelization only support Minuit 2, we can do this cast
auto &minim = static_cast<ROOT::Minuit2::Minuit2Minimizer &>(*minimizer_->_minimizer);

// The master already knows the function value at the current point from
// the line search that Minuit just completed; pre-seeding the derivator's
// cache with it makes the SetupDifferentiate call below skip its full
// likelihood evaluation at the central point.
if (!std::isnan(fValAtX)) {
gradf_.PreseedFVal(fValAtX, {minuit_internal_x_.data(), static_cast<std::size_t>(minimizer_->getNPar())});
}

// note: the next call must stay after the (possible) update of the offset, because it
// calls the likelihood function, so the offset must be correct at this point
gradf_.SetupDifferentiate(minimizer_->getNPar(), minim.GetFCN(), minuit_internal_x_.data(),
Expand Down Expand Up @@ -231,6 +279,7 @@ void LikelihoodGradientJob::fillGradient(double *grad)
{
if (get_manager()->process_manager().is_master()) {
if (!calculation_is_clean_->gradient) {
fval_at_x_ = std::numeric_limits<double>::quiet_NaN();
calculate_all();
}

Expand All @@ -242,13 +291,15 @@ void LikelihoodGradientJob::fillGradient(double *grad)
}

void LikelihoodGradientJob::fillGradientWithPrevResult(double *grad, double *previous_grad, double *previous_g2,
double *previous_gstep)
double *previous_gstep, double fValAtX)
{
if (get_manager()->process_manager().is_master()) {
for (std::size_t i_component = 0; i_component < N_tasks_; ++i_component) {
for (std::size_t i_component = 0; i_component < grad_.size(); ++i_component) {
grad_[i_component] = {previous_grad[i_component], previous_g2[i_component], previous_gstep[i_component]};
}

fval_at_x_ = fValAtX;

if (!calculation_is_clean_->gradient) {
if (RooFit::MultiProcess::Config::getTimingAnalysis()) {
RooFit::MultiProcess::ProcessTimer::start_timer("master:gradient");
Expand Down
12 changes: 9 additions & 3 deletions roofit/roofitcore/src/TestStatistics/LikelihoodGradientJob.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "Minuit2/NumericalDerivator.h"
#include "Minuit2/MnMatrix.h"

#include <limits>
#include <vector>

namespace RooFit {
Expand All @@ -32,8 +33,8 @@ class LikelihoodGradientJob : public MultiProcess::Job, public LikelihoodGradien
RooMinimizer *minimizer, SharedOffset offset);

void fillGradient(double *grad) override;
void fillGradientWithPrevResult(double *grad, double *previous_grad, double *previous_g2,
double *previous_gstep) override;
void fillGradientWithPrevResult(double *grad, double *previous_grad, double *previous_g2, double *previous_gstep,
double fValAtX) override;

void update_state() override;

Expand All @@ -59,11 +60,13 @@ class LikelihoodGradientJob : public MultiProcess::Job, public LikelihoodGradien
// Job overrides:
void evaluate_task(std::size_t task) override;

/// Message header for a task result; followed in the same message by the
/// DerivatorElement results for the parameters of that task.
struct task_result_t {
std::size_t job_id;
std::size_t task_id;
ROOT::Minuit2::DerivatorElement grad;
};
std::size_t taskSize(std::size_t task) const;
void send_back_task_result_from_worker(std::size_t task) override;
bool receive_task_result_on_master(const zmq::message_t &message) override;

Expand All @@ -80,6 +83,9 @@ class LikelihoodGradientJob : public MultiProcess::Job, public LikelihoodGradien
std::size_t N_tasks_ = 0;
std::size_t N_tasks_at_workers_ = 0;
std::vector<double> minuit_internal_x_;
/// Function value at minuit_internal_x_ as known by the master (NaN when unknown); broadcast
/// to workers so their NumericalDerivator setup can skip the central-point evaluation.
double fval_at_x_ = std::numeric_limits<double>::quiet_NaN();

mutable bool isCalculating_ = false;

Expand Down
8 changes: 4 additions & 4 deletions roofit/roofitcore/src/TestStatistics/MinuitFcnGrad.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ class MinuitGradFunctor : public ROOT::Minuit2::FCNBase {
return grad;
}
std::vector<double> GradientWithPrevResult(std::vector<double> const &v, double *previous_grad, double *previous_g2,
double *previous_gstep) const override
double *previous_gstep, double fValAtV) const override
{
std::vector<double> output(v.size());
_fcn.GradientWithPrevResult(v.data(), output.data(), previous_grad, previous_g2, previous_gstep);
_fcn.GradientWithPrevResult(v.data(), output.data(), previous_grad, previous_g2, previous_gstep, fValAtV);
return output;
}
ROOT::Minuit2::GradientParameterSpace gradParameterSpace() const override
Expand Down Expand Up @@ -259,12 +259,12 @@ void MinuitFcnGrad::Gradient(const double *x, double *grad) const
}

void MinuitFcnGrad::GradientWithPrevResult(const double *x, double *grad, double *previous_grad, double *previous_g2,
double *previous_gstep) const
double *previous_gstep, double fValAtX) const
{
_calculatingGradient = true;
syncParameterValuesFromMinuitCalls(x, returnsInMinuit2ParameterSpace());
syncOffsets();
_gradient->fillGradientWithPrevResult(grad, previous_grad, previous_g2, previous_gstep);
_gradient->fillGradientWithPrevResult(grad, previous_grad, previous_g2, previous_gstep, fValAtX);
_calculatingGradient = false;
}

Expand Down
2 changes: 1 addition & 1 deletion roofit/roofitcore/src/TestStatistics/MinuitFcnGrad.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class MinuitFcnGrad : public RooAbsMinimizerFcn {
/// IMultiGradFunction overrides necessary for Minuit
void Gradient(const double *x, double *grad) const;
void GradientWithPrevResult(const double *x, double *grad, double *previous_grad, double *previous_g2,
double *previous_gstep) const;
double *previous_gstep, double fValAtX) const;

inline std::string getFunctionName() const override { return _likelihood->GetName(); }

Expand Down
Loading
Loading