Skip to content

Commit 747738a

Browse files
committed
[RF] Support analytical Hessians with Clad
Implement all the necessary infrastructure to optionally generate Hessians with Clad via RooFit "codegen", and forward them to Minuit 2, including also validation code in the debug macro that can be printed out for RooFit functions that support "codegen". This addresses an Item from the ROOT 2026 plan of work. It doesn't work yet, because a new Clad release is required, where this issue is fixed: vgvassilev/clad#1736 However, it's already good to have all the necessary changes in ROOT so we can test and validate Clad developments for Hessian support more easily.
1 parent d21fdce commit 747738a

10 files changed

Lines changed: 262 additions & 18 deletions

File tree

roofit/roofitcore/inc/RooAbsReal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,15 @@ class RooAbsReal : public RooAbsArg {
391391
virtual void doEval(RooFit::EvalContext &) const;
392392

393393
virtual bool hasGradient() const { return false; }
394+
virtual bool hasHessian() const { return false; }
394395
virtual void gradient(double *) const {
395396
if(!hasGradient()) throw std::runtime_error("RooAbsReal::gradient(double *) not implemented by this class!");
396397
}
398+
virtual void hessian(double *) const
399+
{
400+
if (!hasHessian())
401+
throw std::runtime_error("RooAbsReal::hessian(double *) not implemented by this class!");
402+
}
397403

398404
// PlotOn with command list
399405
virtual RooPlot* plotOn(RooPlot* frame, RooLinkedList& cmdList) const ;

roofit/roofitcore/inc/RooEvaluatorWrapper.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ class RooEvaluatorWrapper final : public RooAbsReal {
6262
void constOptimizeTestStatistic(ConstOpCode /*opcode*/, bool /*doAlsoTrackingOpt*/) override {}
6363

6464
bool hasGradient() const override;
65+
bool hasHessian() const override;
6566

6667
void gradient(double *out) const override;
68+
void hessian(double *out) const override;
6769

6870
void generateGradient();
71+
void generateHessian();
6972

7073
void setUseGeneratedFunctionCode(bool);
7174

roofit/roofitcore/inc/RooMinimizer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class RooMinimizer : public TObject {
8282
Config() {}
8383

8484
bool useGradient = true; // Use the gradient provided by the RooAbsReal, if there is one.
85+
bool useHessian = false; // Use the Hessian provided by the RooAbsReal, if there is one.
8586

8687
double recoverFromNaN = 10.; // RooAbsMinimizerFcn config
8788
int printEvalErrors = 10; // RooAbsMinimizerFcn config

roofit/roofitcore/src/RooAbsMinimizerFcn.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class RooAbsMinimizerFcn {
3939
RooAbsMinimizerFcn(RooArgList paramList, RooMinimizer *context);
4040
virtual ~RooAbsMinimizerFcn() = default;
4141

42-
virtual void initMinimizer(ROOT::Math::Minimizer &) = 0;
42+
virtual void initMinimizer(ROOT::Math::Minimizer &, RooMinimizer *context) = 0;
4343

4444
/// Informs Minuit through its parameter_settings vector of RooFit parameter properties.
4545
bool synchronizeParameterSettings(std::vector<ROOT::Fit::ParameterSettings> &parameters, bool optConst);

roofit/roofitcore/src/RooEvaluatorWrapper.cxx

Lines changed: 193 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,22 @@ class RooFuncWrapper {
128128
RooFuncWrapper(RooAbsReal &obj, const RooAbsData *data, RooSimultaneous const *simPdf, RooArgSet const &paramSet);
129129

130130
bool hasGradient() const { return _hasGradient; }
131+
bool hasHessian() const { return _hasHessian; }
131132
void gradient(double *out) const
132133
{
133134
updateGradientVarBuffer();
134135
std::fill(out, out + _params.size(), 0.0);
135-
136-
_grad(_gradientVarBuffer.data(), _observables.data(), _xlArr.data(), out);
136+
_grad(_varBuffer.data(), _observables.data(), _xlArr.data(), out);
137+
}
138+
void hessian(double *out) const
139+
{
140+
updateGradientVarBuffer();
141+
std::fill(out, out + _params.size() * _params.size(), 0.0);
142+
_hessian(_varBuffer.data(), _observables.data(), _xlArr.data(), out);
137143
}
138144

139145
void createGradient();
146+
void createHessian();
140147

141148
void writeDebugMacro(std::string const &) const;
142149

@@ -145,7 +152,7 @@ class RooFuncWrapper {
145152
double evaluate() const
146153
{
147154
updateGradientVarBuffer();
148-
return _func(_gradientVarBuffer.data(), _observables.data(), _xlArr.data());
155+
return _func(_varBuffer.data(), _observables.data(), _xlArr.data());
149156
}
150157

151158
void loadData(RooAbsData const &data, RooSimultaneous const *simPdf);
@@ -157,13 +164,16 @@ class RooFuncWrapper {
157164

158165
using Func = double (*)(double *, double const *, double const *);
159166
using Grad = void (*)(double *, double const *, double const *, double *);
167+
using Hessian = void (*)(double *, double const *, double const *, double *);
160168

161169
RooArgList _params;
162170
std::string _funcName;
163171
Func _func;
164172
Grad _grad;
173+
Hessian _hessian;
165174
bool _hasGradient = false;
166-
mutable std::vector<double> _gradientVarBuffer;
175+
bool _hasHessian = false;
176+
mutable std::vector<double> _varBuffer;
167177
std::vector<double> _observables;
168178
std::unordered_map<RooFit::Detail::DataKey, std::size_t> _obsInfos;
169179
std::vector<double> _xlArr;
@@ -226,7 +236,7 @@ RooFuncWrapper::RooFuncWrapper(RooAbsReal &obj, const RooAbsData *data, RooSimul
226236
_params.add(*param);
227237
}
228238
}
229-
_gradientVarBuffer.resize(_params.size());
239+
_varBuffer.resize(_params.size());
230240

231241
// Figure out which part of the computation graph depends on data
232242
std::unordered_set<RooFit::Detail::DataKey> dependsOnData;
@@ -355,9 +365,59 @@ void RooFuncWrapper::createGradient()
355365
#endif
356366
}
357367

368+
void RooFuncWrapper::createHessian()
369+
{
370+
#ifdef ROOFIT_CLAD
371+
std::string hessianName = _funcName + "_hessian_0";
372+
std::string requestName = _funcName + "_hessian_req";
373+
374+
// Calculate Hessian
375+
gInterpreter->Declare("#include <Math/CladDerivator.h>\n");
376+
// disable clang-format for making the following code unreadable.
377+
// clang-format off
378+
std::stringstream requestFuncStrm;
379+
std::string paramsStr =
380+
_params.size() == 1 ? "\"params[0]\"" : ("\"params[0:" + std::to_string(_params.size() - 1) + "]\"");
381+
requestFuncStrm << "#pragma clad ON\n"
382+
"void " << requestName << "() {\n"
383+
" clad::hessian(" << _funcName << ", " << paramsStr << ");\n"
384+
"}\n"
385+
"#pragma clad OFF";
386+
// clang-format on
387+
auto print = [](std::string const &msg) { oocoutI(nullptr, Fitting) << msg << std::endl; };
388+
389+
bool cladSuccess = false;
390+
{
391+
ROOT::Math::Util::TimingScope timingScope(print, "Hessian generation time:");
392+
cladSuccess = !gInterpreter->Declare(requestFuncStrm.str().c_str());
393+
}
394+
if (cladSuccess) {
395+
std::stringstream errorMsg;
396+
errorMsg << "Function could not be differentiated. See above for details.";
397+
oocoutE(nullptr, InputArguments) << errorMsg.str() << std::endl;
398+
throw std::runtime_error(errorMsg.str().c_str());
399+
}
400+
401+
// Clad provides different overloads for the Hessian, and we need to
402+
// resolve to the one that we want. Without the static_cast, getting the
403+
// function pointer would be ambiguous.
404+
std::stringstream ss;
405+
ROOT::Math::Util::TimingScope timingScope(print, "Hessian IR to machine code time:");
406+
ss << "static_cast<void (*)(double *, double const *, double const *, double *)>(" << hessianName << ");";
407+
_hessian = reinterpret_cast<Hessian>(gInterpreter->ProcessLine(ss.str().c_str()));
408+
_hasHessian = true;
409+
#else
410+
_hasHessian = false;
411+
std::stringstream errorMsg;
412+
errorMsg << "Function could not be differentiated since ROOT was built without Clad support.";
413+
oocoutE(nullptr, InputArguments) << errorMsg.str() << std::endl;
414+
throw std::runtime_error(errorMsg.str().c_str());
415+
#endif
416+
}
417+
358418
void RooFuncWrapper::updateGradientVarBuffer() const
359419
{
360-
std::transform(_params.begin(), _params.end(), _gradientVarBuffer.begin(), [](RooAbsArg *obj) {
420+
std::transform(_params.begin(), _params.end(), _varBuffer.begin(), [](RooAbsArg *obj) {
361421
return obj->isCategory() ? static_cast<RooAbsCategory *>(obj)->getCurrentIndex()
362422
: static_cast<RooAbsReal *>(obj)->getVal();
363423
});
@@ -385,17 +445,25 @@ void RooFuncWrapper::writeDebugMacro(std::string const &filename) const
385445
}
386446

387447
std::ofstream outFile;
448+
std::string paramsStr =
449+
_params.size() == 1 ? "\"params[0]\"" : ("\"params[0:" + std::to_string(_params.size() - 1) + "]\"");
388450
outFile.open(filename + ".C");
389451
outFile << R"(//auto-generated test macro
390452
#include <RooFit/Detail/MathFuncs.h>
391453
#include <Math/CladDerivator.h>
392454
455+
//#define DO_HESSIAN
456+
393457
)" << allCode.str()
394458
<< R"(
395459
#pragma clad ON
396460
void gradient_request() {
397461
clad::gradient()"
398462
<< _funcName << R"(, "params");
463+
#ifdef DO_HESSIAN
464+
clad::hessian()"
465+
<< _funcName << ", " << paramsStr << R"();
466+
#endif
399467
}
400468
#pragma clad OFF
401469
)";
@@ -423,7 +491,7 @@ void gradient_request() {
423491
};
424492

425493
outFile << "// clang-format off\n" << std::endl;
426-
writeVector("parametersVec", _gradientVarBuffer);
494+
writeVector("parametersVec", _varBuffer);
427495
outFile << std::endl;
428496
writeVector("observablesVec", _observables);
429497
outFile << std::endl;
@@ -436,7 +504,9 @@ void gradient_request() {
436504
void )" << filename
437505
<< R"(()
438506
{
439-
std::vector<double> gradientVec(parametersVec.size());
507+
const std::size_t n = parametersVec.size();
508+
509+
std::vector<double> gradientVec(n);
440510
441511
auto func = [&](std::span<double> params) {
442512
return )"
@@ -465,6 +535,100 @@ void )" << filename
465535
std::cout << " numr : " << numDiff(i) << std::endl;
466536
std::cout << " clad : " << gradientVec[i] << std::endl;
467537
}
538+
539+
#ifdef DO_HESSIAN
540+
std::cout << "\n";
541+
542+
auto hess = [&](std::span<double> params, std::span<double> out) {
543+
return )"
544+
<< _funcName << R"(_hessian_0(params.data(), observablesVec.data(), auxConstantsVec.data(), out.data());
545+
};
546+
547+
std::vector<double> hessianVec(n * n);
548+
hess(parametersVec, hessianVec);
549+
550+
// ---------- Numerical Hessian ----------
551+
// Uses central differences:
552+
// diag: (f(x+ei)-2f(x)+f(x-ei))/eps^2
553+
// offdiag: (f(++ ) - f(+-) - f(-+) + f(--)) / (4 eps^2)
554+
auto numHess = [&](std::size_t i, std::size_t j) {
555+
const double eps = 1e-5; // often needs to be a bit larger than grad eps
556+
std::vector<double> p(parametersVec.begin(), parametersVec.end());
557+
558+
if (i == j) {
559+
const double f0 = func(p);
560+
561+
p[i] = parametersVec[i] + eps;
562+
const double fUp = func(p);
563+
564+
p[i] = parametersVec[i] - eps;
565+
const double fDown = func(p);
566+
567+
return (fUp - 2.0 * f0 + fDown) / (eps * eps);
568+
} else {
569+
// f(x_i + eps, x_j + eps)
570+
p[i] = parametersVec[i] + eps;
571+
p[j] = parametersVec[j] + eps;
572+
const double fPP = func(p);
573+
574+
// f(x_i + eps, x_j - eps)
575+
p[i] = parametersVec[i] + eps;
576+
p[j] = parametersVec[j] - eps;
577+
const double fPM = func(p);
578+
579+
// f(x_i - eps, x_j + eps)
580+
p[i] = parametersVec[i] - eps;
581+
p[j] = parametersVec[j] + eps;
582+
const double fMP = func(p);
583+
584+
// f(x_i - eps, x_j - eps)
585+
p[i] = parametersVec[i] - eps;
586+
p[j] = parametersVec[j] - eps;
587+
const double fMM = func(p);
588+
589+
return (fPP - fPM - fMP + fMM) / (4.0 * eps * eps);
590+
}
591+
};
592+
593+
// Compute full numerical Hessian
594+
std::vector<double> numHessianVec(n * n);
595+
for (std::size_t i = 0; i < n; ++i) {
596+
for (std::size_t j = 0; j < n; ++j) {
597+
numHessianVec[i + n * j] = numHess(i, j); // keep same layout as your print
598+
}
599+
}
600+
601+
// ---------- Compare & print ----------
602+
std::cout << "Hessian comparison (clad vs numeric vs diff):\n\n";
603+
604+
for (std::size_t i = 0; i < n; ++i) {
605+
for (std::size_t j = 0; j < n; ++j) {
606+
const std::size_t idx = i + n * j; // same indexing you used
607+
const double cladH = hessianVec[idx];
608+
const double numH = numHessianVec[idx];
609+
const double diff = cladH - numH;
610+
611+
std::cout << "[" << i << "," << j << "] "
612+
<< "clad=" << cladH << " num=" << numH << " diff=" << diff << "\n";
613+
}
614+
}
615+
616+
std::cout << "\nRaw Clad Hessian matrix:\n";
617+
for (std::size_t i = 0; i < n; ++i) {
618+
for (std::size_t j = 0; j < n; ++j) {
619+
std::cout << hessianVec[i + n * j] << " ";
620+
}
621+
std::cout << "\n";
622+
}
623+
624+
std::cout << "\nRaw Numerical Hessian matrix:\n";
625+
for (std::size_t i = 0; i < n; ++i) {
626+
for (std::size_t j = 0; j < n; ++j) {
627+
std::cout << numHessianVec[i + n * j] << " ";
628+
}
629+
std::cout << "\n";
630+
}
631+
#endif
468632
}
469633
)";
470634
}
@@ -532,7 +696,16 @@ void RooEvaluatorWrapper::generateGradient()
532696
{
533697
if (!_funcWrapper)
534698
createFuncWrapper();
535-
_funcWrapper->createGradient();
699+
if (!_funcWrapper->hasGradient())
700+
_funcWrapper->createGradient();
701+
}
702+
703+
void RooEvaluatorWrapper::generateHessian()
704+
{
705+
if (!_funcWrapper)
706+
createFuncWrapper();
707+
if (!_funcWrapper->hasHessian())
708+
_funcWrapper->createHessian();
536709
}
537710

