Документация Engee
Notebook

Перемножение матриц на разных вычислителях

В этом примере мы покажем, как изучить производительность кода при помощи инструментария BenchmarkTools и сравним его выполнение на CPU и на GPU.

Подключение к GPU

Наличие у пользователей ресурсов GPU пока остается премиум-функцией платформы Engee. GPU – графическая видеокарта, позволяющая существенно распараллелить выполнение кода путем его выполнения на десятках тысячах вычислительных ядер, находящихся внутри графического сопроцессора.

Основной библиотекой для работы с GPU является CUDA.jl (есть и другие, не такие стабильные). Установим эту библиотеку, а вместе с ней – инструментарий для оценки производительности кода.

In [ ]:
# Закомментируйте эту строку, если будете устанавливать библиотеки другим образом
using Pkg; Pkg.add( ["CUDA", "BenchmarkTools"] ); # <- может занять много времени

Перемножение матриц на CPU

Посмотрим, сколько времени в среднем занимает перемножение матриц на обычном процессоре.

In [ ]:
N = 1_000

A = rand(ComplexF64, (N,N))
B = rand(ComplexF64, (N,N))

using BenchmarkTools
@benchmark A*B
Out[0]:
BenchmarkTools.Trial: 16 samples with 1 evaluation.
 Range (minmax):  289.649 ms399.678 ms   GC (min … max): 0.00% … 0.00%
 Time  (median):     302.302 ms                GC (median):    0.00%
 Time  (mean ± σ):   324.611 ms ±  43.508 ms   GC (mean ± σ):  0.07% ± 0.22%

       █▄                                                    ▁  
  ▆▁▁▁▁██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆█ ▁
  290 ms           Histogram: frequency by time          400 ms <

 Memory estimate: 15.26 MiB, allocs estimate: 2.

Выполнение этой ячейки заняло довольно много времени, поскольку команда @benchmark запускает переданную ей операцию много тысяч раз, чтобы исключить эффект "разогрева", присущий Julia. Иногда это также сглаживает влияние плохого стечения обстоятельств, когда код показывает минимально возможную производительность.

В данном конкретном случае показал, что перемножение матриц комплексных чисел размером 1000 на 1000 в среднем занимает 300 миллисекунд.

Перемножение матриц на GPU

Чтобы перемножить матрицы на видеокарте их нужно перенести на нее, что можно сделать множеством способов. Например, командой A |> gpu, но поскольку в системе может не оказаться GPU, мы проверим конфигурацию вычислительного пространства и выберем доступный вычислитель.

После переноса матрицы Matrix теперь являются объектами CuArray. Их перемножение между собой выполняется без дополнительного кода (благодаря перегрузке оператора умножения). Но умножить матрицу A_gpu на матрицу B мы не сможем без переноса обеих матриц на один и тот же вычислитель (иначе вы получите ошибку KernelError: kernel returns a value of type Union{}).

In [ ]:
using CUDA

if CUDA.functional()
    A_gpu = A |> gpu
    B_gpu = B |> gpu
    @benchmark A_gpu * B_gpu
end
Out[0]:
BenchmarkTools.Trial: 1853 samples with 1 evaluation.
 Range (minmax):  19.457 μs  2.108 s   GC (min … max): 0.00% … 0.23%
 Time  (median):     22.032 μs               GC (median):    0.00%
 Time  (mean ± σ):    3.430 ms ± 84.599 ms   GC (mean ± σ):  0.22% ± 0.01%

  ▄▇▆▃▄█▅▁                                     ▁▁     ▁▂     ▁
  █████████▆▆▅▅▆▇▇▆▆▆▅▅▆▅▁▅▄▄▄▁▁▁▁▁▁▁▁▄▁▁▄▁▁▁▁▆███▆▅▅▆███▇▅▄ █
  19.5 μs      Histogram: log(frequency) by time      45.9 μs <

 Memory estimate: 800 bytes, allocs estimate: 35.

На GPU A100 перемножение тех же матриц занимает около 20 микросекунд, то есть операция выполнилась в 15 000 раз быстрее, чем на CPU.

Заключение

Julia позволяет переносить вычисления на GPU и многие библиотеки поддерживают этот способ работы. Мы выполнили перемножение матриц на процессоре и на графической видеокарте и определили, что матрицы квадратные матрицы с размером стороны 1000, состоящие из случайных комплексных чисел, графическая видеокарта перемножила в 15 000 раз быстрее, чем процессор.