как spacy-io использует многопоточность без GIL?

Ссылаясь на этот пост Многопоточный НЛП со Spacy pipe, в котором говорится об этом,

а здесь с https://spacy.io/

from spacy.attrs import *
# All strings mapped to integers, for easy export to numpy
np_array = doc.to_array([LOWER, POS, ENT_TYPE, IS_ALPHA])

from reddit_corpus import RedditComments
reddit = RedditComments('/path/to/reddit/corpus')
# Parse a stream of documents, with multi-threading (no GIL!)
# Processes over 100,000 tokens per second.
for doc in nlp.pipe(reddit.texts, batch_size=10000, n_threads=4):
    # Multi-word expressions, such as names, dates etc
    # can be merged into single tokens
    for ent in doc.ents:
        ent.merge(ent.root.tag_, ent.text, ent.ent_type_)
    # Efficient, lossless serialization --- all annotations
    # saved, same size as uncompressed text
    byte_string = doc.to_bytes()

person user2290820    schedule 05.05.2016    source источник


Ответы (2)


Мне нужно написать надлежащую запись в блоге об этом. Суть в том, что spaCy реализован на Cython, языке, похожем на Python, который преобразуется в C или C++ и в конечном итоге создает расширение Python. Подробнее о выпуске GIL с Cython можно прочитать здесь:

http://docs.cython.org/src/userguide/parallelism.html

Вот реализация метода .pipe в spaCy:

https://github.com/spacy-io/spaCy/blob/master/spacy/syntax/parser.pyx#L135

def pipe(self, stream, int batch_size=1000, int n_threads=2):
    cdef Pool mem = Pool()
    cdef TokenC** doc_ptr = <TokenC**>mem.alloc(batch_size, sizeof(TokenC*))
    cdef int* lengths = <int*>mem.alloc(batch_size, sizeof(int))
    cdef Doc doc
    cdef int i
    cdef int nr_class = self.moves.n_moves
    cdef int nr_feat = self.model.nr_feat
    cdef int status
    queue = []
    for doc in stream:
        doc_ptr[len(queue)] = doc.c
        lengths[len(queue)] = doc.length
        queue.append(doc)
        if len(queue) == batch_size:
            with nogil:
                for i in cython.parallel.prange(batch_size, num_threads=n_threads):
                    status = self.parseC(doc_ptr[i], lengths[i], nr_feat, nr_class)
                    if status != 0:
                        with gil:
                            sent_str = queue[i].text
                            raise ValueError("Error parsing doc: %s" % sent_str)
            PyErr_CheckSignals()
            for doc in queue:
                self.moves.finalize_doc(doc)
                yield doc
            queue = []
    batch_size = len(queue)
    with nogil:
        for i in cython.parallel.prange(batch_size, num_threads=n_threads):
            status = self.parseC(doc_ptr[i], lengths[i], nr_feat, nr_class)
            if status != 0:
                with gil:
                    sent_str = queue[i].text
                    raise ValueError("Error parsing doc: %s" % sent_str)
    PyErr_CheckSignals()
    for doc in queue:
        self.moves.finalize_doc(doc)
        yield doc

Фактическая механика многопоточности очень проста, потому что NLP (часто) смущающе параллелен — каждый документ анализируется независимо, поэтому нам просто нужно сделать цикл prange для потока текстов.

Однако реализовать парсер многопоточным способом было довольно сложно. Чтобы эффективно использовать многопоточность, вам нужно выпустить GIL, а не получать его заново. Это означает не использовать объекты Python, не создавать исключения и т. д.

Когда вы создаете объект Python -- скажем, список --- вам нужно увеличить его счетчик ссылок, который хранится глобально. Это означает приобретение GIL. Нет никакого способа обойти это. Но если вы используете расширение C и хотите просто, скажем, поместить целое число в стек или вызвать malloc или free, вам не нужно приобретать GIL. Поэтому, если вы пишете программу на этом уровне, используя только конструкции C и C++, вы можете выпустить GIL.

Я пишу статистические парсеры на Cython уже несколько лет. (До spaCy у меня была реализация для моего академического исследования.) Было трудно написать весь цикл синтаксического анализа без GIL. К концу 2015 года у меня было машинное обучение, хеш-таблица, внешний цикл синтаксического анализа и большая часть извлечения функций в виде кода nogil. Но объект состояния имел сложный интерфейс и был реализован как класс cdef. Я не мог создать этот объект или сохранить его в контейнере, не приобретя GIL.

Прорыв произошел, когда я придумал недокументированный способ написания класса C++ на Cython. Это позволило мне очистить существующий класс cdef, который контролировал состояние парсера. Я передал его интерфейс внутреннему классу C++, метод за методом. Таким образом, я мог поддерживать работу кода и следить за тем, чтобы не вносить какие-либо незначительные ошибки в расчет функции.

Вы можете увидеть внутренний класс здесь: https://github.com/spacy-io/spaCy/blob/master/spacy/syntax/_state.pxd

Если вы просмотрите историю git этого файла, вы увидите патчи, в которых я реализовал метод .pipe.

person syllogism_    schedule 05.05.2016

Предположительно, он выполняет синтаксический анализ на уровне C, а не на уровне Python. Как только вы перейдете в C, если вам не нужен доступ к каким-либо объектам python, вы можете безопасно выпустить GIL. На самом низком уровне чтения и записи CPython также выпускает GIL. Причина в том, что если есть другие запущенные потоки, и мы собираемся вызвать блокирующую функцию C, мы должны освободить GIL на время вызова функции.

Вы можете увидеть это в действии на самой низкой реализации CPython write.

    if (gil_held) {
        do {
            Py_BEGIN_ALLOW_THREADS
            errno = 0;
#ifdef MS_WINDOWS
            n = write(fd, buf, (int)count);
#else
            n = write(fd, buf, count);
#endif
            /* save/restore errno because PyErr_CheckSignals()
             * and PyErr_SetFromErrno() can modify it */
            err = errno;
            Py_END_ALLOW_THREADS
        } while (n < 0 && err == EINTR &&
                !(async_err = PyErr_CheckSignals()));
person Dunes    schedule 05.05.2016