Pytorch: [RFC] Поддержка формата памяти (он же раскладка NHWC)

Созданный на 10 апр. 2019  ·  68Комментарии  ·  Источник: pytorch/pytorch

Постановка задачи

Операторы CNN используют канонический порядок тензорных измерений и приписывают им семантическое значение. Для 2D-случая в PyTorch сегодня вход в torch.nn.Conv2d должен быть тензором 4d в порядке NCHW -.

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

Примеры библиотек, использующих его, включают:

  • cudnn имеет более высокую производительность на Вольте в NHWC
  • fbgemm и qnnpack не поддерживают NCHW.
  • libxsmm поддерживает NCHW, но снижение производительности составляет примерно 50% (IIRC).

Проблема заключается в том, что преобразование самого порядка измерений является дорогостоящим, поэтому в случаях, когда несколько операций CNN выполняются в строке (например, conv(relu(conv))) ), полезно один раз преобразовать в другой формат памяти, выполнить операции и переупорядочить их. назад.

Таким образом, важно, чтобы PyTorch знал о разных порядках измерений и мог передавать тензоры с разными форматами памяти между операциями как в режиме ожидания, так и в режиме JIT. Кроме того, полезно иметь автоматические проходы JIT-оптимизации, которые пытаются применить эвристику или методы поиска, чтобы выяснить, полезно ли изменение формата памяти с точки зрения производительности и где в модели это имеет смысл.

Мы стремимся создать API, способный представлять:

  • Тензор с другим форматом памяти (вначале просто порядок измерений) присутствует в PyTorch в Eager и JIT. Заблокированные макеты имеют более низкий приоритет, но все равно хороши.
  • Доступные пользователю API для запросов и изменения формата памяти
  • Основные операции CNN могут обрабатывать входные тензоры с различным форматом памяти и маршрутизацией для соответствующей более быстрой реализации
  • Возможность делать выводы и оптимизировать форматы памяти в JIT-проходах

Терминология : указанная выше проблема часто упоминается как «layout» (mxnet), «data_format» (tf), «image_format» (keras), «order» (caffe2). Мы предлагаем использовать в PyTorch название «формат памяти» или «memory_format». Имя «макет», к сожалению, используется в PyTorch со значениями 'strided' и 'sparse_coo', поэтому такая возможность именования недоступна.

Пострадавшие операторы

Следующие операторы, как минимум, должны учитывать формат памяти. Помимо получения правильного результата, они должны обеспечивать максимальную производительность от базовых библиотек И сохранять формат памяти выходных данных, чтобы распространять явно заданное намерение пользователя.

  • свертка
  • различные виды пулинга
  • норма партии, норма слоя, норма экземпляра (как правило, любые нормы)
  • повышающая дискретизация / интерполяция
  • выпадение функции
  • softmax в меньшей степени - размерность может быть указана там вручную, но эффективные реализации присутствуют только для неявной компоновки nchw
  • набивка
  • поэлементные (унарные и бинарные) операции
  • конструкторы тензоров, наследующие формат памяти, например empty_like.

API и изменения поведения

Определите концепцию формата памяти в PyTorch:

  • Константы типа torch.memory_format.channels_first . Они не имеют указанного типа и могут быть произвольными сравнимыми объектами (вероятно, начинаются с перечисления, но в будущем могут быть другие объекты для взаимодействия с концепцией именованного тензора)

    • Альтернатива: использовать torch.channels_first напрямую

  • Значения: channels_first и channels_last (для уменьшения количества констант).
  • Для 1D изображений / 3D-тензоров значения означают NCW, NWC, для 2D-изображений / 4D-тензоров - NCHW, NHWC, для 3D-изображений / 5D-тензоров - NCDHW, NDHWC

Добавьте в Tensor следующие методы:

  • x.is_contiguous(torch.memory_format.channels_first)
  • x.to(memory_format=torch.memory_format.channels_first)

Примечание : на данный момент нет функции x.get_memory_format() , только явные проверки - это позволяет более широкий диапазон возможных реализаций. Но мы могли бы добавить его.

Семантическая компоновка тензор всегда остается неизменной - NCHW! x.size() всегда возвращает (n,c,h,w)

Операции сохраняют поведение формата памяти:

  • свертка, объединение в пул и т. д. (см. выше) возвращают вывод в том же формате памяти, что и ввод, и отправляют изнутри в лучшую реализацию
  • унарные поэлементные операции сохраняют тот же формат памяти и должны выполняться так же быстро, как на непрерывном тензоре
  • Бинарные поэлементные операции обеспечивают некоторые разумные гарантии сохранения формата памяти - вероятно, их можно определить шире, но минимум:

    • NHWC + скаляр → NHWC

    • NHWC + вектор-столбец → NHWC

  • обратные операции для основных операций CNN сохраняют тот же формат памяти, что и при прямом пути. (может потребоваться явное применение, поскольку входящие градиенты для вывода могут быть в другом формате памяти)

Формат памяти - это свойство тензора, которое сохраняется посредством сериализации / десериализации (в случае, если тензор является параметром).

Пошаговая реализация

Тензор в PyTorch сегодня имеет концепцию шагов, которые определяют, как логический тензор размещается в памяти . В частности, каждый тензор имеет вектор strides той же длины, что и sizes . Чтобы индексировать элементы в логической индексации (i1, i2, .., ik) выполняется скалярное произведение с шагом и просматривается память в offset + i0*stride0 + i1*stride1 + ... * ik * stridek . Таким образом, у смежных тензоров есть шаги, которые представляют собой перевернутые кумулятивные произведения размеров. Например, 4D тензор с размерами (n,c,h,w) имеет шаги (c*h*w, h*w, w, 1) .

Шаги могут использоваться для физического представления различных форматов памяти (которые являются переупорядочиванием измерений) при сохранении логического порядка NCHW по умолчанию. Он дает эффективное определение преобразования формата памяти как:

# implementation of x.to(channels_last)
def to_mem_format_nhwc(x):
    return x.permute(0,2,3,1).contiguous().permute(0,3,1,2)

# implementation of x.to(channels_first)
def to_mem_format_nchw(x):
    return x.contiguous()

В формате NHWC вектор шагов - (c*h*w, 1, c*w, c) . Таким образом, в буфере памяти веса находятся в непрерывном порядке для NHWC.

Strides можно использовать для тестирования:

def is_nhwc_contiguous(x):
    return x.permute(0,2,3,1).is_contiguous()

# or alteratively
def is_nhwc_contiguous(x):
    n,c,h,w = x.size() # in any case the sizes remain in NCHW order
    return x.stride() == (c*h*w, 1, c*w, c)

def is_nchw_contiguous(x):
    return x.is_contiguous()


# operator implementations can just check contiguity and carry on directly on data pointer
def my_sample_op(x):
    if x.is_contiguous(nhwc):
        float* p = x.data();
        # Do we need to go to c++ here? 
        # can we have an example in python?
        n,c,h,w = x.size()
        # operate on `p` as it's guaranteed to be (n,h,w,c) array
        y=my_nhwc_op(p)
        # Do we need to convert the layout of y?

    else:
        # Need to convert x to nhwc layout
        x = x.permute(0,2,3,1).contiguous()
        float *p = x.data();
        # Is this needed?
        y = my_nhwc_op(p)
        return y.permute(0,3,1,2).contiguous()

