Pytorch: [RFC] Prise en charge du format de mémoire (aka layout aka NHWC)

Créé le 10 avr. 2019  ·  68Commentaires  ·  Source: pytorch/pytorch

Énoncé du problème

Les opérateurs CNN utilisent l'ordre canonique des dimensions du tenseur et leur attribuent une signification sémantique. Pour le cas 2D dans PyTorch aujourd'hui, une entrée dans torch.nn.Conv2d doit être un tenseur 4D dans l'ordre NCHW -.

Pour des raisons de performances, il est souvent avantageux de réorganiser les dimensions différemment afin que la mémoire accessible par des opérations particulières soit disposée de manière contiguë et que la localité soit mieux utilisée. L'option la plus courante consiste à déplacer les dimensions vers la fin - NHWC. Il peut y avoir des formats de mémoire encore plus complexes qui tuent une dimension en blocs, par exemple.

Exemples de bibliothèques l'utilisant :

  • cudnn a des performances plus rapides sur Volta en NHWC
  • fbgemm et qnnpack ne prennent pas en charge NCHW.
  • libxsmm prend en charge NCHW mais la pénalité de performance est d'environ 50% (IIRC).

Le défi est que la transformation de l'ordre des dimensions elle-même est coûteuse, donc dans les cas où plusieurs opérations CNN sont effectuées à la suite (par exemple conv(relu(conv))) ), il est avantageux de transformer une fois dans le format de mémoire différent, d' effectuer des opérations et de les réorganiser arrière.

Il est donc important de rendre PyTorch conscient des différents ordres de dimensions et de pouvoir passer des tenseurs avec différents formats de mémoire entre les opérations à la fois en mode avide et en mode JIT. De plus, il est avantageux d'avoir des passes d'optimisation JIT automatiques qui essaient d'appliquer des heuristiques ou des techniques de recherche pour déterminer si le changement de format de mémoire est bénéfique en termes de performances et où dans le modèle il est logique de le faire.

Nous nous efforçons de créer une API capable de représenter :

  • Tenseur avec un format de mémoire différent (au début, juste l'ordre des dimensions) présent dans PyTorch dans Eager et JIT. Les mises en page bloquées sont moins prioritaires mais restent agréables.
  • API exposées à l'utilisateur pour interroger et modifier le format de la mémoire
  • Les opérations de base de CNN étant capables de gérer des tenseurs d'entrée avec un format de mémoire différent et un routage vers une implémentation plus rapide correspondante
  • Capacité à déduire et à optimiser les formats de mémoire dans les passes JIT

Terminologie : le problème ci-dessus est souvent appelé « layout » (mxnet), « data_format » (tf), « image_format » (keras), « order » (caffe2). Nous proposons d'utiliser le nom « format mémoire » ou « format_mémoire » dans PyTorch. Le nom « layout » est malheureusement pris dans PyTorch avec les valeurs « strided » par rapport à « sparse_coo », de sorte que cette option de nommage n'est pas disponible.

Opérateurs concernés

Les opérateurs suivants doivent au minimum être conscients du format de la mémoire. En plus de produire le résultat correct, ils doivent fournir les meilleures performances à partir des bibliothèques sous-jacentes ET préserver le format de

  • convolution
  • différents types de mutualisation
  • norme de lot, norme de couche, norme d'instance (généralement, quelles que soient les normes)
  • suréchantillonnage/interpolation
  • abandon de fonctionnalité
  • softmax à un degré moindre - la dimension peut y être spécifiée manuellement, mais des implémentations efficaces ne sont présentes que pour la disposition nchw implicite
  • rembourrage
  • opérations au niveau des éléments (unaires et binaires)
  • constructeurs de tenseurs qui héritent du format de mémoire, par exemple empty_like.

API et changements de comportement

Définir le concept de format de mémoire dans PyTorch :

  • Des constantes comme torch.memory_format.channels_first . Ils n'ont pas de type spécifié et peuvent être des objets comparables arbitraires (commençant probablement par enum mais à l'avenir pourraient être d'autres objets à interagir avec le concept de tenseur nommé)

    • Alternative : utilisez directement torch.channels_first

  • Les valeurs sont channels_first et channels_last (pour permettre moins de constantes)
  • Pour les images 1D / tenseurs 3D, les valeurs signifient NCW, NWC, pour les images 2D / tenseurs 4D - NCHW, NHWC, pour les images 3D / tenseurs 5D - NCDHW, NDHWC

Ajoutez les méthodes suivantes à Tensor :

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

Note : il n'y a pas de fonction x.get_memory_format() pour le moment, seulement des vérifications explicites - cela permet un plus large éventail d'implémentations possibles. Nous voudrions peut-être l'ajouter cependant.

La disposition sémantique du tenseur reste toujours la même - NCHW ! x.size() renvoie toujours (n,c,h,w)

Les opérations préservent le comportement du format de mémoire :

  • la convolution, la mise en commun, etc. (voir ci-dessus) renvoient la sortie dans le même format de mémoire que l'entrée et la répartissent en interne vers la meilleure implémentation
  • les opérations unaires par élément préservent le même format de mémoire et doivent s'exécuter aussi vite que sur un tenseur contigu
  • Les opérations binaires au niveau des éléments fournissent des garanties raisonnables sur la préservation du format de la mémoire - elles peuvent probablement être définies plus larges, mais le minimum est :

    • NHWC + scalaire → NHWC

    • NHWC + vecteur colonne → NHWC

  • les opérations en amont pour les opérations CNN de base préservent le même format de mémoire que dans le chemin d'accès avant. (il peut être nécessaire de l'appliquer explicitement car les gradients entrants pour la sortie peuvent être dans un format de mémoire différent)

Le format de mémoire est une propriété d'un tenseur qui est préservée par sérialisation/désérialisation (au cas où le tenseur est un paramètre).

Implémentation à grands pas

Le tenseur dans PyTorch a aujourd'hui un concept de foulées qui spécifie comment le tenseur mémoire . Plus précisément, chaque tenseur a un vecteur strides de la même longueur que sizes . Afin d'indexer des éléments dans l'indexation logique (i1, i2, .., ik) on fait un produit par points avec des foulées et on recherche la mémoire à offset + i0*stride0 + i1*stride1 + ... * ik * stridek . Les tenseurs contigus ont donc des foulées qui sont des produits cumulatifs inversés de tailles. Par exemple le tenseur 4D avec des tailles (n,c,h,w) a des foulées (c*h*w, h*w, w, 1) .

Les foulées peuvent être utilisées pour représenter physiquement différents formats de mémoire (qui sont la réorganisation des dimensions) tout en préservant l'ordre NCHW par défaut logique. Il donne une définition efficace de la transformation de format de mémoire comme :

# 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()

Au format NHWC, le vecteur de foulée est (c*h*w, 1, c*w, c) . Ainsi, dans la mémoire tampon, les poids sont dans l'ordre contigu pour NHWC.

Les foulées peuvent être utilisées pour tester :

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()

Avantages de cette approche :

  • Utilise le concept de foulées PyTorch existant sans ajouter de nouvelles idées ou paramètres d'API de haut niveau
  • Préserve le comportement logique du tenseur dans l'ordre canonique NCHW
  • Fonctionne pour la réorganisation arbitraire des dimensions d'entrée
  • Les routines de sérialisation existantes préservent déjà les foulées du tenseur
  • Possibilité de réutiliser de nombreuses opérations pour travailler sur différentes configurations de mémoire

