Потоки Python не полностью используют ядра ЦП

Я пишу сценарий для задания, и из-за характера вычислений на моем ноутбуке он выполняется довольно медленно. Поэтому я попытался разделить основной цикл for на столько частей, сколько у меня есть ядер ЦП, но при работе каждое ядро ​​используется только на 25%. По сути, мой вопрос таков: как я могу заставить Python использовать весь процессор?

Код выглядит следующим образом:

import numpy as np
import matplotlib.pyplot as plt

import os
import threading as thr
plt.close('all')

os.system("sudo renice -n -19 -p " + str(os.getpid()))

sinogram = np.random.rand((120, 240)) # for illustration purposes

N = np.shape(sinogram)[1] # width
M = np.shape(sinogram)[0]

threads = []
nthreads = 8
results = [None] * nthreads

for i in range(nthreads):
    srange = range(int((i)*M/nthreads), int((i+1)*M/nthreads))
    t = thr.Thread(target=backproject_threaded, args=(sinogram,srange,results,i))
    print("started thread " + str(i) + " with srange " + str(srange))
    t.start()
    threads.append(t)

backarray = np.zeros([N,N])
for i in range(nthreads):
    threads[i].join()
    backarray += results[i]
        
plt.imshow((backarray), cmap='gray')

с backproject_threaded:

def backproject_threaded(sinogram, srange, results, index): 
    N = np.shape(sinogram)[1] # width
    M = np.shape(sinogram)[0] # height (number of projections)
    backarray = np.zeros([N,N])

    for s in srange:
        angle = np.pi*s/M # angle of axis
        for y in range(N):
            for x in range(N):
                # project coordinate onto rotating axis
                z = np.cos(angle) * (x-N/2) + np.sin(angle) * (y-N/2) # axis-coordinate
                # select the closest coordinate in sinogram(s, :)
                sn = max(0, min(int((z+N/2)), N-1))
                # add this value to backarray value
                backarray[N-y-1, x] += sinogram[s,sn]
    results[index] = backarray
    print("thread " + str(index) + " stopped")
    return backarray

Вычисление дает ожидаемый результат, но вот что я вижу в htop: вид на htop, показывающий, что ядра находятся на уровне 20%

Как я могу сделать это быстрее?

Многопоточность должна быть очень простой, потому что потоки не пишут в один и тот же объект, а в противном случае читают только из синограммы.

Как видите, я попробовал установить приятность, но это не дало никакого эффекта. Еще я попробовал это os.environ["OPENBLAS_MAIN_FREE"] = "1", но это тоже не помогает.

Я надеялся, что с 8 потоками (мое количество ядер) он будет работать в восемь раз быстрее, но ядра не используются полностью. Также я пытался закомментировать все утверждения print, но это тоже ничего не дало.

Масштабирование работы, связанной с процессором, через потоки не будет работать в Python из-за GIL. Вы можете попробовать использовать альтернативные подходы, например отдельные процессы (вместо потоков)

Sergio Tulentsev 21.05.2024 11:34

Для начала ваш код можно векторизовать. Векторизация не означает, что она работает в //. Но, во-первых, это гораздо больший источник повышения производительности, чем ваши 8 потоков (для некоторого кода вы можете получить x1000 с векторизацией. Когда вы не ожидаете большего, чем x8 с 8-поточным распараллеливанием). И, во-вторых, если он не векторизован, нет никаких шансов, что поток OPENBLAS не сможет помочь: как есть, вы просите numpy выполнять операции одну за другой. В то время как, если бы вы попросили его выполнить их пакетно, он мог бы решить сделать это параллельно.

chrslg 21.05.2024 11:49

Таким образом, это вовсе не ответ на вопрос «как использовать потоки для распараллеливания в Python» (короче говоря, ответ на этот вопрос: «Вы этого не делаете»). Либо используйте многопроцессорность, либо вы используете поток на другом языке, например использование ctypes для написания многопоточного кода C, вызываемого из Python). Но поскольку ваша реальная цель — ускорить код, первый шаг — не использовать потоки. Первый шаг — удалить эти двойные циклы Python for и векторизовать ваш код. Вы получите больше ожидаемого в 8 раз. И, возможно, numpy даже будет использовать для вас потоки

chrslg 21.05.2024 11:52

Обратите внимание, что Numpy обычно намного медленнее работает в скалярном режиме, чем Python. На самом деле Numpy предназначен не для этого, а для относительно больших массивов. Накладные расходы на вызовы функций Numpy огромны (например, 0,5–5 мкс) по сравнению со временем вычислений, необходимым для выполнения операции с небольшим массивом (даже дорогостоящие операции cos в настоящее время обычно требуют не более десятков ns на элемент, а в настоящее время значительно меньше). попрактикуйтесь на последних процессорах с хорошей серверной математической библиотекой x86-64, такой как SVML). По этой причине для эффективного использования вычислительной мощности ЦП массивы обычно должны состоять как минимум из сотен элементов.

Jérôme Richard 21.05.2024 12:05

Как мне его векторизовать, если мне также нужна информация об индексе?

Simon Innerbichler 21.05.2024 17:03
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
5
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как правило, многопоточность не очень подходит для интенсивной работы процессора. Это лучше всего подходит для операций, связанных с вводом-выводом. Для высокой загрузки ЦП обычно используется многопроцессорность.

Я удалил ненужные части вашего кода, чтобы продемонстрировать шаблон, который можно использовать.

При этом не учитывается, подходит ли (или нет) numpy в этом случае использования:

import numpy as np
from concurrent.futures import ProcessPoolExecutor, as_completed
import os
import time


def backproject_process(sinogram: np.ndarray, srange: range) -> np.ndarray:
    M, N = np.shape(sinogram)
    backarray = np.zeros([N, N])
    for s in srange:
        angle = np.pi * s / M  # angle of axis
        for y in range(N):
            for x in range(N):
                # project coordinate onto rotating axis
                z = np.cos(angle) * (x - N / 2) + np.sin(angle) * (
                    y - N / 2
                )  # axis-coordinate
                # select the closest coordinate in sinogram(s, :)
                sn = max(0, min(int((z + N / 2)), N - 1))
                # add this value to backarray value
                backarray[N - y - 1, x] += sinogram[s, sn]
    print(f"Process {os.getpid()} completed")
    return backarray


if __name__ == "__main__":
    sinogram = np.random.rand(120, 240)  # for illustration purposes

    M, N = np.shape(sinogram)

    nprocs = 8
    start = time.time()
    with ProcessPoolExecutor() as exe:
        futures = []
        for i in range(nprocs):
            srange = range(int(i * M / nprocs), int((i + 1) * M / nprocs))
            future = exe.submit(backproject_process, sinogram, srange)
            futures.append(future)
        backarray = np.zeros([N, N])
        for f in as_completed(futures):
            backarray += f.result()
    end = time.time()
    print(f"Duration = {end-start:.2f}s")

Выход:

Process 5606 completed
Process 5608 completed
Process 5604 completed
Process 5609 completed
Process 5605 completed
Process 5610 completed
Process 5603 completed
Process 5607 completed
Duration = 2.42s

Платформа:

macOS Sonoma 14.5
Apple Silicon M2 (effectively 8 CPUs)
Python 3.12.3

Вы можете уточнить, что ваше общее правило специфично для CPython.

Jeremy Friesner 21.05.2024 14:29

Другие вопросы по теме