Плюсы такого подхода:

  • Использует существующую концепцию шагов PyTorch без добавления новых идей верхнего уровня или параметров API.
  • Сохраняет логическое поведение тензора в каноническом порядке NCHW
  • Работает для произвольного изменения порядка входных размеров
  • Существующие процедуры сериализации уже сохраняют шаги тензора
  • Возможность многократного использования множества операций для работы с разным расположением памяти.

Минусы :

  • Вызов .contiguous() эквивалентен переключению на NCHW и может произойти случайно от пользователя или внутри одной из операций.

    • Необходим явный аудит операторов, чтобы убедиться, что они сохраняют формат памяти.

  • Не работает для заблокированных / плиточных форматов - нужен другой подход

    • Можно подумать о добавлении их как первоклассных граждан в PyTorch, но это гораздо большее изменение.

    • Альтернативой является обработка их как непрозрачных маркеров, например тензоров MKLDNN.

  • Характеристики производительности базовых реализаций менее очевидны для конечного пользователя.

Самая большая потенциальная проблема связана с неясным намерением пользователя . Невозможно определить, действительно ли пользователь хотел использовать другой формат памяти, или же входной тензор просто так поступил. В частности, это приводит к изменению поведения существующих операций - сегодня свертка может создавать только NCHW-смежные тензоры, даже если входные данные имеют произвольный шаг, в новом мире он может распознавать входные данные как NHWC и, таким образом, также возвращать NHWC. Это не меняет семантику, но приводит к трудным для отладки проблемам с производительностью. Возможное решение может заключаться в том, чтобы явно пометить тензоры указанным пользователем флагом memory_format и следовать только этой аннотации (в дополнение к шагам).

Чтобы решить вышеуказанную проблему, первоначальное предложение состоит в том, чтобы ввести тег «мягкого» формата памяти в тензоре, который записывает последний вызов to(memory_format) выполненный в тензоре. Операторам потребуется распространить эту аннотацию на выходы. Аннотации являются «мягкими», поэтому мы не будем делать жесткую ошибку при несовпадении аннотаций, а будем выдавать предупреждения в режиме профилирования.

Реализации оператора

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

def maxpool(x: Tensor):
    if x.is_contiguous(torch.layout.NHWC):
        return max_pool_impl_nhwc(x)
    return max_pool_impl_default(x.contiguous())

Для обозначения операторов в JIT IR предпочтительнее использовать один символ, например «conv», а не создавать отдельные операторы, такие как «conv_nhwc». Причина в простоте и сохранении IR на уровне семантического представления.

Поэлементные операции

Мы должны гарантировать, что основные операции, такие как поэлементные, сохраняют формат памяти и являются эффективными.

Унарные операции можно в общем случае обрабатывать, проверяя, является ли блок памяти «плотным», т. Е. Охватывают ли элементы область без пропусков и каждая ячейка памяти используется ровно один раз. Это можно проверить с помощью простого алгоритма

def is_dense_format(x):
    p = 1
    for s, d in sorted(zip(x.stride(), x.size())):
        if s != p:
            return False
        p *= d
    return True

def my_unary(x):
    if is_dense_format(x):
        return contig_memory_impl(x.data(), x.numel())
    return default_strided_impl(x)

# is_dense_format can be used in implementations of e.g. empty_like too

Инструменты для повышения производительности

Для повышения производительности отладки мы должны добавить в профилировщик поддержку для:

  • видеть, где в программе происходит реальное переупорядочение памяти - то есть отслеживать вызовы .contiguous ()
  • отслеживание того, какая реализация вызывается
  • выдавать предупреждения об изменениях формата памяти, например, в двоичных операциях (где полезны «мягкие» аннотации)

Эта функция может быть встроена в инструмент профилирования по запросу.

Обработка автограда

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

Возможная реализация:

def conv_backward(input, weight, grad_output, grad_weight, grad_input):
  if input.is_contiguous(torch.memory_format.channels_last):
    grad_output = grad_output.to(torch.memory_format.channels_last)
    return conv_backward_nhwc(...)
  else:
    grad_output = grad_output.contiguous()
    return conv_backward_nchw(...)

Представление в JIT

Текущее предложение должно иметь:

  • Пока нет первоклассной обработки формата памяти в аннотациях типов. Вместо этого мы можем поддерживать дополнительную карту в необходимой форме для проходов, которые управляют форматом памяти.
  • Этап вывода (аналогично shape_inference), который создает аннотации формата для каждого значения
  • Проходы преобразования формата памяти (ручные или автоматические), которые находят, где необходимо, to(memory_format) вызовы должны быть вставлены для оптимальной производительности

В целях обеспечения соблюдения мы также можем использовать такие операторы, как assert x.is_contiguous(channels_last) .

Примечание. Возникает вопрос, где хранить информацию о том, что конкретное устройство имеет предпочтительную комбинацию форматов памяти (например, qconv на маршрутах x86 к fbgemm, который реализует только NHWC). Один из вариантов - поместить его на уровень регистрации op, однако аннотация формата памяти выглядит как дополнительная информация. Мы можем начать с поддержки глобальной карты где-нибудь на этапе JIT, которая обозначает предпочтительные форматы памяти и связанные эвристики. Если будет неаккуратно - можно перейти на регистрационный механизм.

За гранью: заблокированные макеты

Поскольку мы решили добавить более сложные упаковки тензоров, использование первоклассного тензора PyTorch может оказаться маловероятным из-за высокой стоимости и сложности реализации. Возможны две альтернативы:

  • Непрозрачные представления, такие как пользовательские привязки типа C. Это вариант, который можно выбрать для упаковки при выводе, где разнообразие выше с точки зрения оптимизации производительности.
  • Первоклассный тензорный тип, такой как MKLDNNTensor, с некоторыми (но не всеми) операциями, связанными с этим новым типом.

Еще одна альтернатива - реализовать встроенную поддержку блокировки / тайлинга в основном классе PyTorch Tensor.

Именованное тензорное отношение

Существующее предложение для NamedTensor структурировано как механизм проверки типов на тензорах - на данный момент он не придает никакого семантического значения именам измерений. Таким образом, единственный способ вывести значение тензора активации - продолжить использование заранее определенного формата NCHW. Это делает NamedTensor и текущие предложения ортогональными.

Если мы хотим жестко указать значения некоторых имен (например, «каналы», «ширина»), операторы могут использовать эту информацию для ускорения реализации. Это было бы семантическим изменением, поскольку входные тензоры логически имели бы формат памяти NHWC (а не NCHW, как сегодня).

Предшествующий уровень техники

TensorFlow поддерживает как NHWC, так и NCHW на уровне оператора с помощью параметра data_format ; допустимые значения («NHWC», «NCHW») для 4-мерных входов, («NDHWC», «NCDHW») для 5-мерных входов или channels_first / channels_last независимо от входных данных. размерность. Правильная установка параметра зависит от пользователя, т. Е. Он не отслеживается автоматически тензором.

Caffe2 вызывает этот параметр, который называется order а не data_format , но он по-прежнему явно применяется на уровне отдельного оператора.


Приложение: Рассмотрены другие варианты

Вопрос с лакмусовой бумажкой: что печатает следующий код: tensor_in_nhwc_layout.size(1) - количество каналов (потому что по умолчанию в PyTorch используется NCHW) или высота (потому что это то, что в макете NHWC на ​​позиции 1).

