Nathaniel, Thanks for the feedback. Investigations made where I acquire GIL in spots where I already hold it lead me to suspect that the issue is caused by use of Py_BEGIN_ALLOW_THREADS or another macro NPY_BEGIN_ALLOW_THREADS.

to put this point to rest, I have a high degree of confidence in our code. I have run the code in valgrind, and it runs cleanly when I use Python's valgrind suppression file. It is certainly the case the the error is harder to reproduce with valgrind, and it runs so slowly under valgrind that running in a bash loop (the usual way I reproduce) is not practical. I have examined crashes with gdb and I have verified that they occur while I hold the GIL.

However, based on your suggestion, I experimented with acquiring GIL in certain spots in my code where it should not be necessary either because those code pieces are invoked by the main thread when there is only one thread running, or because the code in question is invoked while I'm already holding the GIL. I also added trace output to stderr in those spots as well.

Acquiring the GIL caused deadlocks as expected, except when I used it in new_object template (the only spot we pass data to numpy) below. Aquiring the GIL here fixes the issues!

   206 //
   ****************************************************************************
   207 template <typename NT>
   208 PyArrayObject *new_object(teca_variant_array_impl<NT> *varrt)
   209 {
   *210     PyGILState_STATE gstate;**// experimental, I already hold
   the GIL higher up in stack
   **211     gstate = PyGILState_Ensure();*
   212     TECA_STATUS("teca_py_array::new_object");
   213
   214     // allocate a buffer
   215     npy_intp n_elem = varrt->size();
   216     size_t n_bytes = n_elem*sizeof(NT);
   217     NT *mem = static_cast<NT*>(malloc(n_bytes));
   218     if (!mem)
   219     {
   220         PyErr_Format(PyExc_RuntimeError,
   221             "failed to allocate %lu bytes", n_bytes);
   222         return nullptr;
   223     }
   224
   225     // copy the data
   226     memcpy(mem, varrt->get(), n_bytes);
   227
   228     // put the buffer in to a new numpy object
   229     PyArrayObject *arr = reinterpret_cast<PyArrayObject*>(
   230         PyArray_SimpleNewFromData(1, &n_elem,
   numpy_tt<NT>::code, mem));
   231     PyArray_ENABLEFLAGS(arr, NPY_ARRAY_OWNDATA);
   232
   *233     PyGILState_Release(gstate);*
   234     return arr;
   235 }

now, this function is only used from within the user provided Python callback which is invoked only while I'm holding the GIL. A fact that I've verified via gdb stack traces. This function should be running serially due to the fact that I hold the GIL. However, it's running concurrently, as evidenced from the random crashes when it's used, and the garbled stderr output, both of which go away with the above addition. I don't think that I should have to acquire the GIL here, but the evidence is against me!

In Python docs on the GIL, I noticed that there's a macro Py_BEGIN_ALLOW_THREADS that Python uses internally around blocking I/O and heavy computations. much to my surprise grep reveals Py_BEGIN_ALLOW_THREADS is used in numpy. I think this can explain the issues I'm experiencing. where numpy uses Py_BEGIN_ALLOW_THREADS it would let my threads, who utilize the numpy C-API via new_object template above, run concurrently despite the fact that I hold the GIL. It seems to me that in order for numpy to be thread safe it should not use this macro at all. At least this is my theory, I haven't yet had a chance to modify numpy build to verify. It's possible that Py_BEGIN_ALLOW_THREADS maybe used elsewhere.

