C-расширение Python с numpy случайно аварийно завершает работу после нескольких вызовов (5–10) из кода Python

Чтобы ускорить пользовательские вычисления MFCC, мне пришлось написать некоторый код на C, обернуть его C API Python и C API numpy и скомпилировать его с помощью disutils.core, чтобы импортировать его из кода Python.

Мне удалось успешно скомпилировать и выполнить код Python, используя небольшой образец.

Однако, как только я вызвал одну и ту же функцию с одним и тем же вводом примерно 10 раз подряд, используя цикл timeit ИЛИ цикл for, Python остановился, не выдав никаких сообщений об ошибках.

Я не думаю, что это связано с утечкой памяти, так как входные данные занимают 150 КБ, а у меня на тот момент были гигабайты свободной оперативной памяти.

Самое загадочное? Это работает, когда функция вызывается один или два раза. Кажется, что при вызове двузначных чисел всегда происходит сбой.

Вот мой код, воспроизведенный:

#define PY_SSIZE_T_CLEAN
#include <stdio.h>
#include <stdlib.h>
#include "CMel.h"


typedef struct{
    int real, imag;
} COMPLEX;

static PyMethodDef CMelMethods[] = {
    {"mel_40", mel_40, METH_VARARGS},
    {NULL, NULL}
};

static PyModuleDef CMelModule = {
    PyModuleDef_HEAD_INIT, "CMel", "Python interface for C-code of integer-based Mel Spectrogram calculation", -1, CMelMethods
};

PyMODINIT_FUNC PyInit_CMel(void) {
    import_array();
    return PyModule_Create(&CMelModule);
}

static PyObject* mel_40(PyObject* self, PyObject* args) {
    /* Input: numpy array (nparrayin)
    *  Output: numpy array (nparrayout)
    *  Function: performs mel-spectrogram transformation of given pcm-style array
    *            Always assumes samplerate=16000, framelength=480, hoplength=240, coefficients=40
    */

    PyArrayObject* nparrayin, * nparrayout;
    int* carrayin, * carrayout;
    int numframes;

    // Check i/o types, throw error if something is wrong
    if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &nparrayin))  {
        PyErr_SetString(PyExc_ValueError, "[mel_40] Input array type mismatch!");
        return NULL;
        }
    
    if (nparrayin == NULL)  {
        PyErr_SetString(PyExc_ValueError, "[mel_40] Input array is NULL");
        return NULL;
        }
    if (!is_intvector(nparrayin)) {
        PyErr_SetString(PyExc_ValueError, "[is_intvector] Input array must be of type int16 or int32");
        return NULL;
        }

    // Copy numpy array to C array
    if (nparrayin->descr->type_num == NPY_INT16) {      // this is usually called
        carrayin = pyarray_int16_to_carrayptr_int32(nparrayin);
    }
    else { // if (nparrayin->descr->type_num == NPY_INT32) 
        carrayin = pyarray_int32_to_carrayptr_int32(nparrayin);
    }
    numframes = calculate_numframes(nparrayin->dimensions[0]);

    if (numframes <= 5) {
        PyErr_SetString(PyExc_ValueError, "[mel_40] Input data is too short");
        return NULL;
    }

    // Operate on arrays here
    carrayout = (int*)malloc((numframes - 5) * 40 * sizeof(int));
    Calculate_Melspectrogram(carrayin, carrayout, numframes - 5);

    // Create nparrayout for outputting to python
    const npy_intp dims[2] = {numframes-5, 40};
    nparrayout = (PyArrayObject*)PyArray_SimpleNewFromData(2, dims, NPY_INT32, carrayout);

    free(carrayin);
    // Ref: https://stackoverflow.com/questions/4657764/py-incref-decref-when
    Py_DECREF(nparrayin);
    return PyArray_Return(nparrayout);
}

int is_intvector(PyArrayObject *inputarray) {
    if (inputarray->descr->type_num != NPY_INT16 && inputarray->descr->type_num != NPY_INT32 || inputarray->nd != 1) return 0;
    return 1;
}

int* pyarray_int16_to_carrayptr_int32(PyArrayObject* inputarray) {
    // (int16) Numpy array -> (int32) C Array
    short* pyarray = (short*)inputarray->data;
    int* carray;
    int i, rows;

    rows = inputarray->dimensions[0];

    carray = (int*)malloc(sizeof(int) * rows);

    for (i = 0; i < rows; i++) {
        carray[i] = (int)pyarray[i];
    }
    return carray;
}