На основании этого ответа возможны несколько вариантов:

  • Вариант А - Шаги (представлены выше). Тензорный макет - это полностью внутреннее представление. Похоже на реализацию, это удобнее всего делать шагами.

    • .size (1) возвращает мне «каналы», но внутренняя память устроена иначе

    • pro: не меняет код модели, моя модель все еще может выполнять арифметические вычисления напрямую. Фактически ни один из публичных API не меняется

    • минусы: в поэтапной реализации многие операторы вызывают .contiguous () и могут случайно вернуть макет обратно

    • Минусы: с точки зрения пользователя, понимание того, какие гарантии возврата операции имеют первостепенное значение. Эта IMO исключает подходы только для шагов, потому что становится очень трудно понять формат, в котором они будут возвращены, и нет API, чтобы сказать: «Не обращайте внимания на мои шаги, на самом деле просто верните NCHW-смежную вещь». Это в дополнение к указанным выше ограничениям.

  • Вариант B - Явный тензор NHWC. Пользователь явно манипулирует тензором, который имеет другой порядок размерности, но сам тензор ничего об этом не знает. Нам понадобится аннотация на уровне оператора, чтобы выяснить, чего ожидает пользователь.

    • .size (1) возвращает «высоту».

    • за: никакой магии и очень предсказуемо

    • Минусы: изменение модели с одного макета на другой становится сложной операцией, которая должна отслеживать все обращения к .size () и .reshape () (или вам нужно сделать это явным в API?)

  • Вариант B '- Явный тензор NHWC с флагом макета . То же, что и выше, но мы позволяем прикреплять аннотацию к тензору, чтобы отмечать его семантический макет, который операторы используют в своей реализации. Тогда нет необходимости в аннотации на уровне оператора - оператор может выполнять диспетчеризацию на основе флага макета входных данных.
  • Вариант C - Именованный тензор . ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading = h.2gbe5xpga3w9)

    • .size (1) возвращает «высоту», но мы просим людей НЕ использовать этот API и вместо этого использовать .size ('канал')

    • за: очень четко и то, что хочет пользователь

    • Con: не решает проблему перехода, нам нужно заставить весь код, написанный с учетом макета, использовать именованные тензоры. Если нет - применимы те же проблемы, что и выше.

  • Вариант D - Макет непрозрачного тензорного типа . Относитесь к NHWC так же, как мы относимся к MKLDNN или SparseTensor - отдельный тип тензора с другим DispatchID. Это похоже на вариант A, но с другими компромиссами в поведении по умолчанию - невыполненные операции завершатся ошибкой вместо возврата к NCHW.

    • .size (1) по-прежнему возвращает «каналы»

    • pro: никакой магии и явной, отдельная отправка позволяет операторам решать, что они хотят

    • за / против: все необходимые операторы должны быть реализованы в другом макете, если какая-то операция отсутствует, пользователь получит явную ошибку, что она не поддерживается

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

internals mkldnn triaged

Самый полезный комментарий

Кстати, почему мы должны создавать новую концепцию, а не просто придерживаться layout ? Я не думаю, что у разреженных представлений есть четко определенная концепция макета, такая как "channels_last", поэтому нам не нужно представлять продукт memory_formats * layouts ( layouts относится к текущему использованию ), но только memory_format + layouts что означает, что можно использовать тот же аргумент, что и раньше? Для меня это и короче, и приятнее, и позволит нам избежать расширения подписей фабрик до тысячи аргументов.

Все 68 Комментарий

Есть одна проблема с empty_like ; текущая определенная семантика состоит в том, что вы отбрасываете всю информацию о шаге, поэтому невозможно сохранить макет и быть BC.

@VitalyFedyunin подписан на реализацию битов .contiguous() и torch.memory_layout

Один вопрос - для 4D тензора x с размерами (n, c, h, w)

x = torch.randn(n,c,h,w)
# x.size(): (n, c, h, w)
# x.stride(): (c*h*w, h*w, w, 1)

У нас странная перестановка

y = x.permute(0, 3, 1, 2)
# y.size(): (n, w, c, h)
# y.stride(): (c*h*w, 1, h*w, w)

Теперь проверяем, является ли он непрерывным для формата NHWC. Следуя вашей логике, как показано ниже

def is_nhwc_contiguous(x):
    return x.permute(0,2,3,1).is_contiguous()

# or alternatively
def is_nhwc_contiguous(x):
    n,c,h,w = x.size() # in any case the sizes remain in NCHW order
    return x.stride() == (c*h*w, 1, c*w, c)

В обоих случаях is_nhwc_contiguous(y) вернет True?

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

Что, если шаги имеют тот же порядок, что и формат памяти? В качестве примера возьмем 4-мерный тензор. Для описания тензора у нас есть sizes , strides и stride_indexes :

размеры в (n, c, h, w)
шаги в физическом порядке, т. е.

  • шаги (n, c, h, w), если формат nchw
  • шаги (n, h, w, c), если формат равен nhwc.

stride_indexes отображает шаги в размер nchw:

  • (0, 1, 2, 3), если формат равен nchw,
  • (0, 2, 3, 1), если формат равен nhwc.

Для формата nchw это то же самое, что и раньше. Для nhwc будет аналогично.

def is_nhwc_contiguous(x):
     n,c,h,w = x.size()
     return x.stride() == (h*w*c, w*c, c, 1)

def is_nchw_contiguous(x):
    n,c,h,w = x.size()
    return x.stride() == (c*h*w, h*w, w, 1)

def is_nchw_format(x):
    return x.stride_index() == (0, 1, 2, 3) 

def is_nhwc_format(x):
    return x.stride_index == (0, 2, 3, 1)

def is_contiguous(x):
    if (is_nchw_format(x)):
        return is_nchw_contiguous(x)
    else if (is_nhwc_format(x)):
        return  is_nhwc_contiguous(x)
    else:
        warning_not_support()

# or, to use stride_index
def is_contiguous(x):
    return x.stride() == (x.size[x.stride_index[1]]*x.size[x.stride_index[2]]*x.size[x.stride_index[3]], x.size[x.stride_index[2]] * x.size[x.stride_index[3]], x.size[x.stride_index[3]], 1)

Это также может быть расширено для поддержки заблокированного формата. В качестве примера возьмем nChw16c,

sizes: (n, c, h, w)
block_sizes: (n, c/16, h, w, 16)
strides: strides of (n, c/16, h, w, 16)
stride_indexes: (0, 1, 2, 3, 1)  # assume blocked dimension is always in dense (i.e. on the right side of major dimension)

Более подробную информацию можно будет изучить позже.

Для OP, который принимает только непрерывный тензор nchw, здесь будет некоторая работа.

В качестве альтернативы мы также можем немного изменить прототип, скажем

def is_contiguous(format=nchw):
    ...
def contiguous(format=nchw)
    ...

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

Мы стремимся создать API, способный представлять:

  • Тензор с другим форматом памяти (вначале просто порядок измерений) присутствует в PyTorch в Eager и JIT. Заблокированные макеты имеют более низкий приоритет, но все равно хороши.
  • Доступные пользователю API для запросов и изменения формата памяти
  • Основные операции CNN могут обрабатывать входные тензоры с различным форматом памяти и маршрутизацией для соответствующей более быстрой реализации
  • Возможность делать выводы и оптимизировать форматы памяти в JIT-проходах

Отличное предложение! Могу я выразить свое понимание, чтобы увидеть, правильно ли это (включая предложения по обработке форматов MKL-DNN):

Позвольте мне подумать, что это предложение было реализовано как класс «форматирования». Пока он обеспечивает запросы и изменение API как виртуальный, мы могли бы выполнять наследование / расширения, которые подходят для сложных форматов MKL-DNN. Или другие методы, если они предоставляют основу для обработки форматов, передавая нам эти мелкие детали.

