Замена оси пакетов влияет на производительность в pytorch?

#pytorch #stride

Вопрос:

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

Моя модель вызывает функцию, которая становится проще, если у меня есть другое измерение на первой оси, так что я могу использовать x[k] вместо x[:, k] .

Результаты арифметических операций, по-видимому, сохраняют ту же компоновку памяти

 x = torch.ones(2,3,4).transpose(0,1)
y = torch.ones_like(x)
u = (x   1)
v = (x   y)
print(x.stride(), u.stride(), v.stride())
 

Когда я создаю дополнительные переменные, я создаю их с torch.zeros помощью, а затем транспонирую, чтобы наибольший шаг также шел к оси 1.

напр.

 a,b,c = torch.zeros(
         (3, x.shape[1], ADDITIONAL_DIM, x.shape[0])   x.shape[2:]
).transpose(1,2)
 

Создаст три тензора с одинаковым размером пакета x.shape[1] .
С точки зрения локализации памяти имело бы какое-то значение, если бы

 a,b,c = torch.zeros(
  (x.shape[1], 3, ADDITIONAL_DIM, x.shape[0])   x.shape[2:]
).permute(1,2,0, ...)
 

вместо.

Должен ли я вообще беспокоиться об этом?

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

1. Итак, ваш вопрос заключается в том, есть ли какая-либо разница между выполнением перестановки, а затем работой с x[k] ней ; и работой x[:, k] без необходимости перестановки, верно?

2. Точно, а также для внутренне построенных массивов

Ответ №1:

TLDR; Фрагменты, по-видимому, содержат меньше информации… но на самом деле разделяйте идентичный буфер хранения с исходным тензором. Поскольку перестановка не влияет на базовую компоновку памяти, обе операции по существу эквивалентны.


Эти два по сути одинаковы, базовый буфер хранения данных остается неизменным, меняются только метаданные , т. е. то, как вы взаимодействуете с этим буфером (шаги и форма).

Давайте рассмотрим простой пример:

 >>> x = torch.ones(2,3,4).transpose(0,1)
>>> x_ptr = x.data_ptr()

>>> x.shape, x.stride(), x_ptr
(3, 2, 4), (4, 12, 1), 94674451667072
 

Мы сохранили указатель данных для нашего «базового» тензора в x_ptr :

  1. Нарезка на второй оси:
     >>> y = x[:, 0]
    
    >>> y.shape, y.stride(), x_ptr == y.data_ptr()
    (3, 4), (4, 1), True
     

    Как вы можете видеть, x и x[:, k] разделяли одно и то же хранилище.

  2. Перестановка первых двух осей, а затем нарезка на первой:
     >>> z = x.permute(1, 0, 2)[0]
    
    >>> z.shape, z.stride(), x_ptr == z.data_ptr()
    (3, 4), (4, 1), True
     

    И здесь вы снова замечаете, что x.data_ptr это то же z.data_ptr самое, что и .


На самом деле, вы даже можете перейти от y представления к x представлению с помощью torch.as_strided :

 >>> torch.as_strided(y, size=x.shape, stride=x.stride())
tensor([[[1., 1., 1., 1.],
         [1., 1., 1., 1.]],

        [[1., 1., 1., 1.],
         [1., 1., 1., 1.]],

        [[1., 1., 1., 1.],
         [1., 1., 1., 1.]]])
 

То же самое с z :

 >>> torch.as_strided(z, size=x.shape, stride=x.stride())
 

Оба вернут копию x because torch.as_strided , выделив память для вновь созданного тензора. Эти две строки были просто для иллюстрации того , как мы все еще можем «вернуться» к x фрагменту x , к которому мы можем восстановить видимое содержимое, изменив метаданные тензора.

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

1. Да, я все это знаю, мой вопрос касается производительности. Меня особенно интересует, как pytorch будет распараллеливать операции и производительность передачи памяти с использованием различных компоновок памяти.

2. Ну, я знаю это на первый взгляд, я хочу знать о производительности в распределенных вычислениях, например, в графическом процессоре, без необходимости писать две реализации и бенчмарки

3. В любом из двух методов выполняемые операции будут использовать одинаковую схему хранения памяти, так что да, производительность одинакова.

4. И что лучше-выделять и назначать с помощью срезов a[:] = x y или использовать a = (x y).type(a.dtype) ?

5. Почему бы просто не a = x y сделать это ? Во всяком случае, оба они идентичны.