Multiprocessing fits within PyROOT

The fitting procedure in the ROOT is extremely slow, so, if one want to perform many different fits on hundreds of histograms it is better to do it somehow in parallel.
So, there is a function to handle the histogram fits:

def apply_fit(hist, fit, *frange):
    hist.Fit(fit, "0Q", "", *frange)
    return fit

With the ThreadPool you can use it like this:

pool = ThreadPool(4)
list1 = (hist1, fit_thermal, *fit_range)
list2 = (hist1, fit_doubleexponent, *fit_range)
list3 = (hist1, fit_blastwave, *fit_range)
list4 = (hist1, fit_boseeinstein, *fit_range)
result = pool.starmap(apply_fit, [list1, list2, list3, list4])

And it will work and within results you’ll get your fits.
But ThreadPool is slow due to the GIL in the Python, so for the CPU tasks one must use the multiprocessing.Pool, e.g.:

pool = multiprocessing.Pool(processes=4)
list1 = (hist1, fit_thermal, *fit_range)
list2 = (hist1, fit_doubleexponent, *fit_range)
list3 = (hist1, fit_blastwave, *fit_range)
list4 = (hist1, fit_boseeinstein, *fit_range)
result = pool.starmap(apply_fit, [list1, list2, list3, list4])

Both hist and fit objects (TH1D, TF1) are passed well to the function with the needed parameters for the each fit, but the fit function in the case returns Invalid FitResult (status = 2 ) – it always stops after the second iteration.

So, the question is, how to deal with fits and multiprocessing?


ROOT Version: 6.32.00
Platform: Linux, MacOS
Compiler: gcc, llvm

Thank you for your question @vkireyeu
I’m sure @Danilo or @jonas will be able to answer your question.

Cheers,
Devajith

Hi,

Apologies for the trivial question, but are your fits converging fine in sequential mode?
If yes, can you post a reproducer of the issue that we can run?

Cheers,
Danilo

Hi, yes, fits are working fine in 1) normal “sequential” mode and in 2) “multithreaded” mode (the corresponding code block is in my first message), but not in the 3) “multiprocessing” mode.

I also tried to use the Pipe from the multiprocessing package to send results back to the main process – but the problem is exactly the same as within the multiprocessing.Pool – the fit procedure starts (e.g. four in parallel) but it stops almost immediately on the second iteration.

I initialise the TF1 type fits in the main process with the needed parameters – everything is passed well to the apply_fit() function and the TH1D histogram content is also there.

Here is the very trivial MWE.
With pool = ThreadPool(4) fits are “working”, with pool = multiprocessing.Pool(processes=4) two of them – not (with the Invalid FitResult (status = 2 )).

fits_test.py (1.4 KB)

Hi! So, if there is any workaround maybe?

Hi @vkireyeu,
sorry for the delay in answering but @Danilo is currently on vacation, so he probably didn’t have time to follow the issue in the past couple of weeks.
I’ll try to ping again @jonas who might be able to help on this.

1 Like

Hello,

Here I am.
I confirm I can reproduce the issue and opened an item in our tracker: TF1 and TFitResultPtr do not serialise correctly pickle, and this is an issue with Python multiprocessing · Issue #16184 · root-project/root · GitHub . Thanks for the report, it is very useful.

I could work around the issue by initialising one of the parameters of the function. A stripped-down version of the working code, inspired by the very good example given before:

#!/usr/bin/env python3

import multiprocessing
import ROOT

def apply_fit(hist, fit, *frange):
    fit.SetParameter(0,1) # <--- Workaround waiting for https://github.com/root-project/root/issues/16184
    hist.Fit(fit, "", "", *frange)
    return fit

def main():
    c = ROOT.TCanvas()
    h = ROOT.TH1F("myHist", "myTitle", 64, -4, 4)
    h.FillRandom("gaus")
    fit_range = [-3, 3]
    f1 = ROOT.TF1("f1", "gaus")
    f2 = ROOT.TF1("f2", "expo")
    f3 = ROOT.TF1("f3", "pol3")
    f4 = ROOT.TF1("f4", "landau")

    pool = multiprocessing.Pool(processes=4)
    list1 = (h, f1, *fit_range)
    list2 = (h, f2, *fit_range)
    list3 = (h, f3, *fit_range)
    list4 = (h, f4, *fit_range)

    result = pool.starmap(apply_fit, [list1,list2,list3,list4])
    pool.close()
    pool.join()
    print(result)

    h.Draw()
    result[0].Draw('same')
    result[1].Draw('same')
    result[2].Draw('same')
    result[3].Draw('same')
    c.Print('test.pdf')

if __name__ == "__main__":
    main()

I hope this unblocks you.

Cheers,
D

1 Like

Hi! Thank you for the reply!
That workaround really works well for the predefined functions!

However, if we are using some other functions, this procedure does not work.
Here is a bit updated code with the new defined fit function:
fits_test.py (1.5 KB)

Hello,

Thanks once again for the very clear reproducer.
I acknowledge that there is another problem, different from the previous one: we’ll follow this up in the aforementioned ticket.
In the meantime, I have a workaround - apologies for this, but I thought it’s important to unblock you:

#!/usr/bin/env python3

import multiprocessing
import ROOT

import math

def fit_thermal(x, par):
    dndy = par[0]
    T    = par[1]
    m0   = par[2]
    mt   = math.sqrt(x[0]*x[0] + m0*m0)
    val  = (dndy/(T*(m0 + T))) * math.exp(-(mt - m0)/T)
    return val

class TF1Wrapper:
    def __init__(self, *args):
        self.f = ROOT.TF1(*args)
        self.args = args

def apply_fit(hist, fit, *frange):
    # Work around: we are unfortunately forced to recreate the func
    fitf = ROOT.TF1(*fit.args)
    for npar in range(fit.f.GetNpar()):
        fitf.SetParameter(npar, fit.f.GetParameter(npar))
    hist.Fit(fitf, "", "", *frange)
    return fitf

def main():
    h = ROOT.TH1F("myHist", "myTitle", 64, -4, 4)
    h.FillRandom("gaus")
    fit_range = [-3, 3]
    
    thermal = TF1Wrapper("thermal", fit_thermal, 0, 2, 3)
    thermal.f.SetParNames("dN/dy", "T", "m0")
    thermal.f.SetParameters(20, 0.1, 0.938)

    f1 = TF1Wrapper("f1", "gaus")
    f2 = TF1Wrapper("f2", "expo")
    f3 = TF1Wrapper("f3", "pol3")

    # ~ pool = ThreadPool(4)
    pool = multiprocessing.Pool(processes=1)
    list1 = (h, f1, *fit_range)
    list2 = (h, f2, *fit_range)
    list3 = (h, f3, *fit_range)
    list4 = (h, thermal, *fit_range)

    result = pool.starmap(apply_fit, [list1,list2,list3,list4])
    pool.close()
    pool.join()

    c = ROOT.TCanvas()
    h.Draw()
    result[0].Draw('same')
    result[1].Draw('same')
    result[2].Draw('same')
    result[3].Draw('same')
    c.Print('test.pdf')

if __name__ == "__main__":
    main()

Basically, it consists in re-instantiating the function once in the spawned process.

Thanks for your patience.

Best,
Danilo

1 Like

Thank you!

Hope this thread and your solution helps someone else.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.