Что касается реализации OP, каждый OP может иметь предпочтительные форматы, которые обеспечивают максимальную производительность, и совместимый формат, который работает. Поэлементный оператор (или, в более общем смысле, ОП с ограничением памяти) не имеет предпочтений. OP создает тензор результатов с помощью объекта «формат», этот объект формата гарантирует семантику запроса / изменения, совместимую с ожиданием pytorch по умолчанию, а также то, что он может обрабатывать определенные форматы, если они вызываются сериями оптимизированных функций (например, conv2d (ReLU (conv2d)) кейс)

@uyongw Я хочу уточнить ваш первый пример. Вы настраиваете пример следующим образом: «У меня есть тензор NCHW, который я затем странным образом транспонировал (теперь он выглядит как NWCH); теперь я хочу знать, является ли он смежным с NHWC». Но это неправильный взгляд на это. Лучшая формулировка: «У меня есть тензор NHWC, который я затем преобразовал в тензор NCHW».

Иными словами, физические размеры тензора не имеют внутреннего смысла (когда мы игнорируем шаги). Мы придаем им значение только тогда, когда рассматриваем, как мы относим их к шагам.

Для описания тензора у нас есть размеры, шаги и stride_indexes.

Я действительно думаю, что stride_indexes - удобный способ подумать о проблеме, но он строго избыточен с шагами, потому что все, что вы говорите, это «Примените эту (обратную?) Перестановку к шагам, а затем относитесь к этому как истинных успехов.) @VitalyFedyunin и я говорили о том, что все еще может быть хорошей идеей каким-то образом кэшировать эту информацию, потому что восстанавливать информацию на основе самих успехов очень сложно. Но это выходит за рамки этого предложения.

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

Ага, это мое прочтение плана.

@CaoZhongZ

Позвольте мне подумать, что это предложение было реализовано как класс «форматирования». Пока он обеспечивает запросы и изменение API как виртуальный, мы могли бы выполнять наследование / расширения, которые подходят для сложных форматов MKL-DNN. Или другие методы, если они предоставляют основу для обработки форматов, передавая нам эти мелкие детали.

Я вообще-то не думаю, что это точное описание предложения. Поддержка макета памяти, которую поддерживает это предложение, - это только макеты, которые можно выразить шагами. Все, что невозможно выразить таким образом (например, блочная компоновка), не будет работать таким образом и должно поддерживаться нашим более тяжелым механизмом «компоновки».

Иными словами, физические размеры тензора не имеют внутреннего смысла (когда мы игнорируем шаги). Мы придаем им значение только тогда, когда рассматриваем, как мы относим их к шагам.

Отчасти согласен :-) Но не по этой конкретной проблеме. Скажем, у меня уже есть тензор nhwc. Затем я переставляю его на nwhc. Я хочу перейти к nhwc, а затем выполнить contiguous (). Но у меня уже есть nhwc contiguous. Разве это не сбивает с толку?

Я действительно думаю, что stride_indexes - удобный способ подумать о проблеме, но он строго избыточен с шагами, потому что все, что вы говорите, это «Примените эту (обратную?) Перестановку к шагам, а затем относитесь к этому как к истинным шагам.)

ИМХО, это не будет лишним с шагами, если у вас есть шаги в nhwc (физическом). Потому что вам нужно правильное сопоставление с размерами (логикой). В противном случае невозможно определить реальный порядок.

Кстати, есть более простой подход с использованием обратного сопоставления. Скажем, для nchw это (0, 1, 2, 3), для nhwc это (0, 3, 1, 2) вместо (0, 2, 3, 1). Это говорит о том, что сам stride_index всегда тоже NCHW. Но проблема в том, что его нельзя распространить на заблокированные форматы, такие как nChw16c или OIhw16i16o.

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

