Мне нужно написать надлежащую запись в блоге об этом. Суть в том, что 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