Inconvénients :

  • Appeler .contiguous() équivaut à passer à NCHW et peut se produire par accident de la part de l'utilisateur ou à l'intérieur de l'une des opérations

    • Un audit explicite des opérateurs est nécessaire pour s'assurer qu'ils préservent le format de la mémoire

  • Ne fonctionne pas pour les formats bloqués / en mosaïque - une approche différente est nécessaire

    • Il est possible d'envisager de les ajouter en tant que citoyens de première classe dans PyTorch, mais c'est un changement beaucoup plus important

    • L'alternative est de les traiter comme des poignées opaques, par exemple des tenseurs MKLDNN

  • Les caractéristiques de performances des implémentations sous-jacentes sont moins évidentes pour l'utilisateur final

Le plus gros problème potentiel est l'intention floue de l'utilisateur . Il n'y a aucun moyen de distinguer si l'utilisateur veut vraiment un format de mémoire différent ou si le tenseur d'entrée vient d'être foulé de cette façon. Plus précisément, cela conduit à un changement de comportement pour les opérations existantes - aujourd'hui, la convolution ne peut produire que des tenseurs contigus NCHW même si l'entrée est arbitrairement foulée, dans un nouveau monde, elle pourrait reconnaître l'entrée comme NHWC et donc retournerait également NHWC. Cela ne change pas la sémantique mais conduit à des problèmes de performances difficiles à déboguer. Une solution possible pourrait être de marquer explicitement les tenseurs avec l'indicateur memory_format spécifié par l'utilisateur et de ne suivre que cette annotation (en plus des foulées).

Pour résoudre le problème ci-dessus, la proposition initiale consiste à introduire une balise de format de mémoire « soft » sur le tenseur qui enregistre le dernier appel to(memory_format) effectué sur le tenseur. Les opérateurs devraient propager cette annotation aux sorties. L'annotation est « douce », nous n'aurons donc pas d'erreur matérielle sur les annotations qui ne correspondent pas, mais produirons plutôt des avertissements en mode de profilage.

Implémentations d'opérateurs

La signature des opérateurs existants ne change pas. Les opérateurs peuvent effectuer une répartition codée en dur à l'intérieur de l'opérateur pour accélérer la mise en œuvre. Si la mise en œuvre n'est pas disponible, un aller-retour via un format de mémoire différent est possible. L'alternative serait de générer un message d'erreur.

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

Il est préférable d'utiliser un seul symbole comme « conv » pour faire référence aux opérateurs dans JIT IR au lieu de créer des opérateurs distincts comme « conv_nhwc ». La raison en est la simplicité et le maintien de l'IR au niveau de la représentation sémantique.

Opérations par élément

Nous devons nous assurer que les opérations de base telles que les éléments préservent le format de la mémoire et sont efficaces.

Les opérations unaires peuvent être gérées de manière générique en vérifiant si un bloc de mémoire est « dense » - c'est-à-dire si les éléments s'étendent sur une zone sans lacunes et que chaque emplacement de mémoire est utilisé exactement une fois. Il peut être vérifié avec un algorithme simple

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

Outillage performant

Pour les performances de débogage, nous devons ajouter la prise en charge du profileur pour :

  • voir où dans le programme les réorganisations réelles de la mémoire se produisent - c'est-à-dire suivre les appels à .contiguous()
  • suivi de l'implémentation invoquée
  • émettre des avertissements sur les changements de format de mémoire dans, par exemple, les opérations binaires (où l'annotation « soft » est utile)

Cette fonctionnalité peut être intégrée à un outil de profilage à la demande.

Manutention automatique

Il est logique de s'attendre à ce que la passe arrière s'exécute avec le même format de mémoire que la passe avant. Cela ne se produira pas toujours automatiquement car les dégradés entrants peuvent être arbitrairement foulés. Ainsi, la passe avant doit reconnaître explicitement le format de la mémoire, le stocker dans la fermeture automatique et s'appliquer au tenseur grad avant la fonction arrière.

Implémentation possible :

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(...)

Représentation au JIT

La proposition actuelle est d'avoir :

  • Pas encore de gestion de premier ordre pour le format de mémoire dans les annotations de type. Au lieu de cela, nous pouvons maintenir une carte lookaside dans la forme nécessaire pour les passes qui manipulent le format de la mémoire
  • Passe d'inférence (similaire à shape_inference) qui produit des annotations au format par valeur
  • Passes de transformation de format de mémoire (manuelles ou automatiques) qui trouvent si nécessaire des appels to(memory_format) à insérer pour des performances optimales

À des fins d'application, nous pouvons également utiliser des instructions telles que assert x.is_contiguous(channels_last) .

Remarque : la question se pose de savoir où stocker les informations sur ce périphérique particulier ayant une combinaison de formats de mémoire préférée (par exemple, qconv sur les routes x86 vers fbgemm qui implémente uniquement NHWC). Une option consiste à le mettre au niveau d'enregistrement op, cependant, l'annotation de format de mémoire ressemble plus à une information secondaire. Nous pouvons commencer par maintenir une carte globale quelque part dans la passe JIT qui indique les formats de mémoire préférés et les heuristiques associées. Si cela devient désordonné, nous pouvons passer à un mécanisme basé sur l'enregistrement.

Au-delà : mises en page bloquées

Alors que nous décidons d'ajouter des empilements de tenseurs plus complexes, l'utilisation d'un tenseur PyTorch de première classe pour cela pourrait ne pas être plausible en raison du coût et de la complexité de mise en œuvre élevés. Deux variantes sont possibles :

  • Représentations opaques comme les liaisons de type C personnalisées. Il s'agit d'une option à choisir pour le compactage en inférence où la diversité est plus élevée en termes d'optimisation des performances
  • Type de tenseur de première classe comme MKLDNNTensor avec certaines (mais pas toutes) des opérations liées à ce nouveau type

Une autre alternative consiste à implémenter la prise en charge native du blocage/du carrelage dans la classe PyTorch Tensor de base.

Relation tensorielle nommée

La proposition existante de NamedTensor est structurée comme un mécanisme de vérification de type sur les tenseurs - pour le moment, elle n'attribue aucune signification sémantique aux noms de dimension. Ainsi, la seule façon de déduire la signification du tenseur d'activation est de continuer à utiliser le format NCHW prédéterminé. Cela rend NamedTensor et les propositions actuelles orthogonales.

Si nous sommes prêts à spécifier en dur la signification de certains noms (comme « canaux », « largeurs »), les opérateurs peuvent utiliser ces informations pour accélérer la mise en œuvre. Ce serait un changement sémantique car les tenseurs d'entrée auraient logiquement le format de mémoire NHWC (pas NCHW comme aujourd'hui).

Art antérieur

TensorFlow prend en charge à la fois NHWC et NCHW au niveau de l'opérateur, via le paramètre data_format ; les valeurs acceptables sont ("NHWC", "NCHW") pour les entrées 4-d, ("NDHWC", "NCDHW") pour les entrées 5-d, ou channels_first / channels_last indépendamment de l'entrée dimensionnalité. Il appartient à l'utilisateur de gérer correctement le réglage du paramètre, c'est-à-dire qu'il n'est pas suivi automatiquement par le tenseur.

Caffe2 appelle ce paramètre est appelé order plutôt que data_format , mais il est toujours appliqué explicitement au niveau de l'opérateur individuel.


Annexe : Autres options envisagées

Question décisive : qu'est-ce que le code suivant imprime : tensor_in_nhwc_layout.size(1) - le nombre de canaux (parce que la valeur par défaut est NCHW dans PyTorch) ou la hauteur (parce que c'est ce qui est dans la disposition NHWC à la position 1).