Отчасти согласен :-) Но не по этой конкретной проблеме. Скажем, у меня уже есть тензор nhwc. Затем я переставляю его на nwhc. Я хочу перейти к nhwc, а затем выполнить contiguous (). Но у меня уже есть nhwc contiguous. Разве это не сбивает с толку?

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

  • Тензор «nhwc» согласно этому предложению: «Тензор, физический макет которого - NHWC, но разделен таким образом, чтобы логический макет был NCHW».
  • Чтобы «переставить (тензор, логический макет которого NCHW) тензор (логический макет) NWHC», нужно запустить y = x.permute(0, 2, 3, 1) , поскольку вы переставляете логический макет, а не физический макет. (Я подозреваю, что вы имели в виду не это, потому что в своем исходном сообщении вы упомянули перестановку x.permute(0, 3, 1, 2)
  • Чтобы затем переставить (логический макет) тензор NWHC в (логический макет) NHWC, нужно применить перестановку z = y.permute(0, 2, 3, 1) . Итак, теперь у вас есть тензор, логическая компоновка которого совпадает с физической компоновкой. Это означает, что если мы спросим z.contiguous() мы получим истину (и, что сбивает с толку, z.contiguous(memory_layout=NCHW) тоже будет верным). Но это НЕ будет непрерывным NHWC.

Я не думаю, что вы имели в виду этот пример, и в этом случае вам придется уточнить, что вы подразумеваете под «перестановкой».

ИМХО, это не будет лишним с шагами, если у вас есть шаги в nhwc (физическом). Потому что вам нужно правильное сопоставление с размерами (логикой). В противном случае невозможно определить реальный порядок.

Это суть предложения: мы привилегия NCHW как логическое расположение, всегда. Итак, если у меня есть четырехмерный тензор, о котором я ничего не знаю, я предполагаю, что его логическая структура - NCHW. Это устраняет двусмысленность. Если вы хотите иметь дело с тензорами, логическая структура которых не является NCHW, я действительно думаю, что заявленный API немного усложняет вам жизнь.

@dzhulgakov

Операции сохраняют поведение формата памяти

Если физические тензоры NHWC могут возникать просто шагами, это технически нарушает BC, если только вы не заставляете их сохранять формат памяти только при наличии тега формата памяти (но похоже, что вы не хотите, чтобы это имело семантическое значение, поэтому я Я не уверен, что сейчас предлагается в предложении.) Я не уверен, что это действительно нарушает чей-либо код на практике.

Если физические тензоры NHWC могут возникать просто шагами, это технически нарушает BC, если только вы не заставляете их сохранять формат памяти только при наличии тега формата памяти (но похоже, что вы не хотите, чтобы это имело семантическое значение, поэтому я Я не уверен, что сейчас предлагается в предложении.) Я не уверен, что это действительно нарушает чей-либо код на практике.

Предполагая, что мы можем сделать формат памяти «липким». Операция над тензором, отформатированным в память, создаст тензор, отформатированный в память. Это решит проблему BC.

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

@ezyang О, я только что обнаружил опечатку в моем ответе выше. (Прошу прощения за это. Однако исходный пример все еще верен.) Позвольте мне повторить его, как показано ниже:

  1. У меня есть тензор NCHW (физически непрерывный).
  2. Затем я переставляю его на NWHC (логически).
  3. Я хочу далее переставить его в NHWC с последующим вызовом contiguous ().
  4. Используйте его как NHWC (физически).

Но я получил его NHWC непрерывно уже после шага 2. Тогда я могу пропустить шаг 3 и использовать его как NHWC непосредственно на шаге 4. Но это определенно неверно, потому что физический порядок тензора вообще не меняется.

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

Да, мы можем включить NHWC в качестве первого шага. Однако я на самом деле не думаю, что заблокированный формат - это что-то совершенно другое. Это может быть естественно выражено (с некоторой хорошей абстракцией). Если есть общее описание формата, то другие могут просто зарегистрировать новые форматы с произвольными блокировками / шагами.

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

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

Но я получил его NHWC непрерывно уже после шага 2. Тогда я могу пропустить шаг 3 и использовать его как NHWC непосредственно на шаге 4. Но это определенно неверно, потому что физический порядок тензора вообще не меняется.

Хорошо, теперь я понимаю твой пример. Вы действительно можете остановиться на шаге 2 и использовать его, как если бы это был тензор NCHW; в этом случае вы неправильно интерпретируете W как C и т. д. Это определенно недостаток реализации на основе шага ( @dzhulgakov , мы, вероятно, должны добавить это в предложение). Предложение содержит некоторые положения на этот случай:

Чтобы решить вышеуказанную проблему, первоначальное предложение состоит в том, чтобы ввести тег «мягкого» формата памяти в тензоре, который записывает последний вызов (memory_format), выполненный в тензоре. Операторам потребуется распространить эту аннотацию на выходы. Аннотации являются «мягкими», поэтому мы не будем делать жесткую ошибку при несовпадении аннотаций, а будем выдавать предупреждения в режиме профилирования.

Тег формата мягкой памяти позволит вам отличить тензор NCHW, который вы переставили, от тензора, который физически является NHWC. Но мягкий тег в его текущей форме не является обязательным, поэтому я не уверен, насколько он действительно будет полезен для этого случая.

Другой способ решить проблему - использовать именованные тензоры. С именованными тензорами мы можем использовать имена в (логических) измерениях, чтобы выяснить, рассматриваем ли мы тензор как NCHW (предполагаемое значение по умолчанию) или что-то еще.

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

Здесь есть больше комментариев по теме: https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374

@ezyang Спасибо за ответ. Да, может помочь мягкий тег формата. Проблема в том, что он может быть недостаточно гибким, поскольку порядок размеров может быть произвольным. Кроме того, он сам по себе не вычислим. Именованный тензор имеет семантическое значение для каждого измерения, но я сомневаюсь, что для его поддержки могут потребоваться дополнительные средства.

Лично я думаю, что это можно решить, введя карту от порядка шагов (физического) до порядка размеров NCHW (логического). Как я предлагал выше, для NCHW он почти такой же, как и нынешний дизайн; для NHWC sizes по-прежнему NCHW, strides будет в порядке (N, H, W, C). И мы используем stride_index = (0, 2, 3, 1), чтобы указать индекс измерения шагов.

Более того, комбинация strides и stride_index может использоваться для представления любого тензорного формата. Это может дать другим возможность регистрировать новый формат данных.

@ezyang

Операции сохраняют поведение формата памяти

Если физические тензоры NHWC могут возникать просто шагами, это технически нарушает BC, если только вы не заставляете их сохранять формат памяти только при наличии тега формата памяти (но похоже, что вы не хотите, чтобы это имело семантическое значение, поэтому я Я не уверен, что сейчас предлагается в предложении.) Я не уверен, что это действительно нарушает чей-либо код на практике.

Когда арифметические операции и пороговые значения были перенесены в TensorIterator, это было технически нарушением BC (потому что раньше формат памяти операндов не сохранялся, а TensorIterator сохраняет его). Статус-кво сейчас очень непоследователен - порог сохраняет макет, все другие унарные операции - нет, torch.where - нет, арифметические операции сохраняют макет, если оба операнда имеют одинаковый макет, но по умолчанию будут использовать «nchw» или тензор, который равен contiguous в текущем понимании, если есть несоответствие, я не уверен, что происходит с трансляцией.
Вы также хорошо замечаете, что empty_like и тому подобное, сохраняя макет, не являются BC. Возможно, ему также понадобится аргумент макета, например is_contiguous в предложении

x.is_contiguous(torch.memory_format.channels_first)

@ezyang @ngimel

Есть одна проблема с empty_like; текущая определенная семантика состоит в том, что вы отбрасываете всю информацию о шаге, поэтому невозможно сохранить макет и быть BC.

Вы также хорошо замечаете, что empty_like и тому подобное, сохраняя макет, не являются BC.

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

  • форма: размеры
  • логический порядок: информация о порядке записывается шагами (обычно используется для поддержки транспонирования или перестановки)
  • физический порядок: NCHW или NHWC (может быть адресован как stride_index, как я предложил).

В настоящее время физический порядок такой же, как форма / размеры. Поэтому мы просто постепенно отказываемся от логического порядка. Представьте, что мы разделяем форму и физический порядок, мы также можем просто отбросить логический порядок, но сохранить форму и физический порядок для empty_like . Это означает, что и size() и stride_index() будут сохранены, но stride() будет сброшен. В частности, empty_like тензора NHWC вернет непрерывный тензор NHWC с той же указанной информацией о форме.

@uyongw Я не уверен, что было бы неплохо изменить empty_like ; прямо сейчас его семантика соответствует numpy empty_like .

Статус-кво сейчас очень противоречиво - threshold сохраняет макет, все другие унарные операции - нет, torch.where - нет, арифметические операции сохраняют макет, если оба операнда имеют одинаковый макет, но по умолчанию будут использовать «nchw» или тензор, который является смежным в текущее понимание, если есть несоответствие, я не уверен, что происходит с трансляцией.

@ngimel , да, сейчас они не очень согласованы. Я думаю, что часть разработки того, как представить формат памяти, - это привести наши операторы в согласованное состояние.

Элемент empty_like @ zou3519 numpy, который вы связали, имеет аргумент order который по умолчанию «максимально соответствует макету прототипа». Это не то, что в настоящее время делает empty_like в pytorch (он возвращает "nchw" - непрерывный тензор, даже если прототип является несмежным)

О, понятно, я слишком быстро это читал. В этом случае было бы неплохо, если бы у нас также было совпадение empty_like с numpy, и было бы (возможно?) Также хорошо иметь здесь макет памяти

@ zou3519 Да, я пытаюсь сказать, чтобы сохранить текущую семантику (отбросить логический порядок, как упоминалось в @ngimel ) и в то же время сохранить физический макет, такой как значения по умолчанию для numpy. Таким образом, для прототипа NCHW поведение будет таким же, как и раньше. Для прототипа NHWC его поведение будет по-прежнему совместимым, т. Е. Новый тензор будет непрерывным NHWC, а не NCHW, если вы не измените текущую реализацию.

Два вопроса:

  • Что произойдет, если к тензору NCHW добавить тензор NHWC?
  • Как насчет устранения недостатка (B) путем создания таких методов, как t.channel_dim () для тензора, которые возвращают целочисленное значение, указывающее, где физически находится измерение? Такой подход может потребоваться даже для того, чтобы можно было выбирать другие форматы, например форматы блоков, без изменений в сети.

Если мы обратимся к недостатку (B) с помощью последнего пункта, то (B) мне кажется более предпочтительным. Это интуитивно понятно, а логические ошибки легко обнаружить. Все существующие операции тоже могут работать с тензором, поскольку он выглядит как любой другой смежный тензор. Операции, которые могут понимать семантику (аналогично предложению именованного тензора), также будут работать, как ожидалось.

Элемент empty_like @ zou3519 numpy, который вы связали, имеет аргумент order который по умолчанию «максимально соответствует макету прототипа». Это не то, что в настоящее время делает empty_like в pytorch (он возвращает "nchw" - непрерывный тензор, даже если прототип является несмежным)

В таких случаях мы планируем сохранить формат (для тензоров с форматированием памяти).

Что произойдет, если к тензору NCHW добавить тензор NHWC?
Операция с тензором, отформатированным в память, вернет тензор, отформатированный в память. Если оба тензора отформатированы в памяти, выходной формат будет определяться первым тензором.

Я бы добавил две вещи:

В таких случаях мы планируем сохранить формат (для тензоров с форматированием памяти).

Нам нужно будет провести аудит существующего использования, потому что часто операторы вызывают empty_like а затем предполагают, что они являются смежными NCHW. И я не знаю, как нам поступить со сторонним кодом. Похоже, нам понадобится другое значение по умолчанию, чем numpy, если мы хотим сохранить BC.

Операция с тензором, отформатированным в память, вернет тензор, отформатированный в память. Если оба тензора отформатированы в памяти, выходной формат будет определяться первым тензором.

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

Согласитесь с empty_like, существует довольно много случаев, когда результат empty_like / zeros_like и т. Д. Считается nchw-contiguous (физически непрерывным, я бы сказал, во многих случаях это не операции с изображениями).
Передача выходного тензора в большинстве случаев невозможна, потому что функции с out kwarg не дифференцируемы.

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

Python API

Представляем новый torch.memory_format

torch_memory_format.any # default value
torch_memory_format.preserve
torch.memory_format.contiguous # what most of the functions now behave as default
torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory

Тензор потребует явного преобразования формата памяти

x = torch.zeros((10,3,32,32)) # NCHW
x.permute(0,2,3,1).is_contiguous(memory_format=torch.memory_format.nhwc) == False # because memory still layed out as NCHW

Чтобы "пометить" их определенным форматом:

y = x.to(memory_format=torch.memory_format.nhwc)
y.is_contiguous(memory_format=torch.memory_format.nhwc) == True # We got new tensor with proper memory layout
y.is_contiguous() == False # Required for back compatibility
y.stride() == (3072, 3, 1, 96)

Теперь о empty_like и подобных:

z = torch.empty_like(y) 
z.is_contiguous() == True # For BC

Потому что на самом деле это:

z = torch.empty_like(y, memory_format=torch.memory_format.any ) 

Если мы хотим сохранить формат:

z = torch.empty_like(y, memory_format=torch_memory_format.preserve) 
z.is_contiguous() == False 
z.is_contiguous(memory_format=torch.memory_format.nhwc) == True

Сходным образом:

z = torch.empty_like(y, memory_format=memory_format=torch.memory_format.nhwc) 
z.is_contiguous() == False 
z.is_contiguous(memory_format=torch.memory_format.nhwc) == True

Это означает, что мы можем постепенно определять каждую функцию memory_format по умолчанию в соответствии с текущим состоянием мира, классифицировать их и помнить о том, как мы изменим их в будущем.

Если вы укажете тензор, TensorOptions в настоящее время игнорируются (в лучшем случае они генерируют исключение, например, несоответствие опций переданного устройства с out тензорное устройство).

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

x.zeros((10,3,32,32), memory_format=torch.memory_format.nhwc)
x = x.permute(0,1,3,2).permute(0,1,3,2)
x.is_contiguous(memory_format=torch.memory_format.nhwc) == False (even if strides are similar)

Не уверен в заполнении, буду благодарен за помощь здесь.

Однако мы можем сделать тензор 'tag' x.to (memory_format = torch.memory_format.nhwc) с правильным форматом и вернуть self

Многопроцессорность

Сохранит формат памяти 'tag'

Форматы блочной памяти

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

Внутренние API

Операторы смогут ветвиться в зависимости от формата памяти.

if (self.memory_format(nhwc)) {
 // fast path
} else
{
 // classic implementation
}

Если мы сделаем memory_format как TensorOptions, мы можем подумать о ветвлении на уровне отправки (аналогично устройству, макету)

Небольшая обратная связь с предложением @VitalyFedyunin - я думаю, что здесь требуются тензоры 4D

torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory

является слишком ограничительным (потому что мы также хотим обрабатывать 1D и 3D в дополнение к 2D), и channels_first/channels_last из исходного предложения были более подходящими для этой цели.

Согласитесь, нам нужно более качественное именование. channels_first звучит почти правильно, за исключением того, что партия идет первой =)