int* pyarray_int32_to_carrayptr_int32(PyArrayObject* inputarray) {
    // (int32) Numpy array -> (int32) C Array
    int* pyarray = (int*)inputarray->data;
    int* carray;
    int i, rows;

    rows = inputarray->dimensions[0];

    carray = (int*)malloc(sizeof(int) * rows);

    for (i = 0; i < rows; i++) {
        carray[i] = pyarray[i];
    }
    return carray;
}

int calculate_numframes(int numdata) {
    return ((numdata - 240) / 240);
}


void Calculate_Melspectrogram(int *inputarray, int *outputarray, int numframes) {
    COMPLEX *fftarray = (COMPLEX*)malloc(257 * sizeof(COMPLEX));
    int* window = (int*)calloc(514, sizeof(int));
    
    inputarray += 480;

    for (int i=0; i<numframes; i++) {
        memcpy(window, inputarray, 480 * sizeof(int));
        inputarray += 240;
        MFCC_Extract(window, fftarray);
        memcpy(outputarray, window, 40 * sizeof(int));
        outputarray += 40;
        
        //Reset fftarray
        memset(fftarray, 0, 257 * sizeof(COMPLEX));
        memset(window, 0, 514 * sizeof(int));
    }

    free(window);
    free(fftarray);
}

Функция вызывается так:

import numpy as np
import scipy.io.wavfile as wavfile
import timeit
from CMel import mel_40

samplerate, data = wavfile.read("./example.wav")

print(timeit.Timer(lambda: mel_40(data)).timeit(number=100))

Код C компилируется с использованием следующего: python3 setup.py install

настройка.py:

import os
import numpy as np
from sysconfig import get_paths
from distutils.core import setup, Extension

python_paths = get_paths()

CMelModule = Extension("CMel", define_macros=[('MAJOR_VERSION', '1'), ("MINOR_VERSION", "0")],
                       include_dirs=["/usr/local/include", os.path.join(np.get_include(), "numpy"), python_paths['include']],
                    #    libraries=["python36"],
                    #    library_dirs=[python_paths['stdlib']],
                       sources=["CMel.c", "melcalculations.c"])


def main():
    setup(name = "CMel",
          version = "1.0.0",
          description = "Python interface for integer-based FFT and Mel Spectogram calculation",
          author = "FB",
          author_email = "[email protected]",
          ext_modules=[CMelModule])


if __name__ == "__main__":
    main()

Обратите внимание: MFCC_Extract() — это место, где происходит извлечение MFCC. Публиковать здесь слишком долго, но я убедился, что он работает правильно, и внутри него не происходит выделения памяти.

Кроме того, как ни странно, я попытался записать содержимое вывода функции mel_40 в файл, и это позволило ей работать после порога сбоя ~ 10.

🤔 А знаете ли вы, что...
Python позволяет создавать сценарии для автоматизации задач и обработки данных.


53
1

Ответ:

Решено

Делать Py_DECREF(nparrayin) в конце mel_40() неправильно. Что касается PyArg_ParseTuple(), в документации расширения Python говорится:

Обратите внимание, что любые ссылки на объекты Python, предоставляемые вызывающей стороне, являются заимствованными ссылками; не уменьшайте их счетчик ссылок!

Вот код, который печатает счетчик ссылок входного массива после каждого вызова mel_40() и выходные данные с decref и без него:

import sys
import numpy as np
from CMel import mel_40

data = np.arange(0, (5 + 5)*240 + 240, dtype=np.int16)
# add a couple extra references
data_1 = data_2 = data

print("initial refcount:", sys.getrefcount(data))
for i in range(5):
    result = mel_40(data)
    print("refcount:", sys.getrefcount(data))

С Py_DECREF(nparrayin):

initial refcount: 4
refcount: 3
refcount: 2
double free or corruption (top)
[1]    753324 IOT instruction (core dumped)  python test.py

Без:

initial refcount: 4
refcount: 4
refcount: 4
refcount: 4
refcount: 4
refcount: 4

Интересные вопросы для изучения

Создайте фабрику виджетов в QtПочему GEKKO не предоставляет оптимальные команды, хотя выходные данные не соответствуют эталонным?Java sshtools сгенерировал подпись EDDSA, не совпадающую с сгенерированной подписью пикриптома PythonКак записать атрибуты каждого экземпляра в переменную класса, при этом родительский класс записывает экземпляры всех классов в одну и ту же переменную?Создание динамического массива numpy с использованием двух существующих массивовКак изменить точность плавающей точки в C во время выполнения?Как изменить точность плавающей точки в C во время выполнения?Как строковый параметр может иметь тип byte * и тип wchar_t * одновременно?Проблемы с чтением и печатью файлов в сборке с использованием функций библиотеки C в 64-разрядной версии WindowsСохранение char в массиве char – несовместимые типы при присвоении char char[]