Sur la base de cette réponse plusieurs options sont possibles :

  • Option A - Foulées (présentées ci-dessus). La disposition du tenseur est une représentation complètement interne. Comme la mise en œuvre, il est plus pratique de le faire avec des foulées.

    • .size(1) me renvoie des "canaux", mais la mémoire interne est agencée différemment

    • pro : ne change pas le code du modèle, mon modèle peut toujours faire de l'arithmétique de dimension directement. En fait, aucun des changements de l'API publique

    • inconvénients: dans l'implémentation de strides, de nombreux opérateurs appellent .contiguous() et peuvent accidentellement rétablir la mise en page

    • Inconvénients : Du point de vue de l'utilisateur, comprendre quelles sont les garanties du retour de l'opération est primordiale. Cette OMI élimine les approches uniquement sur les foulées, car il devient très difficile de comprendre le format dans lequel votre opération sera renvoyée, et il n'y a pas d'API pour dire « ignorez mes foulées, en fait, retournez simplement la chose contiguë à NCHW ». Ceci s'ajoute aux limitations ci-dessus.

  • Option B - Tenseur NHWC explicite. L'utilisateur manipule explicitement le tenseur qui a un ordre de dimension différent mais le tenseur lui-même n'en sait rien. Nous aurions besoin d'annotations au niveau de l'opérateur pour comprendre ce que l'utilisateur attend.

    • .size(1) renvoie « hauteur »

    • pro : pas de magie et très prévisible

    • inconvénients : changer de modèle d'un layout à un autre devient une opération complexe qui nécessite de tracer tous les accès à .size() et .reshape() (ou faut-il le rendre explicite dans l'API ?)

  • Option B' - Tenseur NHWC explicite avec indicateur de tracé . Identique à ci-dessus, mais nous permettons d'attacher une annotation au tenseur pour marquer sa disposition sémantique que les opérations consomment dans leur implémentation. Il n'y a donc pas besoin d'annotation au niveau de l'opérateur - un opérateur peut effectuer la répartition en fonction du drapeau de mise en page des entrées.
  • Option C - Tenseur nommé . ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading =h.2gbe5xpga3w9)

    • .size(1) renvoie « hauteur » mais nous demandons aux gens de NE PAS utiliser cette API et d'utiliser à la place .size('channel')

    • pro : très explicite et ce que veut l'utilisateur

    • inconvénient : ne résout pas le problème de transition, nous aurions besoin de forcer tout le code écrit avec la prise en compte de la disposition à utiliser des tenseurs nommés. Sinon - les mêmes problèmes que ci-dessus s'appliquent

  • L'option D-Layout est de type tenseur opaque . Traitez NHWC comme nous traitons MKLDNN ou SparseTensor - type de tenseur séparé avec un DispatchID différent. C'est comme l'option A mais avec des compromis différents sur le comportement par défaut - les opérations non implémentées échoueraient au lieu de revenir à NCHW.

    • .size(1) renvoie toujours « canaux »

    • pro : pas de magie et explicite, l'envoi séparé permet aux opérateurs de décider ce qu'ils veulent

    • pour/contre : tous les opérateurs nécessaires doivent être implémentés sur une mise en page différente, si une opération est manquante, l'utilisateur obtiendra une erreur explicite indiquant qu'elle n'est pas prise en charge

    • inconvénients: nous aurions probablement besoin d'interdire de nombreuses opérations dessus, par exemple les vues car les résultats attendus sont difficiles à prévoir

internals mkldnn triaged

Commentaire le plus utile

BTW pourquoi devons-nous créer un nouveau concept au lieu de simplement s'en tenir à layout ? Je ne pense pas que les représentations éparses aient un concept bien défini de mise en page comme "channels_last", donc nous n'avons pas besoin de représenter un produit de memory_formats * layouts ( layouts fait référence à l'utilisation actuelle ), mais seulement memory_format + layouts ce qui signifie qu'il devrait être possible d'utiliser le même argument qu'avant ? Pour moi c'est à la fois plus court, plus joli, et évitera d'étendre les signatures des usines à mille arguments.

Tous les 68 commentaires

Il y a un problème avec empty_like ; la sémantique actuellement définie est que vous supprimez toutes les informations de foulée, il n'est donc pas possible de conserver la mise en page et d'être BC.

@VitalyFedyunin est inscrit pour implémenter les bits .contiguous() et torch.memory_layout

Une question - pour un tenseur 4D x avec des tailles (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)

Nous avons une permutation étrange

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

Maintenant, nous vérifions s'il est contigu pour le format NHWC. En suivant votre logique comme ci-dessous

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)

Dans les deux cas, is_nhwc_contiguous(y) renverra True ?

C'est correct. Cependant, nous ne pouvons pas relayer uniquement les progrès car nous voulons éviter toute conversion en avant et en arrière pendant les opérations de copie, de destination et similaires.

Et si les foulées avaient le même ordre que le format de la mémoire ? Prenons comme exemple le tenseur 4D. Pour décrire un tenseur, on a sizes , strides et stride_indexes :

tailles en (n, c, h, w)
progrès dans l'ordre physique, c'est-à-dire

  • foulées de (n, c, h, w) si le format est nchw
  • foulées de (n, h, w, c) si le format est nhwc.

stride_indexes mappe les foulées à la taille de nchw :

  • (0, 1, 2, 3) si le format est nchw,
  • (0, 2, 3, 1) si le format est nhwc.

Pour le format nchw, c'est le même qu'avant. Pour nhwc, ce sera similaire.

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)

Cela peut également être étendu pour prendre en charge le format bloqué. Utilisez nChw16c comme exemple,

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)

Plus de détails peuvent être explorés plus en détail plus tard.

Pour les OP qui n'acceptent que le tenseur contigu nchw, ce sera du travail ici.

Alternativement, nous pouvons également modifier légèrement le prototype, disons

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

Ainsi, par défaut, il suppose que seul nchw est contigu. De cette façon, vous n'avez pas besoin de réécrire ces OP, ils seront réorganisés automatiquement sur nchw.

Nous nous efforçons de créer une API capable de représenter :

  • Tenseur avec un format de mémoire différent (au début, juste l'ordre des dimensions) présent dans PyTorch dans Eager et JIT. Les mises en page bloquées sont moins prioritaires mais restent agréables.
  • API exposées à l'utilisateur pour interroger et modifier le format de la mémoire
  • Les opérations de base de CNN étant capables de gérer des tenseurs d'entrée avec un format de mémoire différent et un routage vers une implémentation plus rapide correspondante
  • Capacité à déduire et à optimiser les formats de mémoire dans les passes JIT

Super proposition ! Puis-je expliciter ma compréhension pour voir si cela est correct (y compris des propositions pour la gestion des formats MKL-DNN):

Permettez-moi de penser qu'il y a eu une implémentation de cette proposition en tant que classe de "format". Tant qu'il fournit des requêtes et des modifications d'API comme virtuelles, nous pourrions faire l'héritage/les extensions qui correspondent aux formats complexes MKL-DNN. Ou d'autres méthodes tant qu'elles fournissent un cadre pour la gestion des formats, nous déchargeant de ces détails.

À propos de la mise en œuvre des OP, chaque OP pourrait avoir un format préféré qui maximise ses performances et un format compatible qui fonctionne. L'opérateur par élément (ou plus généralement, les OP limités par la mémoire) supposent n'avoir aucune préférence. OP produit son tenseur de résultats avec un objet "format". Cas)

@uyongw Je veux clarifier un peu plus votre premier exemple. Vous configurez l'exemple comme suit : "J'ai un tenseur NCHW, que j'ai ensuite transposé d'une manière étrange (donc maintenant il ressemble à NWCH); maintenant, je veux savoir s'il est contigu à NHWC." Mais ce n'est pas la bonne façon de voir les choses. Une meilleure formulation est : "J'ai un tenseur NHWC, que j'ai ensuite transposé en un tenseur NCHW."

Pour le dire différemment, il n'y a pas de signification intrinsèque aux dimensions physiques d'un tenseur (quand on ignore les foulées). Nous ne leur donnons de sens que lorsque nous considérons comment nous les référençons par rapport aux foulées.