Мне нравится твое последнее предложение. Изменится ли обработка .contiguous ()? Вам потребуется .contiguous (memory_format = <...>)? Если это так, и многие операторы просто вызывают .contiguous (), они все равно могут неправильно форматировать память. Многие операции сегодня также выделяют выходные данные как empty_like (), что имело бы тот же эффект. Планируется ли обновить их, чтобы определить формат памяти входных данных и сделать правильные смежные и empty_like вызовы?

На данный момент наши пользователи (и все библиотеки) ожидают, что .contiguous() вернет непрерывный тензор памяти с шагами в порядке убывания.

Мы не можем разорвать этот контракт. Однако есть хорошие новости: как только мы поддержим опцию memory_format, JIT сможет понять, когда более эффективно вызывать .contiguous(memory_format=...) вместо классического формата.

@VitalyFedyunin Мы предполагаем, что операции, подобные ниже, не разрешены?

x.zeros(10,3,32,32)
# x is in nchw (default)
# x.size() is [10,3,32,32]
# x.stride() is [3*32*32, 32*32, 32,1]
x = x.permute(0,2,3,1)
# At this point 
# x.size() is [10,32,32,3], size is not in nchw order
# x.stride() is [3*32*32, 32,1,32*32]

# How can this be supported?
y = x.to(memory_format=torch.memory_format.nhwc)

Еще один вариант:

x.zeros(10,3,32,32)
# `x` is in nchw (default)
# x.size() is [10,3,32,32]
# x.stride() is [3*32*32, 32*32, 32,1]
x = x.permute(0,2,3,1)
x=x.contiguous()
# At this point 
# x.size() is [10,32,32,3], size is not in nchw order
# x.stride() is [32*32*3, 32*3,3,1]

# How can this be supported?
y = x.to(memory_format=torch.memory_format.nhwc)

@ raghuramank100 - зачем пользователю вообще вызывать .permute(0,2,3,1) ? Все тензоры в этом предложении имеют семантический размер (n, c, h, w), что означает, что size (1) возвращает вам каналы. Это то, что сегодня предполагает стандартная библиотека PT, а также то, что она предполагает в этом предложении. Так что вряд ли кто-то вообще никогда не назовет .permute

Может ли диспетчер контекста быть полезным, чтобы позволить пользователю переопределить формат памяти выделенных тензоров в области диспетчера для определенного формата?

with torch.memory_format(torch.memory_format.nhwc):
    # a will be allocated with the context managed memory format   
    a = torch.randn(...)

# b will be allocated matching some assumed default format
b = torch.randn(...)

Мне не нравится идея диспетчера контекста, так как он ослабит контроль над memory_format.

Например:

with torch.memory_format(torch.channels_last):
  x = torch.randn(10,3,32,32) # this one is NHWC
  y = torch.randn(10,10) @ this one is not

Когда явный memory_format дает понять:

x = torch.randn(10,3,32,32).to(memory_format=torch.channels_last) # this one is NHWC
y = torch.randn(10,10).to(memory_format=torch.channels_last) # This is errors out as dim == 2

При необходимости мы можем добавить синтаксис, позволяющий:

x = torch.randn(10,3,32,32, memory_format=torch.channels_last)

@ raghuramank100 нет необходимости переставлять.

y = x.to(memory_format=torch.channels_last)

Сделаем за вас всю грязную работу, сохраняя порядок яркости такой же, как в x.