538711
void RooEvaluatorWrapper::setUseGeneratedFunctionCode(bool flag)
@@ -547,11 +720,19 @@ void RooEvaluatorWrapper::gradient(double *out) const
547720
_funcWrapper->gradient(out);
548721
}
549722

723+
void RooEvaluatorWrapper::hessian(double *out) const
724+
{
725+
_funcWrapper->hessian(out);
726+
}
727+
550728
bool RooEvaluatorWrapper::hasGradient() const
551729
{
552-
if (!_funcWrapper)
553-
return false;
554-
return _funcWrapper->hasGradient();
730+
return _funcWrapper && _funcWrapper->hasGradient();
731+
}
732+
733+
bool RooEvaluatorWrapper::hasHessian() const
734+
{
735+
return _funcWrapper && _funcWrapper->hasHessian();
555736
}
556737

557738
void RooEvaluatorWrapper::writeDebugMacro(std::string const &filename) const

roofit/roofitcore/src/RooMinimizer.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ bool RooMinimizer::calculateMinosErrors()
11281128
void RooMinimizer::initMinimizer()
11291129
{
11301130
_minimizer = std::unique_ptr<ROOT::Math::Minimizer>(_config.CreateMinimizer());
1131-
_fcn->initMinimizer(*_minimizer);
1131+
_fcn->initMinimizer(*_minimizer, this);
11321132
_minimizer->SetVariables(_config.ParamsSettings().begin(), _config.ParamsSettings().end());
11331133

11341134
if (_cfg.setInitialCovariance) {

0 commit comments

Comments
 (0)