Pour décrire un tenseur, nous avons des tailles, des foulées et des stride_indexes

Je pense que stride_indexes est un moyen pratique de réfléchir au problème, mais c'est strictement redondant avec les foulées, car tout ce que vous dites est "Appliquez cette permutation (inverse ?) aux foulées, puis traitez-la comme la vraies foulées.) @VitalyFedyunin et moi parlions de la façon dont il pourrait toujours être une bonne idée de mettre en cache ces informations d'une manière ou d'une autre, car il est difficile de reconstruire les informations à partir des foulées elles-mêmes.Mais cela est hors de portée de cette proposition.

Ainsi, par défaut, il suppose que seul nchw est contigu.

Oui, c'est ma lecture du plan.

@CaoZhongZ

Permettez-moi de penser qu'il y a eu une implémentation de cette proposition en tant que classe de "format". Tant qu'il fournit des requêtes et des modifications d'API comme virtuelles, nous pourrions faire l'héritage/les extensions qui correspondent aux formats complexes MKL-DNN. Ou d'autres méthodes tant qu'elles fournissent un cadre pour la gestion des formats, nous déchargeant de ces détails.

En fait, je ne pense pas que ce soit une description précise de la proposition. Le support de disposition de mémoire que la proposition prend en charge ici ne sont que des dispositions qui peuvent être exprimées à travers des foulées. Tout ce qui est inexprimable de cette façon (par exemple, la disposition des blocs) ne fonctionnera pas de cette façon et doit être pris en charge par notre mécanisme de "disposition" plus lourd.

Pour le dire différemment, il n'y a pas de signification intrinsèque aux dimensions physiques d'un tenseur (quand on ignore les foulées). Nous ne leur donnons de sens que lorsque nous considérons comment nous les référençons par rapport aux foulées.

En partie d'accord :-) Mais pas sur ce problème spécifique. Disons que j'ai déjà un tenseur nhwc. Ensuite, je le permute en nwhc. Je veux permuter davantage vers nhwc puis faire un contiguous(). Mais je l'ai déjà nhwc contigu. N'est-ce pas confus?

Je pense que stride_indexes est un moyen pratique de réfléchir au problème, mais c'est strictement redondant avec les foulées, car tout ce que vous dites est "Appliquez cette permutation (inverse?) aux foulées, puis traitez-la comme les vraies foulées.)

À mon humble avis, ce ne sera pas redondant avec les foulées, si vous avez des foulées en nhwc (physique). Parce que vous avez besoin d'un bon mappage avec des tailles (logique). Sinon, il n'y a aucun moyen de dire l'ordre réel.

BTW, il existe une approche plus simple en utilisant le mappage inverse. Disons, pour nchw, c'est (0, 1, 2, 3), pour nhwc, c'est (0, 3, 1, 2) au lieu de (0, 2, 3, 1). Cela dit que le stride_index lui-même est toujours NCHW également. Mais le problème est qu'il ne peut pas être étendu aux formats bloqués comme nChw16c ou OIhw16i16o.

Les formats bloqués nécessitent une implémentation d'opérateurs complètement différente ; pour cette raison, nous préférons ne pas les mélanger avec le 'format mémoire', qui est par définition censé être convivial avec tous les opérateurs existants et fonctionner avec des performances identiques ou meilleures.

En partie d'accord :-) Mais pas sur ce problème spécifique. Disons que j'ai déjà un tenseur nhwc. Ensuite, je le permute en nwhc. Je veux permuter davantage vers nhwc puis faire un contiguous(). Mais je l'ai déjà nhwc contigu. N'est-ce pas confus?

Il est difficile de comprendre votre exemple car vous utilisez certains termes de manière familière et la précision est nécessaire. Voici comment j'interprète ce que vous avez dit :

  • Un tenseur « nhwc » doit être conforme à cette proposition, « Tensor dont la disposition physique est NHWC, mais qui est stridé de sorte que la disposition logique soit NCHW. »
  • Pour "permuter un (tenseur dont la disposition logique est NCHW) tenseur à (disposition logique) NWHC" consiste à exécuter y = x.permute(0, 2, 3, 1) , puisque vous permutez la disposition logique , pas la disposition physique. (Je soupçonne que ce n'est pas ce que vous vouliez dire, car dans votre message d'origine, vous avez mentionné la permutation x.permute(0, 3, 1, 2)
  • Pour ensuite permuter davantage un tenseur NWHC (disposition logique) en (disposition logique) NHWC consiste à appliquer la permutation z = y.permute(0, 2, 3, 1) . Vous avez donc maintenant un tenseur dont la disposition logique coïncide avec la disposition physique. Cela signifie que si nous demandons z.contiguous() nous obtiendrons vrai (et, ce qui prête à confusion, z.contiguous(memory_layout=NCHW) sera également vrai.) Mais ce ne sera PAS contigu NHWC.

Je ne pense pas que ce soit l'exemple que vous aviez en tête, auquel cas vous devrez être plus précis sur ce que vous entendez par "permuter".

À mon humble avis, ce ne sera pas redondant avec les foulées, si vous avez des foulées en nhwc (physique). Parce que vous avez besoin d'un bon mappage avec des tailles (logique). Sinon, il n'y a aucun moyen de dire l'ordre réel.

Ceci est le point crucial de la proposition: nous privilégions NCHW comme la mise en page logique, toujours. Donc, si j'ai un tenseur 4D dont je ne connais rien, je suppose que sa disposition logique est NCHW. Cela lève l'ambiguïté. Si vous voulez traiter des tenseurs dont la disposition logique n'est pas NCHW, je pense que l'API, comme indiqué, vous rend la vie un peu difficile.

@dzhulgakov

Les opérations préservent le comportement du format de mémoire

Si les tenseurs physiques NHWC peuvent se produire uniquement par des foulées, c'est techniquement un bris de BC, à moins que vous ne les fassiez ne conserver le format de mémoire que lorsque la balise de format de mémoire est présente (mais il semble que vous ne vouliez pas que cela ait une signification sémantique, donc je Je ne suis pas sûr de ce que la proposition suggère actuellement.) Je ne sais pas si cela enfreint réellement le code de quiconque dans la pratique.

Si les tenseurs physiques NHWC peuvent se produire uniquement par des foulées, c'est techniquement un bris de BC, à moins que vous ne les fassiez ne conserver le format de mémoire que lorsque la balise de format de mémoire est présente (mais il semble que vous ne vouliez pas que cela ait une signification sémantique, donc je Je ne suis pas sûr de ce que la proposition suggère actuellement.) Je ne sais pas si cela enfreint réellement le code de quiconque dans la pratique.

En supposant que nous puissions rendre le format de mémoire « collant ». Le tenseur formaté Op over memory produira un tenseur formaté en mémoire. Cela résoudra le problème de la Colombie-Britannique.

Cependant, nous devons définir un comportement d'opérations binaires (ou plusieurs membres) lorsque les tenseurs ont des formats de mémoire différents.

@ezyang Oh, je viens de

  1. J'ai un tenseur NCHW (physiquement, contigu).
  2. Ensuite, je le permute en NWHC (logiquement).
  3. Je souhaite le permuter davantage en NHWC avec un appel contiguious() suivi.
  4. Utilisez-le comme NHWC (physiquement).

Mais je l'ai déjà NHWC contigu après l'étape 2. Ensuite, je peux sauter l'étape 3 et l'utiliser comme NHWC directement à l'étape 4. Mais ce n'est sûrement pas correct car l'ordre physique du tenseur ne change pas du tout.

Les formats bloqués nécessitent une implémentation d'opérateurs complètement différente ; pour cette raison, nous préférons ne pas les mélanger avec le 'format mémoire', qui est par définition censé être convivial avec tous les opérateurs existants et fonctionner avec des performances identiques ou meilleures.

Oui, nous pouvons activer NHWC comme première étape. Cependant, je ne pense pas réellement que le format bloqué soit vraiment quelque chose de totalement différent. Il peut être exprimé naturellement (avec une bonne abstraction). S'il existe une description générale du format, les autres peuvent simplement enregistrer de nouveaux formats avec des blocages/pas arbitraires.

De plus, si nous avons déjà bloqué le support, nous ne prenons pas la peine de créer des constructions cachées pour exécuter tout sous-jacent, ce qui crée un monde implicite à l'intérieur et le de/vers entre les deux mondes peut devenir un problème.

Quoi qu'il en soit, il est peut-être trop éloigné de penser au format bloqué. Mais je pense que si possible, il vaut mieux rendre le design extensible.

Mais je l'ai déjà NHWC contigu après l'étape 2. Ensuite, je peux sauter l'étape 3 et l'utiliser comme NHWC directement à l'étape 4. Mais ce n'est sûrement pas correct car l'ordre physique du tenseur ne change pas du tout.

OK, je comprends votre exemple maintenant. Vous pouvez en effet vous arrêter à l'étape 2 et l'utiliser comme s'il s'agissait d'un tenseur NCHW ; auquel cas, vous interpréterez incorrectement W comme C, etc. C'est certainement un inconvénient avec l'implémentation basée sur la foulée ( @dzhulgakov , nous devrions probablement l'ajouter à la proposition). La proposition contient des dispositions pour ce cas :