Так:

x = torch.randn(10, 3, 32, 32)
nhwc = x.to(memory_format=torch.channels_last)
self.assertFalse(nhwc.is_contiguous())
self.assertTrue(nhwc.is_contiguous(memory_format=torch.channels_last))
self.assertEqual(nhwc, x)

И вы можете продолжать обращаться к nhwc в этом формате

nhwc[N][C][H][W]

@VitalyFedyunin В этом есть смысл.

С точки зрения пользователя, название метода (если оно остается таким) кажется мне вводящим в заблуждение, поскольку «to» уже является рекомендуемым способом передачи Tensor на другие устройства.

Кроме того, как насчет чего-то вроде Numpy для преобразования массивов C_ORDER и F_ORDER?

numpy.asfortranarray()
numpy.ascontiguousarray()

Легко представить себе что-то вроде:

torch.randn(32, 3, 64, 64).to(device).as_nhwc()

@VitalyFedyunin : Я понимаю, что преобразование в другой memory_format избавляет пользователей от необходимости переставлять вручную. Однако, как только эта функция станет доступной в torch, что произойдет, если пользователи будут вызывать функции в указанной выше последовательности? По крайней мере, у нас должно быть предупреждение / сообщение об ошибке, в котором говорится, что преобразование макета не удалось.

@VitalyFedyunin : Я понимаю, что преобразование в другой memory_format избавляет пользователей от необходимости переставлять вручную. Однако, как только эта функция станет доступной в torch, что произойдет, если пользователи будут вызывать функции в указанной выше последовательности? По крайней мере, у нас должно быть предупреждение / сообщение об ошибке, в котором говорится, что преобразование макета не удалось.

Это станет возможным только тогда, когда мы реализуем именованные тензоры. Потому что прямо сейчас:

x.zeros(10,10,10,10)
x = x.permute(0,2,3,1)

Никто не может сказать мне, создал ли я только что nchw или nhwc.

Возможно, я неправильно понял исходное предложение, но разве тег формата записанной памяти не должен устранять неоднозначность этой ситуации?

@VitalyFedyunin Имеет смысл, нам нужно убедиться, что об этом сообщают конечным пользователям, когда этот API стабилизируется.

@dzhulgakov @VitalyFedyunin После просмотра # 19975 у меня появились некоторые новые опасения по поводу тега формата записанной памяти в тензоре. Моя основная проблема в том, как нам решить, должны ли операции сохранять тег памяти? Первоначально я думал, что только операторы, «знающие альтернативную компоновку», должны обладать этим умом. Но, глядя на патч Виталия, я думаю, что некоторым основным операторам тоже потребуется корректировка. Например, рассмотрим x[0] ; если x ранее был тензором NHWC, то после этого я должен получить тензор HWC. Я почти уверен, что патч Виталия не справляется с этим правильно, и держу пари, что это очень сбивает с толку пользователей. Возможно, затронуты только те операторы, которые трясутся шагами (в этом случае их не так много, и мы можем их вручную проверять), но похоже, что мы должны это сделать. Что вы думаете?

Подождите, тензоры по-прежнему индексируются в следующем порядке: 0-dim N; 1-й размер C; 2-й размер H; 3rd-dim W. Итак, x [0] возвращает тензор с 0-dim C; 1-й размер H; 2-й тусклый W. Независимо от того, был ли x макетом памяти «каналы_первый» или «каналы_последний».

В противном случае memory_format просто не имеет смысла, и нам нужно только переставить тензор.

Я хочу сказать, что тег формата памяти не сохраняется. Если входной тензор был помечен как channels_last , новый тензор помечается как any

cc @ zou3519 , здесь логика распространения макета во многом напоминает мне распространение именованного измерения в именованной тензорной работе.

Я все еще разбираюсь в этом предложении. Но @ezyang мы могли бы отслеживать логику распространения макета, распространяя флаг (или имя) для каждого измерения, и тогда это было бы эквивалентно названию тензоров с соглашениями об именах

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

Фаза 1

Расширяет функциональность двух тензорных функций .is_contiguous и .contiguous (как python, так и c ++ api).

Примечание. У нас было несколько жалоб .to(memory_format) функцию

  1. .contiguous теперь поддерживает необязательный аргумент, состоящий только из ключевых слов - memory_format , который может быть либо torch.contiguous_format или torch.channels_last .

    • Использование torch.contiguous_format сохранит существующее поведение .contiguous() .

    • Вызов x.contiguous(memory_format=torch.channels_last) возвращает новый тензор, который поддерживает тот же семантический макет (NCHW), но имеет другой шаблон распределения памяти.

      x.contiguous(memory_format=torch.channels_last) ожидает, что входной тензор будет 3d, 4d или 5d; и не работает в противном случае.

  2. .is_contiguous теперь поддерживает необязательный аргумент, состоящий только из ключевых слов - memory_format , который может быть либо torch.contiguous_format или torch.channels_last .

    • x.is_contiguous(memory_format=torch.contiguous_format) сохраняет ту же функциональность, что и x.is_contiguous() и остается без изменений.

    • x.is_contiguous(memory_format=torch.channels_last) возвращает истину, если A) входной тензор непрерывен в памяти И B) выделен в памяти в формате NWHC (или аналогичном для 3d, 5d).

Примечание. К концу первой фазы x.is_contiguous(memory_format=torch.channels_last) будет вычислять состояние Tensor при каждом вызове. Эта функция будет обновлена ​​позже.

Фаза 2

Сохранить формат памяти для определенных операций:

  1. Унарные элементарные операторы сохраняют формат памяти channels_last.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.sin()
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  2. Бинарные поэлементные операторы ( add , sub , mul , div ) сохраняют формат памяти channels_last.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b * torch.randn(H,W)
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  3. Любые операции над размерами, шагами и затемнением приводят к сбросу формата памяти.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.permute(0,2,3,1).permute(0,3,1,2)
    c.is_contiguous(memory_format=torch.channels_last) == False
    

Остается нерешенным

  1. Результат операции изменения формы (и аналогичной), если вывод "channels_last" разборчивый

    import torch
    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.reshape(N,C,-1)
    c.is_contiguous(memory_format=torch.channels_last) # ?
    

    Примечание: в настоящее время memory_format не сохраняется

  2. Результат эксплуатации NHWC + NCHW. Это NHWC?

    Примечание: в настоящее время NHWC + NCHW -> NHWC и NCHW + NHWC -> NHWC.

А как насчет операций типа cat / split? Им будет полезно сохранить формат памяти.

@ezyang - по поводу индексации, думаю, на чем-то стоит остановиться. Различные схемы памяти не являются полностью прозрачными, и некоторым операциям следует разрешить их игнорировать. Я бы сказал, что x[0] должно быть разрешено стирать тег, включая x[0].unsqueeze(0)

Как упоминал Рагху, cat / split должен по возможности сохранить тег, поскольку это довольно распространенное использование. Я думаю, что общее эмпирическое правило должно заключаться в том, что до тех пор, пока работа не меняет ранг и не меняет порядок оси странным образом, мы должны сохранять тег. Если ранг меняется - все ставки снимаются.

Я согласен, что в некоторых случаях мы потеряем метку. Но я бы не согласился насчет x[0] . Мне это кажется очень распространенным способом перехода от NCHW к CHW .

После нескольких разговоров о том, насколько запутанно иметь тензоры с (или нет) «тегом» channels_last, мы решили рискнуть, введя изменение, нарушающее bc, и автоматическое продвижение тензоров в формат channels_last.

