Почему установка значения на GPU замедляет вычисления?

#python #tensorflow #pytorch #gpu

Вопрос:

Я новичок в распараллеливании GPU. Я обнаружил, что предварительное размещение значений на GPU замедляет вычисления и индексацию. Мой код выглядит следующим образом:

 import torch

A = torch.rand(600, 600, device='cuda:0')
row0 = torch.tensor(100, device='cuda:0')
col0 = torch.tensor(100, device='cuda:0')
row1 = torch.tensor(356, device='cuda:0')
col1 = torch.tensor(356, device='cuda:0')
B = torch.rand(256, 256, device='cuda:0')
a = 10

%timeit B[:] = A[row0:row1, col0:col1]
# 395 µs ± 4.01 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit a*A   a**2
# 17 µs ± 256 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

A = torch.rand(600, 600, device='cuda:0')
row0 = 100
col0 = 100
row1 = 356
col1 = 356
B = torch.rand(256, 256, device='cuda:0')
a1 = torch.as_tensor(a).cuda()

%timeit B[:] = A[row0:row1, col0:col1]
# 10.6 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit a1*A   a1**2
# 30.2 µs ± 584 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
 

Может ли кто-нибудь объяснить механизм, стоящий за этим?

Ответ №1:

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

Параллелизм GPU работает за счет запуска большого количества потоков и одновременного вычисления фрагментов некоторой операции. Такие вещи, как умножение матриц и свертка, очень удобны для графического процессора, потому что вы можете разбить их на множество подобных небольших операций.

Однако при выполнении задач на графическом процессоре также возникают накладные расходы.

Только когда было запущено достаточное количество потоков, чтобы преодолеть накладные расходы CUDA, мы наблюдаем ускорение. Давайте рассмотрим пример:

 import torch

device = torch.device('cuda:0')

A = torch.randn(5, 10, device=device)
B = torch.randn(10, 5, device=device)
A_ = torch.randn(5, 10)
B_ = torch.randn(10, 5)

%timeit A @ B
# 10.5 µs ± 745 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit A_ @ B_
# 5.21 µs ± 120 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Вы можете подумать, что это противоречит здравому смыслу-как умножение матриц процессора может быть быстрее, чем его аналог на GPU? Это просто потому, что у нас не было достаточно большой операции для распараллеливания. Давайте попробуем повторить то же самое, но на больших входах:

 A = torch.randn(100, 200, device=device)
B = torch.randn(200, 100, device=device)
A_ = torch.randn(100, 200)
B_ = torch.randn(200, 100)

%timeit A @ B
# 10.4 µs ± 333 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit A_ @ B_
# 45.3 µs ± 647 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
 

Мы увеличили размер входных данных в 20 раз. Графический процессор по-прежнему в основном показывает нам то же время, накладные расходы, в то время как процессорное время увеличилось. Поскольку входные данные были больше, параллелизм GPU может показать свою магию.

В вашем случае вы вообще не проводите никакого распараллеливания. Вы просто пытаетесь разрезать свой тензор с помощью скаляра GPU, тем самым получая какие-то накладные расходы, но никаких преимуществ. Аналогичный случай в другой операции: распараллеливать нечего.

 %timeit a**2
# 200 ns ± 11.9 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a1**2
# 16.8 µs ± 1.43 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Невозможно разбить операцию a1 ** 2 на более мелкие повторяющиеся фрагменты. Очень важно знать, когда и когда не следует использовать графический процессор. Это также может быть полезной отправной точкой, чтобы получить представление о том, как CUDA работает под капотом.

Комментарии:

1. Спасибо за ваше подробное объяснение. На мой взгляд, при выполнении операции среза GPU необходимо получить индексы. Если индексы находятся на хосте, их сначала нужно перенести на устройство, что, как я думал, замедлит нарезку. Однако результат прямо противоположный. Вот чего я не могу понять.