Pour résoudre le problème ci-dessus, la proposition initiale consiste à introduire une balise de format de mémoire « soft » sur le tenseur qui enregistre le dernier appel à (memory_format) effectué sur le tenseur. Les opérateurs devraient propager cette annotation aux sorties. L'annotation est « douce », nous n'aurons donc pas d'erreur matérielle sur les annotations qui ne correspondent pas, mais produirons plutôt des avertissements en mode de profilage.

L'étiquette de format de mémoire logicielle vous permettrait de distinguer un tenseur NCHW que vous avez permuté d'un tenseur qui est en fait, physiquement, NHWC. Mais la balise souple dans sa forme actuelle n'est pas contraignante, donc je ne sais pas à quel point elle serait utile dans ce cas.

Une autre façon de résoudre le problème est d'utiliser des tenseurs nommés. Avec les tenseurs nommés, nous pouvons utiliser les noms sur les dimensions (logiques) pour déterminer si nous considérons un tenseur comme NCHW (la valeur par défaut supposée) ou autre chose.

Cependant, je ne pense pas réellement que le format bloqué soit vraiment quelque chose de totalement différent. Il peut être exprimé naturellement (avec une bonne abstraction). S'il existe une description générale du format, les autres peuvent simplement enregistrer de nouveaux formats avec des blocages/pas arbitraires.

Il y a plus de commentaires sur le sujet ici : https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374

@ezyang Merci pour la réponse. Oui, la balise de format souple peut aider. Le problème est qu'il n'est peut-être pas assez flexible car l'ordre des dimensions peut être arbitraire. De plus, il n'est pas lui-même calculable. Le tenseur nommé a une signification sémantique pour chaque dimension, mais peut avoir besoin de plus de facilités pour prendre en charge, j'en doute.

Personnellement, je pense que cela peut être résolu en introduisant une carte de l'ordre des foulées (physique) à l'ordre des tailles NCHW (logique). Comme je l'ai proposé ci-dessus, pour NCHW, c'est presque la même chose que la conception actuelle ; pour NHWC, sizes est toujours NCHW, strides sera dans l'ordre (N, H, W, C). Et nous utilisons stride_index = (0, 2, 3, 1) pour spécifier l'indice de dimension des foulées.

De plus, la combinaison de strides et stride_index peut être utilisée pour représenter n'importe quel format de tenseur. Cela peut donner à d'autres la possibilité d'enregistrer un nouveau format de données.

@ezyang

Les opérations préservent le comportement du format de mémoire

Si les tenseurs physiques NHWC peuvent se produire uniquement par des foulées, c'est techniquement un bris de BC, à moins que vous ne les fassiez ne conserver le format de mémoire que lorsque la balise de format de mémoire est présente (mais il semble que vous ne vouliez pas que cela ait une signification sémantique, donc je Je ne suis pas sûr de ce que la proposition suggère actuellement.) Je ne sais pas si cela enfreint réellement le code de quiconque dans la pratique.