Что это значит для API:

Любые тензоры 3d, 4d, 5d с такими шагами, как N, 1, H, [W, [D]], автоматически получат формат памяти channels_last.

Чтобы это работало, мы предпримем особые меры предосторожности, чтобы гарантировать, что операторы для тензоров channels_last, которые выводят тензоры channels_last, будут иметь, по крайней мере, такую ​​же производительность, что и операторы для смежных тензоров.

В случае худшего сценария:
1) Пользователи могут вызывать .contiguous () на выходе.
2) Мы напишем код автоматического продвижения таким образом, чтобы изменить это поведение было бы почти тривиально.

Побочными эффектами такого автопродвижения являются:

import torch
x = torch.randn(10,16,16,3).permute(0,3,1,2) 
x.is_contiguous(memory_format=torch.channels_last) == True

С другой стороны, это может решить случай (после небольших доработок):

import torch
x = torch.randn(10,3,16,16).contiguous(memory_format=torch.channels_last)
x = x[0].unsqueeze(0)
x.is_contiguous(memory_format=torch.channels_last) == True

Из неактивных конверсий по

Наталья Гимельшейн [14:19]
Я так понимаю, что не было бы концепции тега.

import torch
#batch = 10, channels = 4, spatial dimensions = 16
x = torch.randn(10,16,16,4).permute(0,3,1,2)
x.is_contiguous(memory_format=torch.channels_last) == True
y = torch.randn(10,16,16,2).permute(0,3,1,2)
x1,x2 = x.chunk(2, dim=1) #chunk along channels dimension, no longer contiguous
x1.is_contiguous(memory_format=torch.channels_last) == False #right? So, if a tensor like this comes into e.g. convolution, what am I supposed to do with it? Did it want to be NHWC? Did it want to be nchw?
z=y+x1 #y is channels_last, x1 is something, what is the z layout?```

Виталий Федюнин [8:23]
z будет channel_last

Виталий Федюнин [8:25]
если x1 не является channel_last в любом из предложенных вариантов (если мы не изменим функцию фрагмента, чтобы не возвращать представления), то свертка преобразует его в непрерывный формат (channels_first) и также вернет непрерывный

Виталий Федюнин [9:12]
@ngimel благодарим вас за обратную связь, я думаю, что мы можем channel_last, чтобы охватить большинство случаев, когда задействованы операции, подобные просмотру. Буду держать вас в курсе.

Наталья Гимельшейн [9:36]
ответил в теме:
Значит, это проблема, не так ли? Разделение по размерам каналов - относительно обычное дело, например, в сетях, подобных начальному. Итак, если тензор - это первый тензор каналов, вывод свертки будет сначала каналами (что является интуитивно понятным поведением и, скорее всего, то, что хочет пользователь), если тензор разбит на фрагменты каналов - последним, тогда вывод свертки снова будет первыми каналами?

Наталья Гимельшейн [9:39]
ответил в теме:
Но только из-за некоммутативного сложения и того, что y является первым аргументом, а каналы - последними, верно? Что будет в результате для x1+y ? Есть ли у нас где-нибудь правила распространения макета для бинарных операций?

Виталий Федюнин [10:44]
1) Да, это проблема, которую мы собираемся решить с помощью альтернативного предложения. Сейчас схожу на тесты и запишу на этой неделе (через день или два).
2) x1 + y - также должен генерировать channels_last, иначе это сбивает с толку, и да, у нас будут записаны правила распространения макета.

Я думаю, что наблюдение, которое я сделал @VitalyFedyunin, когда мы поговорили об этом лично (но я не думаю, что вспомнил, чтобы это где-нибудь записать), заключается в том, что в свертке есть определенная степень свободы, то есть когда она становится аргумент, чей макет памяти не соответствует ни одному из тех, что он знает, как эффективно реализовать, к какому макету он должен присоединиться? По причинам BC сначала требуется примыкание к каналам, но мы приняли здесь произвольное решение - возможно, вы могли бы присоединиться к каналам последними. Возможно, нам следует иметь какой-то локальный переключатель потока, который говорит, какие значения по умолчанию?

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

Таким образом, нечеткость свертки (и других операторов, поддерживающих макет, если на то пошло, например, передискретизация, на которую я недавно смотрел, начинается с вызова .contiguous () на входе - так что это должно означать?) Была основной причиной за введение тега, iirc.

Да, я в порядке, если снова открою дизайн тегов, но потом мы
придется серьезно решить проблемы распространения этих тегов,
даже когда вы теряете макет (как это было бы в случае фрагментации
на каналах). Мне гораздо больше нравится делать "текущий макет"
своего рода диспетчер контекста, чем делать его зависимым от данных.

Выдержки из сообщения ngimel от 2019-06-19 12:43:45 -0700:

Таким образом, нечеткость свертки (и других операторов, поддерживающих макет, если на то пошло, например, передискретизация, на которую я недавно смотрел, начинается с вызова .contiguous () на входе - так что это должно означать?) Была основной причиной за введение тега, iirc.

Кстати, почему мы должны создавать новую концепцию, а не просто придерживаться layout ? Я не думаю, что у разреженных представлений есть четко определенная концепция макета, такая как "channels_last", поэтому нам не нужно представлять продукт memory_formats * layouts ( layouts относится к текущему использованию ), но только memory_format + layouts что означает, что можно использовать тот же аргумент, что и раньше? Для меня это и короче, и приятнее, и позволит нам избежать расширения подписей фабрик до тысячи аргументов.

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

в конце концов, memory_format - это способ тензорного шага и легкого выбора оптимизированных ядер и выходных данных, который является свойством полоскового тензора, а не совершенно другого класса

В некотором смысле разреженные макеты также являются способом легко выбрать оптимизированные ядра для массивов, которые в основном равны нулю 😄 Не могли бы вы подробнее рассказать о части «а также запретить автоматическое преобразование тензоров в другой memory_format на лету», пожалуйста?

Это может быть наивный вопрос, но почему PyTorch рассматривает этот API, а не просто предоставляет возможность использовать NHWC в самих операциях, которые напрямую вызывают базовое ядро ​​CuDNN, если оно доступно?

Похоже, что для обычного варианта использования (смешивание операций с изображениями, таких как conv и объединение с архитектурами LM) это было бы простым решением. Как разработчик, все, что мне нужно, это Conv2d(..., nhwc=True) . Есть ли причина, по которой это не имеет смысла?

@rewonc мы рассмотрели аналогичный подход (добавление опции к операторам вместо производного ядра из шага) и обнаружили, что его сложно применить по следующим причинам:

  • Этот подход потребует, чтобы ядро ​​выполняло ограничение непрерывного тензора для применения ядра NHWC.
  • Следующему оператору придется снова изменить ввод (на непрерывный), если он также не имеет опции nhwc=True .
  • Чтобы NHWC работал в сети, каждому оператору потребуется опция nhwc=True .

PS. Если вас беспокоят функции CudNN Ex , мы хотим предоставить cudnn_batch_norm_nhwc и аналогичные операторы.

Привет @VitalyFedyunin , мы видели, что названный тензор поддерживается в PyTorch 1.3. Может ли это решить (или частично решить) проблемы, связанные с поддержкой формата NHWC (или даже заблокированного)? Есть ли какой-либо план по продвижению состояния NHWC на ​​основе именованного тензора?

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

Спасибо. Это будет хорошо!

Решение задач и прогресс внутри https://github.com/pytorch/pytorch/issues/28619

Была ли эта страница полезной?
0 / 5 - 0 рейтинги