arutunyan_dmitry_lab_5 is ready #19
173
arutunyan_dmitry_lab_5/README.md
Normal file
173
arutunyan_dmitry_lab_5/README.md
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
## Лабораторная работа 5. Вариант 4.
|
||||
### Задание
|
||||
Реализовать умножение двух больших квадратных матриц
|
||||
|
||||
- Создать алгоритм параллельного умножения матриц,
|
||||
- Предусмотреть задание потоков вручную, для работы параллельного алгоритма умжожения как обычного.
|
||||
|
||||
### Как запустить
|
||||
Для запуска программы необходимо с помощью командной строки в корневой директории файлов прокета прописать:
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
Результат работы программы будет выведен в консоль.
|
||||
|
||||
### Используемые технологии
|
||||
- Библиотека `numpy`, используемая для обработки массивов данных и вычислений.
|
||||
- Библиотека `concurrent.futures`- высокоуровневый интерфейс для выполнения параллельных и асинхронных задач.
|
||||
- `ThreadPoolExecutor` - класс пула потоков для выполнения задач в нескольких потоках. Он использует пул потоков, чтобы автоматически управлять созданием и выполнением потоков, что обеспечивает простой способ распараллеливания задач. Метод `submit()` позволяет отправлять задачи в пул потоков, и возвращает объект `Future`, который представляет результат выполнения задачи.
|
||||
- Библиотека `time`, используемая для измерения времени работы программы.
|
||||
- Библиотека `psutil`, используемая для отслеживания нагрузки на процессор и количества загруженной оперативной памяти.
|
||||
|
||||
### Описание работы
|
||||
#### Распараллеливание задачи перемножения матриц
|
||||
Возьмём несколько базовых правил из высшей математики:
|
||||
- Умножение матриц выполнимо, если число столбцов 1го множителя равно числу строк 2го множителя.
|
||||
- При умножении матрицы размерностью `[m, n]`, на матрицу размерностью `[n, m]` матрица-результат будет иметь размерность `[n, n]`.
|
||||
- Умножение матриц не подчиняется переместительному закону умножения.
|
||||
|
||||
Возьмём пример умножения двух матриц:
|
||||
```
|
||||
[[1, 2, 3], [[7, 8], [[58, 64],
|
||||
4, 5, 6]] x [9, 10], = [139, 154]]
|
||||
[11, 12]]
|
||||
```
|
||||
Чтобы безболезненно разделить данную операцию на несколько, можно брать каждую строку 1й матрицы и умножать её на 2ю матрицу. а после собрать полученные промежуточные матрицы в матрицу-результат:
|
||||
|
||||
```
|
||||
[[7, 8],
|
||||
[[1, 2, 3]] x [9, 10], = [[58, 64]]
|
||||
[11, 12]]
|
||||
```
|
||||
```
|
||||
[[7, 8],
|
||||
[[4, 5, 6]] x [9, 10], = [[139, 154]]
|
||||
[11, 12]]
|
||||
```
|
||||
Таким образом, мы можем параллельно умножать все строки 1й матрицы на 2ю матрицу, и, собрав полученные строки-результаты, получить матрицу-результат.
|
||||
|
||||
#### Разработка программы
|
||||
Для начала создадим функцию умножения строки матрицы на матрицу:
|
||||
```python
|
||||
def multiply_row(row, matrix_b):
|
||||
return np.dot(row, matrix_b)
|
||||
```
|
||||
Теперь перейдём к алгоритму параллельного умножения матриц. Для начала найдём сначения строк и столбцов обеих матриц и проверим по 1му правилу, возможно ли их умножить:
|
||||
```python
|
||||
num_rows_a, num_cols_a = matrix_a.shape
|
||||
num_rows_b, num_cols_b = matrix_b.shape
|
||||
assert num_cols_a == num_rows_b, "Размеры матриц несовместимы для перемножения"
|
||||
```
|
||||
После этого, создадим пул потоков с помощью класса `concurrent.futures.ThreadPoolExecutor`, в который будем передавать желаемое количество потоков:
|
||||
```python
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
|
||||
results = []
|
||||
for i in range(num_rows_a):
|
||||
result = executor.submit(multiply_row, matrix_a[i], matrix_b)
|
||||
results.append((i, result))
|
||||
sorted_results = sorted(results, key=lambda x: x[0])
|
||||
result_matrix = np.vstack([result.result() for _, result in sorted_results])
|
||||
```
|
||||
В пул потоков в качестве задач мы передаём последовательно строки 1й матрицы и 2ю матрицу, а в качестве их обработчика указываем ранее созданный метод `multiply_row`. Результаты выполнения задач в пуле потоков могут возвращаться в произвольном порядке, поэтому для каждого ожидаемого результата зададим индекс. После этого. массив строк-результатов отсортируем по индексу и последовательно вложим в матрицу-результат методом `vstack`.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> Поскольку умножение матриц распараллеливается по строкам 1й матрицы, задавать значение кол-ва потоков больше числа строк 1й матрицы не имеет значения. По сути, самым оптимальным решение мудет являться состояние, когда каждый поток умножает свою строку 1й матрицы на 2ю матрицу.
|
||||
|
||||
Создадим тестовый метод и проверим работу калькулятора матриц на грамотность вычислений:
|
||||
```python
|
||||
a = np.array([[1, 2, 3], [4, 5, 6]])
|
||||
b = np.array([[7, 8], [9, 10], [11, 12]])
|
||||
return parallel_matrix_multiplication(a, b, num_threads=1)
|
||||
```
|
||||
Результат вычислений:
|
||||
```
|
||||
[[58, 64]
|
||||
[139, 154]]
|
||||
```
|
||||
Теперь поменяем местами матрицы a и b и проверим результат:
|
||||
```
|
||||
[[39, 54, 69]
|
||||
[49, 68, 87]
|
||||
[59, 82, 105]]
|
||||
```
|
||||
Алгоритм работает верно.
|
||||
|
||||
#### Сбор данных о вычислениях. Бенчмарки.
|
||||
|
||||
Теперь соберём некотрорую статистику о вычислениях. Будем собирать время вычислений, загруженность ЦП и загруженность ОЗУ.
|
||||
|
||||
Время работы:
|
||||
```python
|
||||
start_time = time.time()
|
||||
|
||||
# Вычисления тут
|
||||
|
||||
execution_time = end_time - start_time
|
||||
return execution_time
|
||||
```
|
||||
Загруженность ЦП:
|
||||
```python
|
||||
psutil.cpu_percent(interval=interval)
|
||||
```
|
||||
Где `interval` - время измерений в секундах.
|
||||
|
||||
Загруженность ОЗУ:
|
||||
```python
|
||||
memory = psutil.virtual_memory()
|
||||
|
||||
# Вычисления тут
|
||||
|
||||
return memory.percent
|
||||
```
|
||||
Теперь создадим бенчмарки. Первым будет бенчмарк умножения 2х матриц, размерностью 100 * 100 с рандомным заполнением числами от 0 до 100 (остальные бенчмарки будут создаваться по аналогии, меняться будет только размерность):
|
||||
```python
|
||||
def bench100x100(parallel):
|
||||
a = np.random.randint(0, 100, size=(100, 100))
|
||||
b = np.random.randint(0, 100, size=(100, 100))
|
||||
if parallel:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=100, interval=1)
|
||||
else:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=1, interval=1)
|
||||
print("Результат умножения:")
|
||||
print(result[0])
|
||||
print("Время выполнения: " + str(result[1]) + " сек.")
|
||||
print("Загрузка ЦП: " + str(result[2]) + "%")
|
||||
print("Использование ОЗУ: " + str(result[3]) + "%")
|
||||
```
|
||||
|
||||
#### Замеры параметров
|
||||
Прогоним все бенчмарки и сравним измеряемые показатели при максимальной многопоточности (кол-во потоков = кол-ву строк матрицы) и монопоточности.
|
||||
|
||||
Результаты (параллельный - справа, обычный - слева):
|
||||
```
|
||||
100 * 100
|
||||
_________________________________________________________________
|
||||
|
||||
Время выполнения: 0.0099.. сек. | Время выполнения: 0.0110.. сек.
|
||||
Загрузка ЦП: 0.8% | Загрузка ЦП: 4.1%
|
||||
Использование ОЗУ: 64.0% | Использование ОЗУ: 64.1%
|
||||
|
||||
300 * 300
|
||||
_________________________________________________________________
|
||||
|
||||
Время выполнения: 0.0369.. сек. | Время выполнения: 0.0340.. сек.
|
||||
Загрузка ЦП: 0.6% | Загрузка ЦП: 1.2%
|
||||
Использование ОЗУ: 63.5% | Использование ОЗУ: 63.6%
|
||||
|
||||
500 * 500
|
||||
_________________________________________________________________
|
||||
|
||||
Время выполнения: 0.1360.. сек. | Время выполнения: 0.0529.. сек.
|
||||
Загрузка ЦП: 0.6% | Загрузка ЦП: 1.7%
|
||||
Использование ОЗУ: 62.6% | Использование ОЗУ: 62.9%
|
||||
```
|
||||
|
||||
### Вывод
|
||||
По результатам замеров видно, что до матриц с размерностью 100 наиболее быстро обрабатывались вычисления в одном потоке, при размерности матриц в 300 многопоточные вычисления начали незначительно лидировать по скорости, а при размерности матриц в 500 превзошли однопоточные в скорости более чем в два раза. По нагрузке процессора, многопоточные вычисления всегда нагружали его чуть больше, загруженность ОЗУ оставатась примерно одинаковый во всех экспериментах.
|
||||
|
||||
Получается, можно сделать вывод, что параллельные вычисления ресурсозатратнее, чем обычные и работают эффективнее последних только при достаточно больших наборах данных.
|
||||
|
||||
### Видео
|
||||
https://youtu.be/A8aYkuwn4yU
|
92
arutunyan_dmitry_lab_5/main..py
Normal file
92
arutunyan_dmitry_lab_5/main..py
Normal file
@ -0,0 +1,92 @@
|
||||
import numpy as np
|
||||
import concurrent.futures
|
||||
import time
|
||||
import psutil
|
||||
|
||||
|
||||
def multiply_row(row, matrix_b): # Функция для умножения строки матрицы A на матрицу B
|
||||
return np.dot(row, matrix_b)
|
||||
|
||||
|
||||
def parallel_matrix_multiplication(matrix_a, matrix_b, num_threads, interval):
|
||||
memory = psutil.virtual_memory()
|
||||
num_rows_a, num_cols_a = matrix_a.shape
|
||||
num_rows_b, num_cols_b = matrix_b.shape
|
||||
assert num_cols_a == num_rows_b, "Размеры матриц несовместимы для перемножения"
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: # Отправляем задачи в пул потоков
|
||||
start_time = time.time()
|
||||
results = []
|
||||
for i in range(num_rows_a):
|
||||
result = executor.submit(multiply_row, matrix_a[i], matrix_b)
|
||||
results.append((i, result))
|
||||
sorted_results = sorted(results, key=lambda x: x[0])
|
||||
result_matrix = np.vstack(
|
||||
[result.result() for _, result in sorted_results]) # Получаем результаты выполнения задач
|
||||
end_time = time.time()
|
||||
|
||||
execution_time = end_time - start_time
|
||||
return result_matrix, execution_time, psutil.cpu_percent(interval=interval), memory.percent
|
||||
|
||||
|
||||
def test(parallel):
|
||||
a = np.array([[1, 2, 3], [4, 5, 6]])
|
||||
b = np.array([[7, 8], [9, 10], [11, 12]])
|
||||
if parallel:
|
||||
result = parallel_matrix_multiplication(b, a, num_threads=2, interval=0.5)
|
||||
else:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=1, interval=0.5)
|
||||
print("Результат умножения:")
|
||||
print(result[0])
|
||||
print("Время выполнения: " + str(result[1]) + " сек.")
|
||||
print("Загрузка ЦП: " + str(result[2]) + "%")
|
||||
print("Использование ОЗУ: " + str(result[3]) + "%")
|
||||
|
||||
|
||||
def bench100x100(parallel):
|
||||
a = np.random.randint(0, 100, size=(100, 100))
|
||||
b = np.random.randint(0, 100, size=(100, 100))
|
||||
if parallel:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=100, interval=1)
|
||||
else:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=1, interval=1)
|
||||
print("Результат умножения:")
|
||||
print(result[0])
|
||||
print("Время выполнения: " + str(result[1]) + " сек.")
|
||||
print("Загрузка ЦП: " + str(result[2]) + "%")
|
||||
print("Использование ОЗУ: " + str(result[3]) + "%")
|
||||
|
||||
|
||||
def bench300x300(parallel):
|
||||
a = np.random.randint(0, 100, size=(300, 300))
|
||||
b = np.random.randint(0, 100, size=(300, 300))
|
||||
if parallel:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=300, interval=1)
|
||||
else:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=1, interval=1)
|
||||
print("Результат умножения:")
|
||||
print(result[0])
|
||||
print("Время выполнения: " + str(result[1]) + " сек.")
|
||||
print("Загрузка ЦП: " + str(result[2]) + "%")
|
||||
print("Использование ОЗУ: " + str(result[3]) + "%")
|
||||
|
||||
|
||||
def bench500x500(parallel):
|
||||
a = np.random.randint(0, 100, size=(500, 500))
|
||||
b = np.random.randint(0, 100, size=(500, 500))
|
||||
if parallel:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=500, interval=1)
|
||||
else:
|
||||
result = parallel_matrix_multiplication(a, b, num_threads=1, interval=1)
|
||||
print("Результат умножения:")
|
||||
print(result[0])
|
||||
print("Время выполнения: " + str(result[1]) + " сек.")
|
||||
print("Загрузка ЦП: " + str(result[2]) + "%")
|
||||
print("Использование ОЗУ: " + str(result[3]) + "%")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test(parallel=True)
|
||||
# bench100x100(parallel=True)
|
||||
# bench300x300(parallel=True)
|
||||
# bench500x500(parallel=True)
|
Loading…
Reference in New Issue
Block a user