Lorsque les opérations arithmétiques et le seuil ont été déplacés vers TensorIterator, cela brisait techniquement BC (car le format de mémoire des opérandes n'était pas conservé, et TensorIterator le préserve). Le statu quo est maintenant très incohérent - le seuil préserve la disposition, toutes les autres opérations unaires ne le font pas, torch.where ne le fait pas, les opérations arithmétiques préservent la disposition si les deux opérandes ont la même disposition, mais seraient par défaut "nchw" ou un tenseur qui est contiguous dans la compréhension actuelle s'il y a un décalage, je ne suis pas sûr de ce qui se passe pour la diffusion.
Vous faites également un bon point à propos de empty_like et la préservation de la mise en page similaire n'étant pas BC. Peut-être aura-t-il également besoin d'un argument de mise en page, comme is_contuous dans la proposition

x.is_contiguous(torch.memory_format.channels_first)

@ezyang @ngimel

Il y a un problème avec empty_like ; la sémantique actuellement définie est que vous supprimez toutes les informations de foulée, il n'est donc pas possible de conserver la mise en page et d'être BC.

Vous faites également un bon point à propos de empty_like et de la préservation de la mise en page similaire qui n'est pas BC.

Si nous ne comptons pas sur les foulées pour exprimer l'ordre physique, empty_like ne casse pas nécessairement BC. Il existe 3 types d'informations de dimension dans le tenseur :

  • forme: tailles
  • ordre logique : informations de commande enregistrées par foulées (généralement utilisées pour prendre en charge la transposition ou la permutation)
  • ordre physique : NCHW ou NHWC (peut être adressé comme stride_index comme je l'ai proposé).

Actuellement, l'ordre physique est le même que la forme/les tailles. Donc, nous abandonnons simplement l'ordre logique par enjambées. Considérons que nous découplons la forme et l'ordre physique, nous pouvons également simplement supprimer l'ordre logique mais conserver la forme et l'ordre physique pour empty_like . Cela signifie que size() et stride_index() seront conservés, mais que stride() sera réinitialisé. En particulier, empty_like d'un tenseur NHWC renverra un tenseur contigu NHWC avec les mêmes informations de forme spécifiées.

@uyongw Je ne suis pas sûr que ce serait une bonne idée de changer empty_like ; pour le moment, sa sémantique correspond à empty_like numpy .

Le statu quo est maintenant très incohérent - le seuil préserve la disposition, toutes les autres opérations unaires ne le font pas, torch.where ne le fait pas, les opérations arithmétiques préservent la disposition si les deux opérandes ont la même disposition, mais seraient par défaut "nchw" ou un tenseur contigu dans compréhension actuelle s'il y a un décalage, je ne suis pas sûr de ce qui se passe pour la diffusion.

@ngimel , oui, ceux-ci ne sont pas très cohérents en ce moment. Je pense qu'une partie du travail sur la façon de représenter le format de la mémoire consiste à amener nos opérateurs à un état cohérent

Le vide_like de @ zou3519 numpy que vous avez lié a order argument empty_like dans pytorch (il renvoie "nchw" - tenseur contigu, même si le prototype est non contigu)

Oh, je vois, j'ai lu ça trop vite. Dans ce cas, ce serait bien d'avoir aussi nos numpy de match empty_like et ce serait (probablement?) Bien d'avoir ici aussi la disposition de la mémoire

@ zou3519 Oui, ce que j'essaie de dire, c'est de conserver la sémantique actuelle (abandonner l'ordre logique comme @ezyang et @ngimel l'ont mentionné) et en même temps de préserver la disposition physique comme les valeurs par défaut de numpy. Ainsi, pour le prototype NCHW, le comportement sera le même qu'avant. Pour le prototype NHWC, son comportement sera toujours compatible, c'est-à-dire que le nouveau tenseur sera contigu NHWC, au lieu de contigu NCHW si vous ne modifiez pas l'implémentation actuelle.

Deux questions:

  • Que se passe-t-il si un tenseur NHWC est ajouté à un tenseur NCHW ?
  • Qu'en est-il de l'inconvénient de (B) en créant des méthodes telles que t.channel_dim() sur un tenseur qui renvoie la valeur entière indiquant où se trouve physiquement la dimension ? Cette approche peut même être nécessaire pour permettre à d'autres formats, comme les formats de blocs, d'être choisis sans modifications du réseau.

Si nous abordons le con de (B) avec le dernier point, alors (B) me semble préférable. C'est intuitivement clair et les erreurs logiques doivent être faciles à détecter. Toutes les opérations existantes peuvent également fonctionner sur le tenseur, car il ressemble à n'importe quel autre tenseur contigu. Les opérations qui peuvent comprendre la sémantique (analogue à la proposition de tenseur nommé) fonctionneront également comme prévu.

Le vide_like de @ zou3519 numpy que vous avez lié a order argument empty_like dans pytorch (il renvoie "nchw" - tenseur contigu, même si le prototype est non contigu)

Nous prévoyons de conserver le format dans de tels cas (pour les tenseurs formatés en mémoire)

Que se passe-t-il si un tenseur NHWC est ajouté à un tenseur NCHW ?
Le fonctionnement avec un tenseur formaté en mémoire renverra un tenseur formaté en mémoire. Si les deux tenseurs sont formatés en mémoire, le format de sortie sera déterminé par le premier tenseur.

J'ajouterais deux choses :

Nous prévoyons de conserver le format dans de tels cas (pour les tenseurs formatés en mémoire)

Nous aurions besoin d'auditer les utilisations existantes, car souvent les opérateurs appellent empty_like et supposent ensuite qu'ils sont contigus NCHW. Et je ne sais pas comment nous traiterions avec le code tiers. Il semble que nous aurions besoin d'une valeur par défaut différente de numpy si nous voulons préserver BC.

Le fonctionnement avec un tenseur formaté en mémoire renverra un tenseur formaté en mémoire. Si les deux tenseurs sont formatés en mémoire, le format de sortie sera déterminé par le premier tenseur.

J'ajouterais également, si vous vous souciez vraiment du format de votre sortie, transmettez un tenseur de sortie.

D'accord sur empty_like, il y a pas mal de cas où le résultat de empty_like/zeros_like etc est supposé nchw-contigu (physiquement contigu devrais-je dire, dans de nombreux cas ce ne sont pas des opérations d'image).
Le passage du tenseur de sortie n'est pas une option dans la plupart des cas, car les fonctions avec out kwarg ne sont pas dérivables.

Beaucoup de nos problèmes proviennent de l'incohérence des dispositions de sortie attendues. Nous ne pouvons pas tous les résoudre en même temps, mais nous pouvons essayer de verrouiller l'état actuel (au moins pour les foulées) et les cerner un par un. Voici donc la proposition.

API Python

Introduire le nouveau 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

Le tenseur nécessitera une conversion explicite du format de mémoire

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

Pour les "taguer" avec un format spécifique :

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)

Maintenant à propos de empty_like et similaire :

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

Car c'est en fait :

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

Si on veut garder le format :

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

De la même manière:

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

Cela signifie que nous pouvons définir lentement chaque fonction memory_format par défaut à l'état actuel du monde, les classer et être attentif à la façon dont nous les modifions à l'avenir.

Si vous spécifiez le tenseur, les TensorOptions sont actuellement ignorées (dans le meilleur des cas, elles lèvent une exception, par exemple une incompatibilité d'option de périphérique transmise avec le périphérique out tenseur

Format de mémoire censé être léger, donc toute permutation le perdra.

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)

Pas sûr du rembourrage, j'apprécierai de l'aide ici.

Cependant, nous pouvons faire x.to(memory_format=torch.memory_format.nhwc) 'tag' tensor avec le format approprié et retourner self

Multitraitement

Conservera le format de mémoire « tag »

Bloquer les formats de mémoire

L'API ci-dessus ne repose pas sur les dimensions/les foulées/les tailles, ce qui signifie que nous pouvons étendre les fonctionnalités à l'avenir en gardant la même API.

API internes

Les opérateurs pourraient se brancher en fonction du format de la mémoire

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

Si nous faisons memory_format en tant que TensorOptions, nous pouvons penser à une branche au niveau de la répartition (de la même manière que périphérique, mise en page)

Petit retour d'informations sur la proposition de

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

est beaucoup trop restrictif (parce que nous voulons également gérer 1D et 3D en plus de 2D), et channels_first/channels_last de la proposition originale étaient plus accommodants à cet effet.

D'accord, nous avons besoin d'une meilleure dénomination. channels_first sonne presque juste sauf que le lot passe en premier =)

J'aime ta dernière proposition. Est-ce que la gestion de .contiguous() changerait ? Auriez-vous besoin de .contiguous(memory_format=<...>) ? Si c'est le cas, et beaucoup d'opérations appellent simplement .contiguous(), il se peut qu'elles formatent toujours la mémoire de manière incorrecte. De nombreuses opérations aujourd'hui allouent également des sorties en tant que empty_like(), ce qui aurait le même effet. Le plan serait-il de les mettre à jour pour détecter le format de mémoire des entrées et effectuer les appels corrects contigus et empty_like ?

Pour le moment, nos utilisateurs (et toutes les bibliothèques) s'attendent .contiguous() ce que

Nous ne pouvons pas rompre ce contrat. Cependant, la bonne nouvelle est que dès que nous prendrons en charge l'option memory_format, JIT pourra comprendre quand il est plus efficace d'appeler .contiguous(memory_format=...) au lieu du format classique.

@VitalyFedyunin Supposons -nous que les opérations comme ci-dessous ne sont pas autorisées ?

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)

Une autre variante serait :

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 - pourquoi l'utilisateur appellerait-il .permute(0,2,3,1) en premier lieu ? Tous les tenseurs de cette proposition ont une taille sémantique de (n,c,h,w), ce qui signifie que size(1) renvoie vos canaux. C'est ce que la bibliothèque standard de PT suppose aujourd'hui et ce qu'elle supposerait également dans cette proposition. Donc, on n'appellerait probablement jamais .permute du tout

Un gestionnaire de contexte peut-il être utile pour permettre à l'utilisateur de remplacer le format de mémoire des tenseurs alloués dans la portée du gestionnaire à un format spécifique ?

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(...)

Je n'aime pas l'idée du gestionnaire de contexte, car cela desserrera le contrôle de memory_format.

Par exemple:

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

Lorsque memory_format explicite l'indique clairement :

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

Si nécessaire, nous pouvons ajouter une syntaxe pour permettre :

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

@raghuramank100 il n'est pas nécessaire de permuter.

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

Fera tout le sale boulot pour vous, en gardant le même ordre que dans x.

Donc:

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)

Et vous pouvez continuer à adresser nhwc dans ce format

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

@VitalyFedyunin Cela a du sens.