here's my question: given Py_BEGIN_ALLOW_THREADS is used by numpy how can numpy be thread safe? and how can someone using the C-API know where it's necessary to acquire the GIL? Maybe someone can explain this?

   $grep BEGIN_ALLOW_THREADS ./ -rIn
   ./doc/source/f2py/signature-file.rst:250:      Use
   ``Py_BEGIN_ALLOW_THREADS .. Py_END_ALLOW_THREADS`` block
   ./doc/source/reference/c-api.array.rst:3092:    .. c:macro::
   NPY_BEGIN_ALLOW_THREADS
   ./doc/source/reference/c-api.array.rst:3094:        Equivalent to
   :c:macro:`Py_BEGIN_ALLOW_THREADS` except it uses
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2109:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2125:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2143:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2159:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2177:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2193:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2211:
   Py_BEGIN_ALLOW_THREADS
   ./build/src.linux-x86_64-2.7/numpy/core/src/multiarray/scalartypes.c:2227:
   Py_BEGIN_ALLOW_THREADS
   ./build/lib.linux-x86_64-2.7/numpy/f2py/rules.py:419: {isthreadsafe:
   '\t\t\tPy_BEGIN_ALLOW_THREADS'},
   ./build/lib.linux-x86_64-2.7/numpy/f2py/rules.py:457: {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./build/lib.linux-x86_64-2.7/numpy/f2py/rules.py:495: {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./build/lib.linux-x86_64-2.7/numpy/f2py/rules.py:541: {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./build/lib.linux-x86_64-2.7/numpy/f2py/rules.py:583: {isthreadsafe:
   '\t\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/f2py/rules.py:419:            {isthreadsafe:
   '\t\t\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/f2py/rules.py:457:            {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/f2py/rules.py:495:            {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/f2py/rules.py:541:            {isthreadsafe:
   '\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/f2py/rules.py:583: {isthreadsafe: '\t\tPy_BEGIN_ALLOW_THREADS'},
   ./numpy/fft/fftpack_litemodule.c:45: Py_BEGIN_ALLOW_THREADS;
   ./numpy/fft/fftpack_litemodule.c:98: Py_BEGIN_ALLOW_THREADS;
   ./numpy/fft/fftpack_litemodule.c:135: Py_BEGIN_ALLOW_THREADS;
   ./numpy/fft/fftpack_litemodule.c:191: Py_BEGIN_ALLOW_THREADS;
   ./numpy/fft/fftpack_litemodule.c:254: Py_BEGIN_ALLOW_THREADS;
   ./numpy/fft/fftpack_litemodule.c:295:  Py_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/ctors.c:3237: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/ctors.c:3277: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/cblasfuncs.c:378: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/cblasfuncs.c:525: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/cblasfuncs.c:549: NPY_BEGIN_ALLOW_THREADS
   ./numpy/core/src/multiarray/cblasfuncs.c:576: NPY_BEGIN_ALLOW_THREADS
   ./numpy/core/src/multiarray/cblasfuncs.c:628: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/cblasfuncs.c:761: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:157:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:181:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:473:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:810:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:1014:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/compiled_base.c:1049:
   NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/scalartypes.c.src:939:
   Py_BEGIN_ALLOW_THREADS
   ./numpy/core/src/multiarray/scalartypes.c.src:955:
   Py_BEGIN_ALLOW_THREADS
   ./numpy/core/src/multiarray/convert.c:98: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/convert.c:210: NPY_BEGIN_ALLOW_THREADS;
   ./numpy/core/src/multiarray/multiarraymodule.c:3981:
   Py_BEGIN_ALLOW_THREADS;
   ./numpy/core/include/numpy/ndarraytypes.h:932:#define
   NPY_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
   ./numpy/core/include/numpy/ndarraytypes.h:952:#define
   NPY_BEGIN_ALLOW_THREADS

On 06/13/2016 07:07 PM, Nathaniel Smith wrote:
Hi Burlen,

On Jun 13, 2016 5:24 PM, "Burlen Loring" <blor...@lbl.gov> wrote:
Hi All,

I'm working on a threaded pipeline where we want the end user to be able to 
code up Python functions to do numerical work. Threading is all done in C++11 
and in each thread we've acquired gill before we invoke the user provided 
Python callback and release it only when the callback returns. We've used SWIG 
to expose bindings to C++ objects in Python.

When run with more than 1 thread I get inconsistent segv's in various numpy 
routines, and occasionally see a *** Reference count error detected: an attempt 
was made to deallocate 11 (f) ***.

To pass data from C++ to the Python callback as numpy array we have
206 // 
****************************************************************************
207 template <typename NT>
208 PyArrayObject *new_object(teca_variant_array_impl<NT> *varrt)
209 {
210     // allocate a buffer
211     npy_intp n_elem = varrt->size();
212     size_t n_bytes = n_elem*sizeof(NT);
213     NT *mem = static_cast<NT*>(malloc(n_bytes));
214     if (!mem)
215     {
216         PyErr_Format(PyExc_RuntimeError,
217             "failed to allocate %lu bytes", n_bytes);
218         return nullptr;
219     }
220
221     // copy the data
222     memcpy(mem, varrt->get(), n_bytes);
223
224     // put the buffer in to a new numpy object
225     PyArrayObject *arr = reinterpret_cast<PyArrayObject*>(
226         PyArray_SimpleNewFromData(1, &n_elem, numpy_tt<NT>::code, mem));
227     PyArray_ENABLEFLAGS(arr, NPY_ARRAY_OWNDATA);
228
229     return arr;
230 }
This code would probably be much simpler if you let numpy allocate the
buffer with PyArray_SimpleNew and then did the memcpy. I doubt that's
your problem, though. Numpy should be assuming that "owned" data was
allocated using malloc(), and if it were using a different allocator
then I think you'd be seeing crashes much sooner.

This is the only place we create numpy objects in the C++ side.

In my demo the Python callback is as follows:
  33 def get_execute(rank, var_names):
  34     def execute(port, data_in, req):
  35         sys.stderr.write('descriptive_stats::execute MPI %d\n'%(rank))
  36
  37         mesh = as_teca_cartesian_mesh(data_in[0])
  38
  39         table = teca_table.New()
  40         table.copy_metadata(mesh)
  41
  42         table.declare_columns(['step','time'], ['ul','d'])
  43         table << mesh.get_time_step() << mesh.get_time()
  44
  45         for var_name in var_names:
  46
  47             table.declare_columns(['min '+var_name, 'avg '+var_name, \
  48                 'max '+var_name, 'std '+var_name, 'low_q '+var_name, \
  49                 'med '+var_name, 'up_q '+var_name], ['d']*7)
  50
  51             var = mesh.get_point_arrays().get(var_name).as_array()
  52
  53             table << float(np.min(var)) << float(np.average(var)) \
  54                 << float(np.max(var)) << float(np.std(var)) \
  55                 << map(float, np.percentile(var, [25.,50.,75.]))
  56
  57         return table
  58     return execute
this callback is the only spot where numpy is used. the as_array call is 
implemented by new_object template above.
Further, If I remove our use of PyArray_SimpleNewFromData, by replacing line 51 
in the Python code above with var = np.array(range(1, 1100), 'f'), the problem 
disappears. It must have something to do with use of PyArray_SimpleNewFromData.

I'm at a loss to see why things are going south. I'm using the GIL and I 
thought that would serialize the Python code. I suspect that numpy is using 
global or static variables some where internally and that it's inherently 
thread unsafe. Can anyone confirm/deny? maybe point me in the right direction?
Numpy does use global/static variables, and it is unsafe to call into
numpy simultaneously from different threads. But that's ok, because
you're not allowed to call numpy functions simultaneously from
different threads -- you have to hold the GIL first, and that
serializes access to all of numpy's internal state. Numpy is very
commonly used in threaded code and most people aren't seeing random
segfaults, so the problem is most likely in your code. Sorry I can't
help much more than that... I guess I'd start by triple-checking that
the code really truly does hold the GIL every time that it calls into
numpy/python APIs. I'd also try running it under valgrind in case it's
some other random memory corruption that's just showing up in a weird
way.

-n
_______________________________________________
NumPy-Discussion mailing list
NumPy-Discussion@scipy.org
https://mail.scipy.org/mailman/listinfo/numpy-discussion

_______________________________________________
NumPy-Discussion mailing list
NumPy-Discussion@scipy.org
https://mail.scipy.org/mailman/listinfo/numpy-discussion

Reply via email to