From 0621e75c705dc9b68dc68d1b27caf4fcf8ecf875 Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Tue, 3 Feb 2026 11:39:07 +0100 Subject: [PATCH 1/4] [PyROOT] Release GIL before TH1.Fit runs. TH1.Fit might call into Python functions, which would require the GIL to be held. If TH1.Fit holds it, though, the process deadlocks. Here, the GIL is released before TH1.Fit, which runs fully in C++ anyway. Fix #21080. --- .../pyroot/pythonizations/python/ROOT/_pythonization/_th1.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_th1.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_th1.py index c4fbd2bbfc9ff..bb322f387403a 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_th1.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_th1.py @@ -270,3 +270,4 @@ def pythonize_th1(klass): _add_indexing_features(klass) inject_clone_releasing_ownership(klass) + klass.Fit.__release_gil__ = True From b4f6c1fb842e38b8d2765334372c85e59efaf3d1 Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Tue, 3 Feb 2026 12:53:49 +0100 Subject: [PATCH 2/4] [PyROOT] Rename TH1's test file to collect future tests in one file. This saves startup times and collects TH1 tests in one place. --- bindings/pyroot/pythonizations/test/CMakeLists.txt | 2 +- .../pyroot/pythonizations/test/{th1_operators.py => th1.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bindings/pyroot/pythonizations/test/{th1_operators.py => th1.py} (100%) diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 539316700e149..ca217104e9b59 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -60,7 +60,7 @@ ROOT_ADD_PYUNITTEST(pyroot_pyz_ttree_branch ttree_branch.py PYTHON_DEPS numpy) ROOT_ADD_PYUNITTEST(pyroot_pyz_tcolor tcolor.py) # TH1 and subclasses pythonizations -ROOT_ADD_PYUNITTEST(pyroot_pyz_th1_operators th1_operators.py) +ROOT_ADD_PYUNITTEST(pyroot_pyz_th1 th1.py) ROOT_ADD_PYUNITTEST(pyroot_pyz_th1_fillN th1_fillN.py PYTHON_DEPS numpy) ROOT_ADD_PYUNITTEST(pyroot_pyz_th2_fillN th2_fillN.py PYTHON_DEPS numpy) ROOT_ADD_PYUNITTEST(pyroot_pyz_th2 th2.py) diff --git a/bindings/pyroot/pythonizations/test/th1_operators.py b/bindings/pyroot/pythonizations/test/th1.py similarity index 100% rename from bindings/pyroot/pythonizations/test/th1_operators.py rename to bindings/pyroot/pythonizations/test/th1.py From aa67da8ada83da43beb5bd1c4538233ab1d35d5f Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Tue, 3 Feb 2026 13:50:42 +0100 Subject: [PATCH 3/4] [PyROOT] Add test for TH1.Fit with IMT. The timeout of the test is reduced because the deadlock would otherwise show only after 1500s. --- .../pyroot/pythonizations/test/CMakeLists.txt | 2 + bindings/pyroot/pythonizations/test/th1.py | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index ca217104e9b59..7457069a500aa 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -61,6 +61,8 @@ ROOT_ADD_PYUNITTEST(pyroot_pyz_tcolor tcolor.py) # TH1 and subclasses pythonizations ROOT_ADD_PYUNITTEST(pyroot_pyz_th1 th1.py) +# The above tests a deadlock. It should complete in about 1s. If we don't reduce the timeout, we need to wait 1500 s. +set_tests_properties(pyunittests-bindings-pyroot-pythonizations-pyroot-pyz-th1 PROPERTIES TIMEOUT 30) ROOT_ADD_PYUNITTEST(pyroot_pyz_th1_fillN th1_fillN.py PYTHON_DEPS numpy) ROOT_ADD_PYUNITTEST(pyroot_pyz_th2_fillN th2_fillN.py PYTHON_DEPS numpy) ROOT_ADD_PYUNITTEST(pyroot_pyz_th2 th2.py) diff --git a/bindings/pyroot/pythonizations/test/th1.py b/bindings/pyroot/pythonizations/test/th1.py index 881f89e8baa19..5dd7f32e90367 100644 --- a/bindings/pyroot/pythonizations/test/th1.py +++ b/bindings/pyroot/pythonizations/test/th1.py @@ -15,7 +15,7 @@ def test_imul(self): h = ROOT.TH1F("testHist", "", nbins, -4, 4) h.FillRandom("gaus") - initial_bins = [ h.GetBinContent(i) for i in range(nbins) ] + initial_bins = [h.GetBinContent(i) for i in range(nbins)] c = 2 # Multiply in place @@ -26,5 +26,45 @@ def test_imul(self): self.assertEqual(h.GetBinContent(i), initial_bins[i] * c) -if __name__ == '__main__': +class TH1IMT(unittest.TestCase): + """ + Test a deadlock when IMT is used in conjunction with a fit function in Python. + Since TH1.Fit held the GIL, the fit function could never be evaluated + """ + + @classmethod + def setUpClass(cls): + ROOT.ROOT.EnableImplicitMT(4) + + @classmethod + def tearDownClass(cls): + ROOT.ROOT.DisableImplicitMT() + + def test_fit_python_function(self): + xmin = 0 + xmax = 1 + + h1 = ROOT.TH1F("h1", "", 20, xmin, xmax) + h1.FillRandom("gaus", 1000) + + def func(x, pars): + return pars[0] + pars[1] * x[0] + + my_func = ROOT.TF1("f1", func, xmin, xmax, npar=2, ndim=1) + + my_func.SetParNames( + "A", + "B", + ) + my_func.SetParameter(0, 1) + my_func.SetParameter(1, -1) + + r = h1.Fit(my_func, "SE0Q", "", xmin, xmax) + + self.assertFalse(r.IsEmpty()) + self.assertTrue(r.IsValid()) + self.assertGreater(r.Parameter(0), 0) + + +if __name__ == "__main__": unittest.main() From 2232fb9acba8bbe876a90f8bdfb3c80755efd7fc Mon Sep 17 00:00:00 2001 From: siliataider Date: Thu, 5 Feb 2026 13:46:00 +0100 Subject: [PATCH 4/4] [DF][Python] Deserialize Dask results in the main thread --- bindings/distrdf/python/DistRDF/Backends/Dask/Backend.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bindings/distrdf/python/DistRDF/Backends/Dask/Backend.py b/bindings/distrdf/python/DistRDF/Backends/Dask/Backend.py index 9d629feae33ed..0874f5d36c1b8 100644 --- a/bindings/distrdf/python/DistRDF/Backends/Dask/Backend.py +++ b/bindings/distrdf/python/DistRDF/Backends/Dask/Backend.py @@ -323,10 +323,11 @@ def _process_partial_results(self, cumulative_plots: Dict[int, Any] = {} # Collect all futures in batches that had arrived since the last iteration - for batch in as_completed(future_tasks, with_results=True).batches(): - for future, result in batch: - merged_results = reducer(merged_results, result) if merged_results else result - + for batch in as_completed(future_tasks, with_results=False).batches(): + for future in batch: + result = future.result() + merged_results = reducer(merged_results, result) if merged_results else result + mergeables = merged_results.mergeables for pad_num, (drawable_id, (callbacks_list, index, operation_name)) in enumerate(drawables_info_dict.items(), start=1):