D'un point de vue utilisateur, la dénomination de la méthode (si elle reste ainsi) me semble trompeuse car "to" est déjà la méthode recommandée pour transférer Tensor vers différents appareils.

Aussi, qu'en est-il de quelque chose comme celui de Numpy pour convertir les tableaux C_ORDER et F_ORDER ?

numpy.asfortranarray()
numpy.ascontiguousarray()

On peut facilement imaginer quelque chose comme :

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

@VitalyFedyunin : Je comprends que la conversion vers un autre memory_format élimine le besoin pour les utilisateurs de permuter manuellement. Cependant, une fois cette fonctionnalité disponible dans la torche, que se passerait-il si les utilisateurs appelaient les fonctions dans l'ordre que j'ai décrit ci-dessus ? Nous devrions au moins avoir un message d'avertissement/d'erreur indiquant que la transformation de la mise en page a échoué.

@VitalyFedyunin : Je comprends que la conversion vers un autre memory_format élimine le besoin pour les utilisateurs de permuter manuellement. Cependant, une fois cette fonctionnalité disponible dans la torche, que se passerait-il si les utilisateurs appelaient les fonctions dans l'ordre que j'ai décrit ci-dessus ? Nous devrions au moins avoir un message d'avertissement/d'erreur indiquant que la transformation de la mise en page a échoué.

Cela ne sera possible que lorsque nous implémenterons des tenseurs nommés. Parce qu'en ce moment :

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

Personne ne peut me dire si je viens de créer nchw ou nhwc.

J'ai peut-être mal compris la proposition originale, mais l'étiquette de format de mémoire enregistrée n'est-elle pas censée lever l'ambiguïté de cette situation ?

@VitalyFedyunin Cela a du sens, nous devons nous assurer que cela est communiqué aux utilisateurs finaux lorsque cette API se stabilise.

@dzhulgakov @VitalyFedyunin Après avoir examiné #19975, j'ai de nouvelles inquiétudes concernant la balise de format de mémoire enregistrée dans le tenseur. Mon problème de base est de savoir comment décider si les opérations doivent préserver la balise mémoire ? À l'origine, j'avais pensé que seuls les opérateurs « conscients de la mise en page alternative » auraient besoin de cette intelligence. Mais en regardant le patch de Vitaly, je pense que certains opérateurs principaux auront également besoin d'être ajustés. Par exemple, considérons x[0] ; si x était auparavant un tenseur NHWC, alors je devrais sortir un tenseur HWC après avoir fait cela. Je suis assez sûr que le patch de Vitaly ne gère pas cela correctement, et je parie que ce serait très déroutant pour les utilisateurs. Peut-être que les seuls opérateurs concernés sont ceux qui s'amusent avec des foulées (auquel cas, ils ne sont pas trop nombreux et nous pouvons les auditer manuellement), mais cela semble être une chose que nous devrions faire. Qu'est-ce que tu penses?

Attendez, les tenseurs restent toujours indexés dans l'ordre de : 0-dim N ; 1re-dim C ; 2e-dim H ; 3e-dim W. Donc x[0] renvoie le tenseur avec 0-dim C ; 1ère dim H ; 2nd-dim W. Peu importe si x était la disposition de mémoire channel_first ou channels_last.

Sinon, memory_format n'a aucun sens et nous n'avons besoin que de permuter le tenseur.

Mon point est que la balise de format de mémoire n'est pas conservée. Si le tenseur d'entrée a été étiqueté channels_last , le nouveau tenseur est étiqueté any

cc @ zou3519 , la logique de propagation de la disposition ici me rappelle beaucoup de propagation de dimension nommée dans le travail de tenseur nommé.

Je suis toujours en train de rattraper cette proposition. Mais @ezyang nous pourrions garder une trace de la logique de propagation de la disposition en propageant un indicateur (ou nom) par dimension et cela équivaudrait alors à avoir nommé des tenseurs avec des conventions de nom

Ce serait bien si nous pouvions aligner exactement la logique de la balise mémoire et la logique du tenseur nommé, même si nous les avons comme deux chemins d'implémentation séparés au début.

La phase 1

Développe les fonctionnalités de deux fonctions de tenseur .is_contiguous et .contiguous (à la fois python et c++ api).

Remarque : Nous avons reçu plusieurs plaintes concernant la fonction .to(memory_format) et avons décidé de ne pas la prendre en charge.

  1. .contiguous désormais en charge l'argument facultatif de mot-clé uniquement - memory_format , qui peut être soit torch.contiguous_format soit torch.channels_last .

    • L'utilisation de torch.contiguous_format conservera le comportement .contiguous() existant.

    • L'appel de x.contiguous(memory_format=torch.channels_last) renvoie un nouveau tenseur qui conserve la même disposition sémantique (NCHW), mais a un modèle d'allocation de mémoire différent.

      x.contiguous(memory_format=torch.channels_last) s'attend

  2. .is_contiguous désormais en charge l'argument facultatif de mot-clé uniquement - memory_format , qui peut être soit torch.contiguous_format soit torch.channels_last .

    • x.is_contiguous(memory_format=torch.contiguous_format) conserve les mêmes fonctionnalités que x.is_contiguous() et reste inchangé.

    • x.is_contiguous(memory_format=torch.channels_last) renvoie vrai si A) le tenseur d'entrée est contigu en mémoire ET B) alloué dans la mémoire au format NWHC (ou similaire pour 3d,5d).

Remarque : À la fin de la phase, un x.is_contiguous(memory_format=torch.channels_last) calculera l'état du tenseur à chaque appel. Cette fonctionnalité sera mise à jour ultérieurement.

Phase 2

Conserver le format de la mémoire pour des opérations spécifiques :

  1. Les opérateurs unaires au niveau des éléments préservent le format de mémoire channel_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. Les opérateurs binaires au niveau des éléments ( add , sub , mul , div ) préservent le format de mémoire channel_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. Toutes les opérations sur les tailles, les foulées et les dims ordonnent de réinitialiser le format de la mémoire.

    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
    

Reste indécis

  1. Résultat de l'opération de remodelage (et similaire), si la sortie est 'channels_last' lisible

    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) # ?
    

    Remarque : Actuellement, memory_format n'est pas conservé

  2. Résultat de l'opération NHWC + NCHW. Est-ce NHWC ?

    Remarque : Actuellement NHWC + NCHW -> NHWC et NCHW + NHWC -> NHWC

Qu'en est-il des opérations comme cat/split ? Il leur sera utile de préserver le format de la mémoire.

@ezyang - concernant l'indexation, je pense que nous devrions nous arrêter quelque part. Les différentes configurations de mémoire ne sont pas entièrement transparentes et certaines opérations devraient être autorisées à les ignorer. Je dirais que x[0] devrait être autorisé à effacer la balise, y compris x[0].unsqueeze(0)

Comme Raghu l'a mentionné, cat/split devrait conserver la balise si possible car c'est un usage assez courant. Je pense que la règle générale devrait être que tant que l'exploitation ne change pas de rang ou ne réorganise pas l'axe de manière étrange, nous devons préserver la balise. Si le classement change - tous les paris sont ouverts.

Je suis d'accord dans certains cas, nous perdrons l'étiquette. Mais je ne serais pas d'accord sur x[0] . Cela me semble être un moyen très courant de passer de NCHW à CHW .

Après plusieurs conversations sur le fait qu'il est déroutant d'avoir des Tensors pour porter (ou non) le 'tag' channel_last, nous avons décidé de prendre le risque d'introduire un changement révolutionnaire et de promouvoir automatiquement les tenseurs au format channels_last.

Qu'est-ce que cela signifie pour l'API :

Tous les tenseurs 3d,4d,5d avec des foulées comme N,1,H,[W,[D]] obtiendront automatiquement le format de mémoire channel_last.

Pour que cela fonctionne, nous prendrons des précautions particulières pour garantir que les opérateurs sur les tenseurs channel_last qui génèrent les tenseurs channel_last auront au moins des performances similaires aux opérateurs sur les tenseurs contigus.

Dans le cas du pire scénario :
1) Les utilisateurs peuvent appeler .contuous() en sortie.
2) Nous écrirons du code de promotion automatique de manière à ce qu'il soit presque trivial de modifier ce comportement.

Les effets secondaires d'une telle promotion automobile sont :

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

D'autre part il peut résoudre le cas (après de légères modifications) :

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

À partir des conversions slack, à la demande de @ezyang

Natalia Gimelshein [14:19]
Donc je suppose qu'il n'y aurait pas de concept de tag.

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?```

Vitaly Fedyunin [8:23 AM]
z va être channels_last

Vitaly Fedyunin [8:25 AM]
si x1 n'est pas channels_last dans l'une des variantes proposées (à moins que nous ne modifiions la fonction chunk pour ne pas renvoyer de vues), la convolution le convertira au format contigu (channels_first) et renverra également contigu

Vitaly Fedyunin [9:12 AM]
@ngimel merci pour les commentaires, je pense que nous pouvons proposer une définition plus significative du channel_last pour couvrir la plupart des cas où des opérations de type vue sont impliquées. Vous tiendra au courant.

Natalia Gimelshein [9:36 AM]
répondu à un fil :
Donc ça a l'air d'être un problème, non ? Le fractionnement de la dimension des canaux est une chose relativement courante, par exemple dans les réseaux de type création. Donc, si le tenseur est le premier tenseur des canaux, la sortie de convolution sera les canaux d'abord (ce qui est un comportement intuitif, et très probablement ce que l'utilisateur veut), si le tenseur est le dernier des canaux, la sortie de convolution sera à nouveau les canaux d'abord ?

Natalia Gimelshein [9:39]
répondu à un fil :
Mais uniquement en raison du comportement d'addition non commutatif et du fait que y est le premier argument et les canaux en dernier, n'est-ce pas ? Quel serait le résultat pour x1+y ? Avons-nous quelque part des règles de propagation de la mise en page pour les opérations binaires ?

Vitaly Fedyunin [10:44]
1) Oui, c'est un problème que nous allons résoudre avec une proposition alternative. Je vais faire des tests maintenant et je vais l'écrire cette semaine (dans un jour ou deux).
2) x1+y - devrait également produire channels_last sinon c'est déroutant, et oui, nous aurons des règles de propagation de mise en page écrites.

Je pense que l'observation que j'ai faite à @VitalyFedyunin lorsque nous avons discuté de cela en personne (mais je ne pense pas que je me sois souvenu de l'écrire nulle part), c'est qu'il y a un degré de liberté dans la convolution, c'est-à-dire que quand ça devient un argument dont la disposition de la mémoire ne correspond à aucune qu'il sait implémenter efficacement, à quelle disposition doit-il être contigu ? Pour des raisons BC, la contiguïté aux canaux est d'abord requise, mais nous avons pris une décision arbitraire ici - on peut soutenir que vous pourriez également contiguiser aux canaux en dernier. Peut-être devrions-nous avoir une sorte de bascule locale de thread qui indique quelles sont les valeurs par défaut ?

Mais il semble qu'il y ait beaucoup de détails ici à débattre, et je ne suis pas sûr que cela fonctionne à la fin.

Ainsi, le flou de la convolution (et d'autres opérateurs sensibles à la mise en page, d'ailleurs, par exemple le suréchantillonnage que j'ai récemment examiné commence par appeler .contiguous() sur l'entrée - alors qu'est-ce que cela signifie ?) était la principale raison pour l'introduction de la balise, iirc.

Ouais, donc je suis d'accord pour ouvrir à nouveau le design de l'étiquette, mais alors nous
doivent résoudre sérieusement les problèmes de propagation de ces balises,
même lorsque vous perdez la mise en page (comme cela aurait été le cas avec le fractionnement
sur les canaux). J'aime beaucoup plus faire de la "mise en page actuelle"
sorte de gestionnaire de contexte, que de le rendre dépendant des données.

Extraits du message de ngimel du 2019-06-19 12:43:45 -0700:

Ainsi, le flou de la convolution (et d'autres opérateurs sensibles à la mise en page, d'ailleurs, par exemple le suréchantillonnage que j'ai récemment examiné commence par appeler .contiguous() sur l'entrée - alors qu'est-ce que cela signifie ?) était la principale raison pour l'introduction de la balise, iirc.

BTW pourquoi devons-nous créer un nouveau concept au lieu de simplement s'en tenir à layout ? Je ne pense pas que les représentations éparses aient un concept bien défini de mise en page comme "channels_last", donc nous n'avons pas besoin de représenter un produit de memory_formats * layouts ( layouts fait référence à l'utilisation actuelle ), mais seulement memory_format + layouts ce qui signifie qu'il devrait être possible d'utiliser le même argument qu'avant ? Pour moi c'est à la fois plus court, plus joli, et évitera d'étendre les signatures des usines à mille arguments.

L'option de mise en page a été envisagée (consultez l'annexe), mais nous avons constaté que cela entraînerait de nombreuses duplications de code et interdirait la conversion automatique des tenseurs en un autre memory_format à la volée

après tout memory_format est un moyen de tenseur stride et de choisir facilement des noyaux et des sorties optimisés qui sont la propriété du tenseur stridé, pas une classe complètement différente

Dans un certain sens, les dispositions clairsemées sont également un moyen de choisir facilement des noyaux optimisés pour des tableaux qui sont pour la plupart nuls

Cela peut être une question naïve, mais pourquoi PyTorch envisage-t-il cette API plutôt que d'exposer simplement une option pour utiliser NHWC dans les opérations elles-mêmes, ce qui appellerait directement le noyau CuDNN sous-jacent lorsqu'il est disponible ?

Il semble que pour un cas d'utilisation courant (mélange d'opérations d'images telles que conv et mise en commun avec des architectures LM), ce serait une solution simple. En tant que développeur, tout ce que je veux, c'est un Conv2d(..., nhwc=True) . Y a-t-il une raison pour laquelle cela n'a pas de sens?

@rewonc, nous avons envisagé une approche similaire (ajout d'une option aux opérateurs au lieu du noyau dérivé du striding) et avons eu du mal à l'appliquer pour les raisons suivantes :

  • Cette approche nécessitera que le noyau effectue un recadrage du tenseur contigu pour appliquer le noyau NHWC.
  • L'opérateur suivant devra à nouveau redéfinir l'entrée (à contigu) à moins qu'il n'ait également l'option nhwc=True .
  • Pour avoir NHWC sur le réseau, chaque opérateur aurait besoin nhwc=True option

PS. Si vous êtes préoccupé par les fonctions CudNN Ex , nous cherchons à exposer cudnn_batch_norm_nhwc et des opérateurs similaires.

Salut @VitalyFedyunin , nous avons vu que le tenseur nommé était pris en charge dans PyTorch 1.3. Cela peut-il résoudre (ou partiellement résoudre) les problèmes de prise en charge du format NHWC (ou même bloqué) ? Existe-t-il un plan pour faire avancer l'état NHWC basé sur le tenseur nommé ?

Nous avançons avec le dernier support des canaux, je publierai la feuille de route cette semaine ici et dans les canaux slack. Nous n'envisageons pas d'ajouter des formats bloqués de sitôt (car cela nécessitera la réécriture de TOUS les opérateurs).

Merci. Ce sera bien !

Tâches et progression à l'intérieur de https://github.com/pytorch/pytorch/issues/28619

Cette page vous a été utile?
0 / 5 - 0 notes