CNN-Operatoren verwenden die kanonische Ordnung der Tensordimensionen und weisen ihnen eine semantische Bedeutung zu. Für den 2D-Fall in PyTorch muss heute eine Eingabe in Fackel.nn.Conv2d ein 4D-Tensor in NCHW-Reihenfolge sein -
Aus Leistungsgründen ist es oft von Vorteil, Dimensionen anders anzuordnen, damit der Speicher, auf den durch bestimmte Operationen zugegriffen wird, zusammenhängend angeordnet ist und die Lokalität besser ausgenutzt wird. Die häufigste Option ist das Verschieben von Dimensionen zum Ende hin - NHWC. Es kann noch komplexere Speicherformate geben, die eine Dimension in Blöcke aufteilen, z
Beispielbibliotheken, die es verwenden, umfassen:
Die Herausforderung besteht darin, dass die Transformation der Dimensionsreihenfolge selbst teuer ist. Wenn also mehrere CNNs-Operationen hintereinander ausgeführt werden (z. B. conv(relu(conv)))
), ist es von Vorteil , einmal in das andere Speicherformat zu transformieren , Operationen auszuführen und sie neu anzuordnen zurück.
Daher ist es wichtig, PyTorch unterschiedliche Dimensionsreihenfolgen bewusst zu machen und Tensoren mit unterschiedlichen Speicherformaten zwischen Operationen sowohl im Eager- als auch im JIT-Modus zu übergeben. Darüber hinaus ist es von Vorteil, über automatische JIT-Optimierungsdurchgänge zu verfügen, die versuchen, Heuristiken oder Suchtechniken anzuwenden, um herauszufinden, ob das Ändern des Speicherformats in Bezug auf die Leistung von Vorteil ist und wo es im Modell sinnvoll ist, dies zu tun.
Wir bemühen uns, eine API zu entwickeln, die Folgendes darstellen kann:
Terminologie : Das obige Problem wird oft als „layout“ (mxnet), „data_format“ (tf), „image_format“ (keras), „order“ (caffe2) bezeichnet. Wir schlagen vor, in PyTorch den Namen „memory format“ oder „memory_format“ zu verwenden. Der Name „layout“ wird in PyTorch leider mit den Werten 'strrided' vs 'sparse_coo' verwendet, so dass die Namensoption nicht zur Verfügung steht.
Folgende Operatoren sollten mindestens das Speicherformat berücksichtigen. Sie müssen nicht nur das richtige Ergebnis liefern, sondern auch die beste Leistung der zugrunde liegenden Bibliotheken liefern UND das Speicherformat der Ausgaben
Definieren Sie das Konzept des Speicherformats in PyTorch:
torch.memory_format.channels_first
. Sie haben keinen angegebenen Typ und können beliebige vergleichbare Objekte sein (beginnen wahrscheinlich mit enum, könnten aber in Zukunft andere Objekte sein, die mit dem Konzept des benannten Tensors interoperieren).torch.channels_first
direkt verwendenchannels_first
und channels_last
(um weniger Konstanten zu ermöglichen)Fügen Sie Tensor folgende Methoden hinzu:
x.is_contiguous(torch.memory_format.channels_first)
x.to(memory_format=torch.memory_format.channels_first)
Hinweis : x.get_memory_format()
Funktion, sondern nur explizite Überprüfungen - sie ermöglicht eine breitere Palette möglicher Implementierungen. Vielleicht möchten wir es jedoch hinzufügen.
Das semantische Layout der Tensoren bleibt immer gleich - NCHW! x.size()
immer (n,c,h,w)
Operationen bewahren das Speicherformatverhalten:
Das Speicherformat ist eine Eigenschaft eines Tensors, die durch Serialisierung/Deserialisierung erhalten bleibt (falls der Tensor ein Parameter ist).
Tensor in PyTorch hat heute ein Konzept von Schritten, die angeben, wie der logische Tensor im Speicher angeordnet ist . Genauer gesagt hat jeder Tensor einen strides
Vektor der gleichen Länge wie sizes
. Um Elemente in der logischen Indizierung (i1, i2, .., ik)
zu indizieren, macht man das Punktprodukt mit Schritten und sucht den Speicher bei offset + i0*stride0 + i1*stride1 + ... * ik * stridek
. Aneinandergrenzende Tensoren haben somit Schritte, die umgekehrte kumulative Größenprodukte sind. Zum Beispiel hat ein 4D-Tensor mit den Größen (n,c,h,w)
Schritte (c*h*w, h*w, w, 1)
.
Strides können verwendet werden, um verschiedene Speicherformate (die eine Dimensionsneuordnung sind) physisch darzustellen, während die logische Standard-NCHW-Reihenfolge beibehalten wird. Es gibt eine effektive Definition der Speicherformattransformation als:
# 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()
Im NHWC-Format ist der Schrittvektor (c*h*w, 1, c*w, c)
. Somit sind im Speicherpuffer die Gewichtungen für NHWC in zusammenhängender Reihenfolge.
Strides können zum Testen verwendet werden:
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()
Vorteile dieses Ansatzes:
Nachteile :
.contiguous()
entspricht dem Wechsel zu NCHW und kann versehentlich vom Benutzer oder innerhalb einer der Ops erfolgenDas größte potenzielle Problem ist eine unklare Benutzerabsicht . Es gibt keine Möglichkeit zu unterscheiden, ob der Benutzer wirklich ein anderes Speicherformat wollte oder ob der Eingabetensor zufällig auf diese Weise schritt. Konkret führt dies zu einer Verhaltensänderung für die bestehenden Operationen - heute kann die Faltung nur NCHW-zusammenhängende Tensoren erzeugen, selbst wenn die Eingabe willkürlich ist, in einer neuen Welt könnte sie die Eingabe als NHWC erkennen und daher auch NHWC zurückgeben. Es ändert die Semantik nicht, führt jedoch zu schwer zu debuggenden Leistungsproblemen. Eine mögliche Lösung könnte darin bestehen, Tensoren explizit mit dem benutzerdefinierten Flag memory_format zu kennzeichnen und nur dieser Anmerkung (zusätzlich zu den Schritten) zu folgen.
Um das obige Problem zu lösen, besteht der erste Vorschlag darin, ein „weiches“ Speicherformat-Tag auf dem Tensor einzuführen, das den letzten to(memory_format)
Aufruf auf dem Tensor aufzeichnet. Operatoren müssten diese Anmerkung an die Ausgaben weitergeben. Die Annotation ist „weich“, sodass wir bei nicht übereinstimmenden Annotationen keine Fehler machen, sondern im Profiling-Modus Warnungen ausgeben.
Die Signatur der bestehenden Operatoren ändert sich nicht. Operatoren können innerhalb des Operators einen fest codierten Dispatch durchführen, um eine schnellere Implementierung zu ermöglichen. Wenn keine Implementierung verfügbar ist, ist ein Round-Tripping durch verschiedene Speicherformate möglich. Alternative wäre eine Fehlermeldung.
def maxpool(x: Tensor):
if x.is_contiguous(torch.layout.NHWC):
return max_pool_impl_nhwc(x)
return max_pool_impl_default(x.contiguous())
Es wird bevorzugt, ein einzelnes Symbol wie 'conv' zu verwenden, um auf die Operatoren in JIT IR zu verweisen, anstatt separate Operatoren wie 'conv_nhwc' zu erstellen. Der Grund dafür ist die Einfachheit und das Halten von IR auf der Ebene der semantischen Repräsentation.
Wir müssen sicherstellen, dass Kernoperationen wie elementweise das Speicherformat beibehalten und effizient sind.
Unäre Operationen können generisch gehandhabt werden, indem überprüft wird, ob ein Speicherblock „dicht“ ist – dh ob Elemente einen Bereich ohne Lücken überspannen und jede Speicherstelle genau einmal verwendet wird. Es kann mit einem einfachen Algorithmus überprüft werden
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
Für die Debugging-Leistung sollten wir dem Profiler Unterstützung für Folgendes hinzufügen:
Diese Funktionalität kann in ein On-Demand-Profiling-Tool integriert werden.
Es ist logisch zu erwarten, dass der Rückwärtsdurchlauf mit dem gleichen Speicherformat wie der Vorwärtsdurchlauf ausgeführt wird. Dies geschieht nicht immer automatisch, da eingehende Gradienten willkürlich schritten sein können. Der Vorwärtspass muss also das Speicherformat explizit erkennen, in Autograd-Abschluss speichern und vor der Rückwärtsfunktion auf den Grad-Tensor anwenden.
Mögliche Umsetzung:
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(...)
Aktueller Vorschlag ist:
to(memory_format)
Aufrufe finden, müssen für eine optimale Leistung eingefügt werdenZu Durchsetzungszwecken können wir auch Aussagen wie assert x.is_contiguous(channels_last)
.
Hinweis: Es stellt sich die Frage, wo Informationen darüber gespeichert werden sollen, dass ein bestimmtes Gerät eine bevorzugte Speicherformatkombination hat (z. B. qconv auf x86-Routen zu fbgemm, das nur NHWC implementiert). Eine Möglichkeit besteht darin, es auf der Ebene der Op-Registrierung zu platzieren, jedoch fühlt sich die Annotation des Speicherformats eher wie eine Nebeninformation an. Wir können damit beginnen, dass wir irgendwo im JIT-Pass eine globale Karte pflegen, die bevorzugte Speicherformate und zugehörige Heuristiken bezeichnet. Wenn es unordentlich wird, können wir auf einen registrierungsbasierten Mechanismus umstellen.
Da wir uns entscheiden, komplexere Packungen von Tensoren hinzuzufügen, ist die Verwendung des erstklassigen PyTorch-Tensors aufgrund der hohen Implementierungskosten und der Komplexität möglicherweise nicht plausibel. Zwei Alternativen sind möglich:
Eine weitere Alternative besteht darin, die native Unterstützung für Blockieren/Kacheln in der Kernklasse PyTorch Tensor zu implementieren.
Der bestehende Vorschlag für NamedTensor ist als Typprüfungsmechanismus für Tensoren strukturiert - im Moment weist er Dimensionsnamen keine semantische Bedeutung zu. Die einzige Möglichkeit, auf die Bedeutung des Aktivierungstensors zu schließen, besteht darin, weiterhin das vorgegebene NCHW-Format zu verwenden. Es macht NamedTensor und die aktuellen Vorschläge orthogonal.
Wenn wir bereit sind, die Bedeutung einiger Namen (wie "Kanäle", "Breiten") fest zu spezifizieren, können Operatoren diese Informationen verwenden, um eine schnellere Implementierung zu erreichen. Es wäre jedoch eine semantische Änderung, da die Eingabetensoren logischerweise das Speicherformat NHWC (nicht NCHW wie heute) haben würden.
TensorFlow unterstützt sowohl NHWC als auch NCHW auf Bedienerebene über den Parameter data_format
; akzeptable Werte sind („NHWC“, „NCHW“) für 4-d-Eingänge, („NDHWC“, „NCDHW“) für 5-d-Eingänge oder channels_first
/ channels_last
unabhängig vom Eingang Dimensionalität. Es liegt in der Hand des Benutzers, die Einstellung des Parameters korrekt zu handhaben, dh er wird nicht automatisch vom Tensor nachgeführt.
Caffe2 ruft diesen Parameter auf und heißt order
statt data_format
, aber er wird immer noch explizit auf die Ebene einzelner Operatoren angewendet.
Lackmusfrage: Was gibt der folgende Code aus: tensor_in_nhwc_layout.size(1)
- die Anzahl der Kanäle (weil die Standardeinstellung in PyTorch NCHW ist) oder die Höhe (da dies im NHWC-Layout an Position 1 steht).
Basierend auf dieser Antwort sind mehrere Optionen möglich:
Es gibt ein Problem mit empty_like
; Die derzeit definierte Semantik sieht vor, dass alle Schrittinformationen gelöscht werden, sodass es nicht möglich ist, das Layout beizubehalten und BC zu sein.
@VitalyFedyunin ist angemeldet, um die Bits .contiguous()
und torch.memory_layout
zu implementieren
Eine Frage - für einen 4D-Tensor x
mit den Größen (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)
Wir haben eine seltsame Permutation
y = x.permute(0, 3, 1, 2)
# y.size(): (n, w, c, h)
# y.stride(): (c*h*w, 1, h*w, w)
Jetzt prüfen wir, ob es für das NHWC-Format zusammenhängend ist. Folgen Sie Ihrer Logik wie unten
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)
In beiden Fällen wird is_nhwc_contiguous(y)
True zurückgeben?
Das ist richtig. Wir können uns jedoch nicht nur auf die Schritte verlassen, da wir jegliche Konvertierungen hin und her beim Kopieren, in und ähnlichen Operationen vermeiden möchten.
Was ist, wenn Schritte dieselbe Reihenfolge wie das Speicherformat haben? Nehmen wir als Beispiel den 4D-Tensor. Um einen Tensor zu beschreiben, haben wir sizes
, strides
und stride_indexes
:
Größen in (n, c, h, b)
Schritte in physischer Reihenfolge, dh
stride_indexes ordnet Schritte der nchw-Größe zu:
Für das nchw-Format ist dies dasselbe wie zuvor. Bei nhwc wird es ähnlich sein.
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)
Dies kann auch erweitert werden, um das blockierte Format zu unterstützen. Verwenden Sie nChw16c als Beispiel,
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)
Weitere Details können später genauer untersucht werden.
Für OPs, die nur nchw zusammenhängenden Tensor akzeptieren, wird das hier etwas Arbeit sein.
Alternativ können wir auch den Prototypen leicht verändern, sagen wir
def is_contiguous(format=nchw):
...
def contiguous(format=nchw)
...
Daher wird standardmäßig davon ausgegangen, dass nur nchw zusammenhängend ist. Auf diese Weise müssen Sie diese OPs nicht neu schreiben, sie werden automatisch in nchw umgeordnet.
Wir bemühen uns, eine API zu entwickeln, die Folgendes darstellen kann:
- Tensor mit unterschiedlichem Speicherformat (am Anfang nur Dimensionsreihenfolge) vorhanden in PyTorch in Eager und JIT. Blockierte Layouts haben eine niedrigere Priorität, sind aber trotzdem schön.
- Benutzerexponierte APIs zum Abfragen und Ändern des Speicherformats
- CNN-Kernoperationen können Eingabetensoren mit unterschiedlichem Speicherformat und Routing zu einer entsprechenden schnelleren Implementierung verarbeiten
- Möglichkeit, Speicherformate in JIT-Pässen abzuleiten und zu optimieren
Toller Vorschlag! Darf ich mein Verständnis ausdrücken, um zu sehen, ob es richtig ist (einschließlich Vorschläge für die Handhabung von MKL-DNN-Formaten):
Lassen Sie mich vermuten, dass dieser Vorschlag als "Format"-Klasse implementiert wurde. Solange es das Abfragen und Ändern der API als virtuelles Angebot bereitstellt, könnten wir die Vererbung/Erweiterungen durchführen, die zu komplexen MKL-DNN-Formaten passen. Oder andere Methoden, solange sie einen Rahmen für den Umgang mit Formaten bieten und diese Details an uns auslagern.
In Bezug auf die OPs-Implementierung könnte jedes OP ein bevorzugtes Format haben, das seine Leistung maximiert, und ein kompatibles Format, das funktionsfähig ist. Elementweise Operatoren (oder allgemeiner gesagt, speicherbegrenzte OPs) sollen keine Präferenz haben. OP erzeugt seinen Ergebnistensor mit einem "Format" -Objekt. Dieses Formatobjekt garantiert die Abfrage-/Änderungssemantik, die mit der Standard-Pytorch-Erwartung kompatibel ist, sowie dass es bestimmte Formate verarbeiten kann, wenn es als Serien von optimierten Funktionen aufgerufen wird (wie conv2d (ReLU (conv2d)) Fall)
@uyongw Ich möchte etwas mehr zu Ihrem ersten Beispiel
Anders ausgedrückt, die physikalischen Dimensionen eines Tensors haben keine intrinsische Bedeutung (wenn wir Schritte ignorieren). Wir geben ihnen nur eine Bedeutung, wenn wir uns überlegen, wie wir sie in Bezug auf Schritte beziehen.
Um einen Tensor zu beschreiben, haben wir Größen, Schritte und stride_indexes
Ich denke, stride_indexes
ist ein bequemer Weg, um über das Problem nachzudenken, aber es ist bei Schritten absolut überflüssig, weil Sie nur sagen: "Wende diese (umgekehrte?) Permutation auf die Schritte an und behandle das dann als die wahre Schritte.) @VitalyFedyunin und ich haben darüber gesprochen, dass es immer noch eine gute Idee sein könnte, diese Informationen in irgendeiner Weise zwischenzuspeichern, da es
Daher wird standardmäßig davon ausgegangen, dass nur nchw zusammenhängend ist.
Ja, das ist meine Lesart des Plans.
@CaoZhongZ
Lassen Sie mich vermuten, dass dieser Vorschlag als "Format"-Klasse implementiert wurde. Solange es das Abfragen und Ändern der API als virtuelles Angebot bereitstellt, könnten wir die Vererbung/Erweiterungen durchführen, die zu komplexen MKL-DNN-Formaten passen. Oder andere Methoden, solange sie einen Rahmen für den Umgang mit Formaten bieten und diese Details an uns auslagern.
Ich glaube nicht, dass dies eine genaue Beschreibung des Vorschlags ist. Die Speicherlayoutunterstützung, die der Vorschlag hier unterstützt, sind nur Layouts, die durch Schritte ausgedrückt werden können. Alles, was auf diese Weise unaussprechlich ist (zB Blocklayout), funktioniert auf diese Weise nicht und muss von unserem schwereren "Layout"-Mechanismus unterstützt werden.
Anders ausgedrückt, die physikalischen Dimensionen eines Tensors haben keine intrinsische Bedeutung (wenn wir Schritte ignorieren). Wir geben ihnen nur eine Bedeutung, wenn wir uns überlegen, wie wir sie in Bezug auf Schritte beziehen.
Stimme teilweise zu :-) Aber nicht bei diesem speziellen Problem. Sprich ich habe schon einen nhwc Tensor. Dann permutiere ich es in nwhc. Ich möchte weiter zu nhwc permutieren und dann ein contiguous() ausführen. Aber ich habe es nhwc zusammenhängend schon bekommen. Ist es nicht verwirrend?
Ich denke, stride_indexes ist eine bequeme Möglichkeit, über das Problem nachzudenken, aber es ist mit Strides strikt überflüssig, denn alles, was Sie sagen, ist "Wende diese (umgekehrte?) Permutation auf Strides an und behandle das dann als die wahren Schritte.)
IMHO, es wird nicht mit Strides überflüssig sein, wenn Sie Strides in nhwc (physisch) haben. Da braucht man ein richtiges Mapping mit Größen(Logik). Ansonsten gibt es keine Möglichkeit, die tatsächliche Reihenfolge zu bestimmen.
Übrigens, es gibt einen einfacheren Ansatz, indem man Reverse Mapping verwendet. Sagen wir, für nchw ist es (0, 1, 2, 3), für nhwc ist es (0, 3, 1, 2) anstelle von (0, 2, 3, 1). Das heißt, der stride_index selbst ist auch immer NCHW. Aber das Problem ist, dass es nicht auf blockierte Formate wie nChw16c oder OIhw16i16o erweitert werden kann.
Blockierte Formate erfordern eine völlig andere Implementierung von Operatoren; Aus diesem Grund ziehen wir es vor, sie nicht mit dem 'Speicherformat' zu mischen, das per Definition mit allen bestehenden Operatoren kompatibel sein und mit gleicher oder besserer Leistung arbeiten soll.
Stimme teilweise zu :-) Aber nicht bei diesem speziellen Problem. Sprich ich habe schon einen nhwc Tensor. Dann permutiere ich es in nwhc. Ich möchte weiter zu nhwc permutieren und dann ein contiguous() ausführen. Aber ich habe es nhwc zusammenhängend schon bekommen. Ist es nicht verwirrend?
Ihr Beispiel ist schwer zu verstehen, da Sie einige Begriffe umgangssprachlich verwenden und Genauigkeit erforderlich ist. So interpretiere ich das, was Sie gesagt haben:
y = x.permute(0, 2, 3, 1)
ausführen, da Sie das logische Layout und nicht das physische Layout permutieren. (Ich vermute, das ist nicht das, was Sie gemeint haben, denn in Ihrem ursprünglichen Beitrag haben Sie die Permutation x.permute(0, 3, 1, 2)
mentioned erwähntz = y.permute(0, 2, 3, 1)
anwenden. Jetzt haben Sie also einen Tensor, dessen logisches Layout mit dem physischen Layout übereinstimmt. Das bedeutet, dass, wenn wir z.contiguous()
fragen, wir wahr werden (und verwirrenderweise wird z.contiguous(memory_layout=NCHW)
auch wahr sein). Aber es wird NICHT NHWC zusammenhängend sein.Ich glaube nicht, dass dies das Beispiel ist, das Sie sich vorgestellt haben. In diesem Fall müssen Sie genauer sagen, was Sie mit "permutieren" meinen.
IMHO, es wird nicht mit Strides überflüssig sein, wenn Sie Strides in nhwc (physisch) haben. Da braucht man ein richtiges Mapping mit Größen(Logik). Ansonsten gibt es keine Möglichkeit, die tatsächliche Reihenfolge zu bestimmen.
Dies ist der Kern des Vorschlags: wir Privileg NCHW als logisches Layout, immer. Wenn ich also einen 4D-Tensor habe, von dem ich nichts weiß, gehe ich davon aus, dass sein logisches Layout NCHW ist. Das beseitigt die Mehrdeutigkeit. Wenn Sie mit Tensoren umgehen möchten, deren logisches Layout nicht NCHW ist, denke ich, dass Ihnen die API wie angegeben das Leben etwas schwer macht.
@dzhulgakov
Operationen bewahren das Speicherformatverhalten
Wenn physische NHWC-Tensoren nur durch Schritte auftreten können, ist dies technisch gesehen BC-brechend, es sei denn, Sie lassen das Speicherformat nur dann erhalten, wenn das Speicherformat-Tag vorhanden ist (aber es klingt so, als ob dies keine semantische Bedeutung haben soll, also ich Ich bin mir nicht sicher, was der Vorschlag derzeit vorschlägt.) Ich bin mir jedoch nicht sicher, ob dies tatsächlich den Code von jemandem in der Praxis bricht.
Wenn physische NHWC-Tensoren nur durch Schritte auftreten können, ist dies technisch gesehen BC-brechend, es sei denn, Sie lassen das Speicherformat nur dann erhalten, wenn das Speicherformat-Tag vorhanden ist (aber es klingt so, als ob dies keine semantische Bedeutung haben soll, also ich Ich bin mir nicht sicher, was der Vorschlag derzeit vorschlägt.) Ich bin mir jedoch nicht sicher, ob dies tatsächlich den Code von jemandem in der Praxis bricht.
Angenommen, wir können das Speicherformat "klebrig" machen. Op over speicherformatierter Tensor erzeugt speicherformatierten Tensor. Das wird das BC-Problem lösen.
Wir müssen jedoch ein Verhalten von binären (oder mehrgliedrigen) Operationen definieren, wenn Tensoren unterschiedliche Speicherformate haben.
@ezyang Oh, ich habe gerade festgestellt, dass sich in meiner obigen Antwort ein Tippfehler befindet. (Das tut mir leid. Das ursprüngliche Beispiel ist jedoch immer noch korrekt.) Lassen Sie es mich wie folgt neu formulieren:
Aber ich habe es schon nach Schritt 2 NHWC zusammenhängend bekommen. Dann kann ich Schritt 3 überspringen und direkt in Schritt 4 als NHWC verwenden. Aber das ist sicher nicht richtig, da sich die physikalische Ordnung des Tensors überhaupt nicht ändert.
Blockierte Formate erfordern eine völlig andere Implementierung von Operatoren; Aus diesem Grund ziehen wir es vor, sie nicht mit dem 'Speicherformat' zu mischen, das per Definition mit allen bestehenden Operatoren kompatibel sein und mit gleicher oder besserer Leistung arbeiten soll.
Ja, wir können NHWC als ersten Schritt aktivieren. Ich glaube jedoch nicht, dass das blockierte Format wirklich etwas ganz anderes ist. Es kann natürlich ausgedrückt werden (mit einer guten Abstraktion). Wenn es eine allgemeine Formatbeschreibung gibt, können andere einfach neue Formate mit willkürlichen Blockierungen/Schritten registrieren.
Wenn wir die Unterstützung bereits blockiert haben, machen wir uns nicht die Mühe, versteckte Konstrukte zu erstellen, um alles darunterliegende auszuführen, was eine implizite Welt im Inneren erzeugt und das Von/Bis zwischen den beiden Welten zu einem Problem werden kann.
Wie auch immer, es kann zu weit weg sein, über blockierte Formate nachzudenken. Aber ich würde denken, wenn möglich, besser, das Design erweiterbar zu machen.
Aber ich habe es schon nach Schritt 2 NHWC zusammenhängend bekommen. Dann kann ich Schritt 3 überspringen und direkt in Schritt 4 als NHWC verwenden. Aber das ist sicher nicht richtig, da sich die physikalische Ordnung des Tensors überhaupt nicht ändert.
Okay, jetzt verstehe ich dein Beispiel. Sie können tatsächlich bei Schritt 2 aufhören und es verwenden, als ob es ein NCHW-Tensor wäre; in diesem Fall interpretieren Sie W falsch als C usw. Dies ist definitiv ein Nachteil bei der schrittbasierten Implementierung (
Um das obige Problem zu lösen, besteht der erste Vorschlag darin, ein "weiches" Speicherformat-Tag auf dem Tensor einzuführen, das den letzten to(memory_format)-Aufruf aufzeichnet, der auf dem Tensor ausgeführt wurde. Operatoren müssten diese Anmerkung an die Ausgaben weitergeben. Die Annotation ist „weich“, sodass wir bei nicht übereinstimmenden Annotationen keine Fehler machen, sondern im Profiling-Modus Warnungen ausgeben.
Mit dem Soft-Memory-Format-Tag können Sie einen NCHW-Tensor, den Sie permutiert haben, von einem Tensor unterscheiden, der tatsächlich physisch NHWC ist. Aber das Soft-Tag in seiner jetzigen Form ist unverbindlich, daher bin ich mir nicht sicher, wie nützlich es für diesen Fall tatsächlich wäre.
Eine andere Möglichkeit, das Problem zu lösen, sind benannte Tensoren. Bei benannten Tensoren können wir die Namen der (logischen) Dimensionen verwenden, um herauszufinden, ob wir einen Tensor als NCHW (die angenommene Vorgabe) oder etwas anderes betrachten.
Ich glaube jedoch nicht, dass das blockierte Format wirklich etwas ganz anderes ist. Es kann natürlich ausgedrückt werden (mit einer guten Abstraktion). Wenn es eine allgemeine Formatbeschreibung gibt, können andere einfach neue Formate mit willkürlichen Blockierungen/Schritten registrieren.
Weitere Kommentare zum Thema gibt es hier: https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374
@ezyang Danke für die Antwort. Ja, ein Soft-Format-Tag kann helfen. Das Problem besteht darin, dass es möglicherweise nicht flexibel genug ist, da die Dimensionsreihenfolge beliebig sein kann. Auch ist es selbst nicht berechenbar. Der benannte Tensor hat für jede Dimension eine semantische Bedeutung, benötigt jedoch möglicherweise einige weitere Funktionen zur Unterstützung, die ich bezweifle.
Persönlich würde ich denken, dass dies durch die Einführung einer Karte von der Schrittreihenfolge (physisch) zur NCHW-Größenreihenfolge (logisch) gelöst werden kann. Wie ich oben vorgeschlagen habe, ist es für NCHW fast dasselbe wie das aktuelle Design; für NHWC ist sizes
immer noch NCHW, strides
wird in (N, H, W, C) Reihenfolge sein. Und wir verwenden stride_index
= (0, 2, 3, 1), um den Dimensionsindex der Schritte anzugeben.
Außerdem kann die Kombination von strides
und stride_index
verwendet werden, um jedes beliebige Tensorformat darzustellen. Dies kann anderen die Flexibilität geben, neue Datenformate zu registrieren.
@ezyang
Operationen bewahren das Speicherformatverhalten
Wenn physische NHWC-Tensoren nur durch Schritte auftreten können, ist dies technisch gesehen BC-brechend, es sei denn, Sie lassen das Speicherformat nur dann erhalten, wenn das Speicherformat-Tag vorhanden ist (aber es klingt so, als ob dies keine semantische Bedeutung haben soll, also ich Ich bin mir nicht sicher, was der Vorschlag derzeit vorschlägt.) Ich bin mir jedoch nicht sicher, ob dies tatsächlich den Code von jemandem in der Praxis bricht.
Als arithmetische Operationen und Schwellenwerte in TensorIterator verschoben wurden, war das technisch gesehen BC-breaking (da das Speicherformat der Operanden früher nicht beibehalten wurde und TensorIterator es behält). Der Status Quo ist jetzt sehr inkonsistent - Schwellenwert behält das Layout bei, alle anderen unären Operationen nicht, Fackel.where nicht, arithmetische Operationen behalten das Layout bei, wenn beide Operanden das gleiche Layout haben, aber standardmäßig "nchw" oder Tensor ist, der contiguous
Nach derzeitigem Verständnis bin ich mir nicht sicher, was bei der Übertragung passiert, wenn es eine Diskrepanz gibt.
Sie machen auch einen guten Hinweis darauf, dass empty_like
und dergleichen das Layout beibehalten, das nicht BC ist. Vielleicht wird auch ein Layout-Argument benötigt, wie is_contiguous im Vorschlag
x.is_contiguous(torch.memory_format.channels_first)
@ezyang @ngimel
Es gibt ein Problem mit empty_like; Die derzeit definierte Semantik sieht vor, dass alle Schrittinformationen gelöscht werden, sodass es nicht möglich ist, das Layout beizubehalten und BC zu sein.
Sie machen auch einen guten Hinweis darauf, dass empty_like und dergleichen das Layout beibehalten, das nicht BC ist.
Wenn wir uns nicht auf Schritte verlassen, um physische Ordnung auszudrücken, bricht empty_like
BC nicht unbedingt. Es gibt 3 Arten von Dimensionsinformationen im Tensor:
Derzeit entspricht die physische Bestellung der Form/Größe. Also lassen wir die logische Reihenfolge einfach fallen. Angenommen, wir entkoppeln Form und physikalische Ordnung, wir können auch einfach die logische Ordnung fallen lassen, aber die Form und die physikalische Ordnung für empty_like
beibehalten. Das bedeutet, dass sowohl size()
als auch stride_index()
erhalten bleiben, aber stride()
zurückgesetzt werden. Insbesondere empty_like
eines NHWC-Tensors gibt einen zusammenhängenden NHWC-Tensor mit denselben angegebenen Forminformationen zurück.
@uyongw Ich bin mir nicht sicher, ob es eine gute Idee wäre, empty_like
zu ändern; im Moment stimmt seine Semantik mit
Der Status quo ist jetzt sehr inkonsistent - Schwellenwert behält das Layout bei, alle anderen unären Operationen nicht, Fackel.where nicht, arithmetische Operationen behalten das Layout bei, wenn beide Operanden das gleiche Layout haben, aber standardmäßig "nchw" oder Tensor, der zusammenhängend ist, Nach derzeitigem Verständnis bin ich mir nicht sicher, was bei der Ausstrahlung passiert, wenn es eine Diskrepanz gibt.
@ngimel , ja, diese sind im
@zou3519 numpys empty_like, das Sie verlinkt haben, hat das Argument order
, das standardmäßig "dem Layout des Prototyps so genau wie möglich entspricht". Das ist nicht das, was empty_like
in pytorch derzeit tut (es gibt "nchw" zurück - zusammenhängender Tensor, auch wenn der Prototyp nicht zusammenhängend ist)
Oh, ich sehe, das habe ich zu schnell gelesen. In diesem Fall wäre es schön, auch unsere empty_like match numpys zu haben und es wäre (wahrscheinlich?) auch gut für das Speicherlayout hier
@zou3519 Ja, was ich damit sagen beizubehalten (die logische Reihenfolge wie @ngimel erwähnt zu
Zwei Fragen:
Wenn wir den Nachteil von (B) mit dem letzten Aufzählungspunkt ansprechen, dann erscheint mir (B) vorzuziehen. Es ist intuitiv klar und logische Fehler sollten leicht zu erkennen sein. Alle vorhandenen Ops können auch mit dem Tensor arbeiten, da er wie jeder andere zusammenhängende Tensor aussieht. Ops, die Semantik verstehen können (analog zum benannten Tensorvorschlag) werden auch wie erwartet funktionieren.
@zou3519 numpys empty_like, das Sie verlinkt haben, hat das Argument
order
, das standardmäßig "dem Layout des Prototyps so genau wie möglich entspricht". Das ist nicht das, wasempty_like
in pytorch derzeit tut (es gibt "nchw" zurück - zusammenhängender Tensor, auch wenn der Prototyp nicht zusammenhängend ist)
Wir planen, in solchen Fällen das Format beizubehalten (für speicherformatierte Tensoren)
Was passiert, wenn ein NHWC-Tensor zu einem NCHW-Tensor hinzugefügt wird?
Die Operation mit speicherformatiertem Tensor gibt speicherformatierten Tensor zurück. Wenn beide Tensoren speicherformatiert sind, wird das Ausgabeformat durch den ersten Tensor bestimmt.
Zwei Dinge würde ich hinzufügen:
Wir planen, in solchen Fällen das Format beizubehalten (für speicherformatierte Tensoren)
Wir müssten vorhandene Nutzungen überprüfen, da Operatoren oft empty_like
aufrufen und dann davon ausgehen, dass sie NCHW-zusammenhängend sind. Und ich weiß nicht, wie wir mit Code von Drittanbietern umgehen würden. Es scheint, als bräuchten wir einen anderen Standard als numpy, wenn wir BC beibehalten möchten.
Die Operation mit speicherformatiertem Tensor gibt speicherformatierten Tensor zurück. Wenn beide Tensoren speicherformatiert sind, wird das Ausgabeformat durch den ersten Tensor bestimmt.
Ich würde auch hinzufügen, wenn es Ihnen wirklich wichtig ist, in welchem Format Ihre Ausgabe erfolgt - übergeben Sie einen Ausgabetensor.
Stimmen Sie sich auf empty_like zu, es gibt einige Fälle, in denen das Ergebnis von empty_like/zeros_like usw.
Die Übergabe des Ausgabetensors ist in den meisten Fällen keine Option, da Funktionen mit out
kwarg nicht differenzierbar sind.
Viele unserer Probleme entstehen durch die Inkonsistenz der erwarteten Ausgabelayouts. Wir können sie nicht alle auf einmal lösen, aber wir können versuchen, den aktuellen Zustand (zumindest für Schritte) zu sperren und sie einzeln festzunageln. Hier also der Vorschlag.
Python-API
Einführung des neuen Torchens.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
Der Tensor erfordert eine explizite Speicherformatkonvertierung
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
Um sie mit einem bestimmten Format zu 'kennzeichnen':
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)
Nun zu empty_like und ähnlichem:
z = torch.empty_like(y)
z.is_contiguous() == True # For BC
Denn es ist tatsächlich:
z = torch.empty_like(y, memory_format=torch.memory_format.any )
Wenn wir das Format beibehalten möchten:
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
Ähnlich:
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
Das bedeutet, dass wir langsam jede Funktion memory_format defaults auf den aktuellen Zustand der Welt definieren, sie klassifizieren und darauf achten können, wie wir sie in Zukunft ändern.
Wenn Sie Tensor TensorOptions derzeit ignoriert werden angeben aus (im besten Fall werfen sie Ausnahme zum Beispiel übergeben wird , Geräteoption Mismatch mit out
Tensor - Gerät).
Das Speicherformat soll leicht sein, sodass es bei jeder Permutation verloren geht.
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)
Bei der Polsterung bin ich mir nicht sicher, werde hier Hilfe zu schätzen wissen.
Wir können jedoch x.to(memory_format=torch.memory_format.nhwc) 'tag' Tensor mit dem richtigen Format machen und self zurückgeben
Multiprocessing
Bewahrt das Speicherformat 'tag'
Speicherformate blockieren
Die obige API basiert nicht auf Abmessungen/Schritten/Größen, was bedeutet, dass wir die Funktionalität in Zukunft erweitern können, wobei die gleiche API beibehalten wird.
Interne APIs
Operatoren könnten basierend auf dem Speicherformat verzweigen
if (self.memory_format(nhwc)) {
// fast path
} else
{
// classic implementation
}
Wenn wir memory_format als TensorOptions verwenden, können wir über Verzweigungen auf Dispatch-Ebene nachdenken (ähnlich wie bei Gerät, Layout).
Kleines Feedback zum Vorschlag von
torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory
ist viel zu restriktiv (weil wir neben 2D auch 1D und 3D behandeln wollen), und channels_first/channels_last
aus dem ursprünglichen Vorschlag waren für diesen Zweck entgegenkommender.
Stimmen Sie zu, wir brauchen eine bessere Namensgebung. channels_first
klingt fast richtig, außer Batch geht zuerst =)
Dein neuster Vorschlag gefällt mir. Würde sich die Handhabung von .contiguous() ändern? Benötigen Sie .contiguous(memory_format=<...>)? Wenn dies der Fall ist und viele Operationen einfach .contiguous() aufrufen, könnten sie den Speicher immer noch falsch formatieren. Viele Operationen ordnen heute auch Ausgaben als empty_like() zu, was den gleichen Effekt hätte. Wäre der Plan, diese zu aktualisieren, um das Speicherformat der Eingänge zu erkennen und die richtigen zusammenhängenden und leeren_ähnlichen Aufrufe durchzuführen?
Was im Moment unsere Benutzer (und alle Bibliotheken) erwarten, dass .contiguous()
den Speicher zusammenhängenden Tensor mit Schritten in absteigender Reihenfolge zurückgibt.
Wir können diesen Vertrag nicht brechen. Die gute Nachricht ist jedoch: Sobald wir die Option memory_format unterstützen, könnte JIT verstehen, wann es effizienter ist, .contiguous(memory_format=...)
anstelle des klassischen Formats aufzurufen.
@VitalyFedyunin Gehen wir davon aus, dass Operationen wie unten nicht zulässig sind?
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)
Eine weitere Variante wäre:
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 - warum sollte der Benutzer überhaupt .permute(0,2,3,1)
anrufen? Alle Tensoren in diesem Vorschlag haben eine semantische Größe von (n,c,h,w), was bedeutet, dass size(1) Ihnen Kanäle zurückgibt. Davon geht die Standardbibliothek von PT heute und auch in diesem Vorschlag aus. Also würde man .permute wahrscheinlich nie nennen
Kann ein Kontextmanager nützlich sein, um dem Benutzer zu ermöglichen, das Speicherformat von zugewiesenen Tensoren innerhalb des Managerbereichs auf ein bestimmtes Format zu überschreiben?
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(...)
Ich mag die Idee des Kontextmanagers nicht, da er die Kontrolle über memory_format lockert.
Zum Beispiel:
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
Wenn explizites memory_format klar macht:
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
Bei Bedarf können wir Syntax hinzufügen, um Folgendes zu ermöglichen:
x = torch.randn(10,3,32,32, memory_format=torch.channels_last)
@raghuramank100 Es besteht keine Notwendigkeit, zu permutieren.
y = x.to(memory_format=torch.channels_last)
Wird alle Drecksarbeiten für Sie erledigen und die Dims-Reihenfolge wie in x beibehalten.
So:
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)
Und Sie können nhwc weiterhin in diesem Format ansprechen
nhwc[N][C][H][W]
@VitalyFedyunin Das macht Sinn.
Aus Anwendersicht erscheint mir die Benennung der Methode (wenn es so bleibt) irreführend, da "to" bereits der empfohlene Weg ist, Tensor auf verschiedene Geräte zu übertragen.
Was ist auch mit etwas wie Numpys zum Konvertieren von C_ORDER- und F_ORDER-Arrays?
numpy.asfortranarray()
numpy.ascontiguousarray()
Man kann sich leicht etwas vorstellen wie:
torch.randn(32, 3, 64, 64).to(device).as_nhwc()
@VitalyFedyunin : Ich verstehe, dass die Konvertierung in ein anderes memory_format die manuelle Permutierung durch die Benutzer überflüssig macht. Was würde jedoch passieren, wenn diese Funktionalität in der Fackel verfügbar ist, wenn Benutzer die Funktionen in der oben beschriebenen Reihenfolge aufrufen würden? Wir sollten mindestens eine Warn-/Fehlermeldung erhalten, die besagt, dass die Layout-Transformation fehlgeschlagen ist.
@VitalyFedyunin : Ich verstehe, dass die Konvertierung in ein anderes memory_format die manuelle Permutierung durch die Benutzer überflüssig macht. Was würde jedoch passieren, wenn diese Funktionalität in der Fackel verfügbar ist, wenn Benutzer die Funktionen in der oben beschriebenen Reihenfolge aufrufen würden? Wir sollten mindestens eine Warn-/Fehlermeldung erhalten, die besagt, dass die Layout-Transformation fehlgeschlagen ist.
Dies wird nur möglich sein, wenn wir benannte Tensoren implementieren. Denn im Moment:
x.zeros(10,10,10,10)
x = x.permute(0,2,3,1)
Niemand kann mir sagen, ob ich gerade nchw oder nhwc erstellt habe.
Vielleicht habe ich den ursprünglichen Vorschlag falsch verstanden, aber soll das aufgezeichnete Speicherformat-Tag diese Situation nicht eindeutig machen?
@VitalyFedyunin Es ist sinnvoll, wir müssen sicherstellen, dass dies den Endbenutzern mitgeteilt wird, wenn sich diese API stabilisiert.
@dzhulgakov @VitalyFedyunin Nach der Überprüfung von #19975 habe ich einige neue Bedenken bezüglich des aufgezeichneten Speicherformat-Tags im Tensor. Mein grundlegendes Problem ist, wie sollen wir entscheiden, ob Operationen Speicher-Tag beibehalten sollen? Ursprünglich hatte ich gedacht, dass nur "alternatives Layout-bewusste" Operatoren über diese Fähigkeiten verfügen müssen. Aber wenn ich mir Vitalys Patch anschaue, denke ich, dass auch einige Kernoperatoren angepasst werden müssen. Betrachten Sie beispielsweise x[0]
; Wenn x zuvor ein NHWC-Tensor war, sollte ich danach einen HWC-Tensor herausholen. Ich bin mir ziemlich sicher, dass Vitalys Patch dies nicht richtig handhabt, und ich wette, das würde die Benutzer sehr verwirren. Vielleicht sind die einzigen Operatoren, die davon betroffen sind, diejenigen, die mit Schritten herumalbern (in diesem Fall gibt es nicht allzu viele von ihnen und wir können sie manuell überprüfen), aber es scheint eine Sache zu sein, die wir tun sollten. Was denken Sie?
Warten Sie, Tensoren bleiben weiterhin in der folgenden Reihenfolge indiziert: 0-dim N; 1.Dim C; 2. Dim H; 3rd-dim W. Also gibt x[0] einen Tensor mit 0-dim C zurück; 1. Dim H; 2nd-dim W. Unabhängig davon, ob x das Speicherlayout "channels_first" oder "channels_last" war.
Ansonsten macht memory_format einfach keinen Sinn und wir brauchen nur den Tensor zu permutieren.
Mein Punkt ist, dass das Speicherformat-Tag nicht beibehalten wird. Wenn der Eingabetensor mit channels_last
markiert wurde, wird der neue Tensor mit any
markiert
cc @zou3519 , die Layout-Propagierungslogik hier erinnert mich stark an die
Ich hol diesen Vorschlag noch nach. Aber @ezyang könnten wir die Layout-Verbreitungslogik verfolgen,
Es wäre schön, wenn wir die Speicher-Tag-Logik und die benannte Tensor-Logik genau aneinanderreihen könnten, auch wenn wir sie am Anfang als zwei separate Implementierungspfade haben.
Erweitert die Funktionalität von zwei Tensorfunktionen .is_contiguous
und .contiguous
(sowohl Python- als auch C++-API).
Hinweis: Wir hatten mehrere Beschwerden über die Funktion .to(memory_format)
und haben uns entschieden, sie nicht zu unterstützen.
.contiguous
jetzt das optionale Nur-Schlüsselwort-Argument - memory_format
, das entweder torch.contiguous_format
oder torch.channels_last
.
Die Verwendung von torch.contiguous_format
behält das vorhandene Verhalten von .contiguous()
bei.
Der Aufruf von x.contiguous(memory_format=torch.channels_last)
gibt einen neuen Tensor zurück, der das gleiche semantische Layout (NCHW) beibehält, aber ein anderes Speicherzuweisungsmuster hat.
x.contiguous(memory_format=torch.channels_last)
erwartet, dass der Eingabetensor 3d, 4d oder 5d ist; und scheitert sonst.
.is_contiguous
jetzt das optionale Nur-Schlüsselwort-Argument - memory_format
, das entweder torch.contiguous_format
oder torch.channels_last
.
x.is_contiguous(memory_format=torch.contiguous_format)
behält dieselbe Funktionalität wie x.is_contiguous()
und bleibt unverändert.
x.is_contiguous(memory_format=torch.channels_last)
gibt true zurück, wenn A) der Eingabetensor im Speicher zusammenhängend ist UND B) im Speicher im NWHC-Format (oder ähnlich für 3d, 5d) zugewiesen wurde.
Hinweis: Am Ende der Phase 1 berechnet x.is_contiguous(memory_format=torch.channels_last)
den Zustand des Tensors bei jedem Aufruf. Diese Funktionalität wird später aktualisiert.
Speicherformat für bestimmte Vorgänge beibehalten:
Unäre elementweise Operatoren behalten das Speicherformat vonchannels_last bei.
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
Binäre elementweise Operatoren ( add
, sub
, mul
, div
) bewahren das Speicherformat von 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
Alle Operationen über Größen, Schritte und Dims ordnen das Speicherformat zurück.
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
Bleibt unentschlossen
Ergebnis der Umformung (und ähnlicher) Operation, wenn die Ausgabe 'channels_last' lesbar ist
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) # ?
Hinweis: Derzeit wird memory_format nicht beibehalten
Ergebnis des Betriebs NHWC + NCHW. Ist es NHWC?
Hinweis: Derzeit NHWC + NCHW -> NHWC und NCHW + NHWC -> NHWC
Was ist mit Operationen wie Cat/Split? Es wird für sie nützlich sein, das Speicherformat beizubehalten.
@ezyang - bezüglich der Indizierung denke ich, dass wir irgendwo aufhören sollten. Unterschiedliche Speicherlayouts sind nicht vollständig transparent und einige Ops sollten sie ignorieren dürfen. Ich würde argumentieren, dass x[0]
erlaubt sein sollte, das Tag zu löschen, einschließlich x[0].unsqueeze(0)
Wie Raghu erwähnte, sollte cat/split das Tag jedoch nach Möglichkeit beibehalten, da dies eine recht häufige Verwendung ist. Ich denke, die allgemeine Faustregel sollte sein, dass wir das Tag beibehalten sollten, solange der Betrieb den Rang nicht ändert oder die Achse seltsam neu ordnet. Wenn sich der Rang ändert, sind alle Wetten deaktiviert.
Ich stimme zu, dass wir in einigen Fällen das Etikett verlieren werden. Aber bei x[0]
würde ich anderer Meinung sein. Das scheint mir ein sehr üblicher Weg zu sein, um von NCHW
zu CHW
.
Nach mehreren Gesprächen darüber, wie verwirrend es ist, Tensoren zu haben, die channel_last 'Tag' tragen (oder nicht) haben wir uns entschieden, das Risiko einzugehen, bc-breaking change einzuführen und Tensoren automatisch in das Channels_last-Format zu promoten.
Was bedeutet es für die API:
Alle 3d,4d,5d-Tensoren mit Schritten wie N,1,H,[W,[D]] erhalten automatisch das channel_last memory format.
Damit dies funktioniert, werden wir besondere Vorkehrungen treffen, um zu garantieren, dass Operatoren auf channel_last-Tensoren, die channel_last-Tensoren ausgeben, mindestens eine ähnliche Leistung haben wie Operatoren auf zusammenhängenden Tensoren.
Im schlimmsten Szenario:
1) Benutzer können .contiguous() bei der Ausgabe aufrufen.
2) Wir werden Auto-Promoting-Code so schreiben, dass es fast trivial wäre, dieses Verhalten zu ändern.
Nebenwirkungen einer solchen Auto-Promotion sind:
import torch
x = torch.randn(10,16,16,3).permute(0,3,1,2)
x.is_contiguous(memory_format=torch.channels_last) == True
Auf der anderen Seite kann es den Fall lösen (nach leichten Modifikationen):
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
Von Slack-Conversions, gemäß der Anfrage von
Natalia Gimelshein [14:19]
Ich gehe also davon aus, dass es kein Konzept für Tags geben würde.
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]
z wird channel_last
Vitaly Fedyunin [8:25]
Wenn x1 in einer der vorgeschlagenen Varianten nicht channel_last ist (es sei denn, wir ändern die Chunk-Funktion so, dass keine Ansichten zurückgegeben werden), konvertiert es die Faltung in das fortlaufende (channels_first)-Format und gibt auch fortlaufend zurück
Vitaly Fedyunin [9:12 Uhr]
@ngimel vielen Dank für das Feedback. Ich denke, wir können eine aussagekräftigere Definition von ansichtsähnliche Operationen beteiligt sind. Wird dich auf dem Laufenden halten.
Natalia Gimelshein [9:36 Uhr]
auf einen Thread geantwortet:
Es scheint also ein Problem zu sein, oder? Das Chunking über die Dimension der Kanäle hinweg ist eine relativ häufige Sache, zB in inception-ähnlichen Netzwerken. Wenn der Tensor also der erste Tensor mit Chunked-Kanälen ist, wird die Faltungsausgabe Kanäle zuerst sein (was ein intuitives Verhalten ist und höchstwahrscheinlich das, was der Benutzer wünscht), wenn der Tensor der Chunked-Channels-zuletzt ist, dann wird die Faltungsausgabe wieder die Kanäle zuerst sein?
Natalia Gimelshein [9:39]
auf einen Thread geantwortet:
Aber nur aufgrund des nicht-kommutativen Additionsverhaltens und y
ist das erste Argument und die Kanäle zuletzt, oder? Was wäre das Ergebnis für x1+y
? Haben wir irgendwo Layout-Verbreitungsregeln für binäre Operationen?
Vitaly Fedyunin [10:44]
1) Ja, es ist ein Problem, das wir mit einem alternativen Vorschlag lösen werden. Ich mache jetzt ein paar Tests und werde es diese Woche aufschreiben (in ein oder zwei Tagen).
2) x1+y - sollte auch channel_last erzeugen, sonst ist es verwirrend, und ja, wir werden Layout-Verbreitungsregeln aufgeschrieben haben.
Ich denke, die Beobachtung, die ich @VitalyFedyunin gemacht
Aber es scheint, als gäbe es hier viele Details, die herausgearbeitet werden müssen, und ich bin mir nicht sicher, ob es am Ende funktioniert.
Die Verschwommenheit der Faltung (und anderer Layout-bewusster Operatoren, z. B. Upsampling, die ich mir kürzlich angesehen habe, beginnt mit dem Aufruf von .contiguous() für die Eingabe - was soll das also bedeuten?) war der Hauptgrund für die Einführung des Tags iirc.
Ja, also bin ich damit einverstanden, das Tag-Design wieder zu öffnen, aber dann wir
müssen die Probleme bei der Verbreitung dieser Tags ernsthaft lösen,
auch wenn Sie das Layout verlieren (wie es beim Chunking der Fall gewesen wäre)
auf Kanälen). Ich mache viel lieber "aktuelles Layout" etwas
eine Art Kontextmanager, als ihn datenabhängig zu machen.
Auszüge aus der Nachricht von ngimel vom 19.06.2019 12:43:45 -0700:
Die Verschwommenheit der Faltung (und anderer Layout-bewusster Operatoren, z. B. Upsampling, die ich mir kürzlich angesehen habe, beginnt mit dem Aufruf von .contiguous() für die Eingabe - was soll das also bedeuten?) war der Hauptgrund für die Einführung des Tags iirc.
Übrigens, warum müssen wir ein neues Konzept entwickeln, anstatt nur bei layout
bleiben? Ich glaube nicht, dass spärliche Darstellungen ein gut definiertes Konzept eines Layouts wie "channels_last" haben, also müssen wir kein Produkt von memory_formats * layouts
( layouts
bezieht sich auf die aktuelle Verwendung ), aber nur memory_format + layouts
was bedeutet, dass es in Ordnung sein sollte, dasselbe Argument wie früher zu verwenden? Für mich ist es sowohl kürzer als auch schöner und lässt uns vermeiden, die Unterschriften von Fabriken auf tausend Argumente auszudehnen.
Layout-Option wurde in Betracht gezogen (siehe Anhang), aber wir haben festgestellt, dass dies zu viel Code-Duplizierung führt und die automatische Konvertierung von Tensoren in ein anderes memory_format on fly nicht zulassen wird
Schließlich ist memory_format ein Weg, um den Tensor zu schreiten und optimierte Kernel und Ausgaben einfach auszuwählen, was eine Eigenschaft des schrittförmigen Tensors ist, keine völlig andere Klasse
In gewisser Weise sind spärliche Layouts auch eine Möglichkeit, optimierte Kernel für Arrays auszuwählen, die meistens null sind.
Dies mag eine naive Frage sein, aber warum zieht PyTorch diese API in Betracht, anstatt nur eine Option zur Verwendung von NHWC in den Operationen selbst bereitzustellen, die den zugrunde liegenden CuDNN-Kernel, sofern verfügbar, direkt aufrufen würde?
Es scheint, als ob dies für einen allgemeinen Anwendungsfall (Mischen von Image-Operationen wie Conv und Pooling mit LM-Architekturen) eine einfache Lösung wäre. Als Entwickler möchte ich nur ein Conv2d(..., nhwc=True)
. Gibt es einen Grund, warum das keinen Sinn macht?
@rewonc Wir haben den ähnlichen Ansatz in Betracht gezogen (Optionen zu Operatoren hinzufügen, anstatt den Kernel vom Striding abzuleiten) und fanden es aus folgenden Gründen schwierig, ihn anzuwenden:
nhwc=True
.nhwc=True
benötigen.PS. Wenn Sie sich über CudNN Ex
Funktionen Sorgen machen, möchten wir cudnn_batch_norm_nhwc
und ähnliche Operatoren enthüllen.
Hallo @VitalyFedyunin , wir haben gesehen, dass der benannte Tensor in PyTorch 1.3 unterstützt wird. Kann das die Bedenken bezüglich der NHWC- (oder sogar blockierten) Formatunterstützung lösen (oder teilweise lösen)? Gibt es einen Plan, den NHWC-Zustand basierend auf dem benannten Tensor voranzutreiben?
Wir gehen mit dem letzten Support der Kanäle voran. Ich werde die Roadmap diese Woche hier und in Slack-Kanälen veröffentlichen. Wir erwägen nicht, in absehbarer Zeit blockierte Formate hinzuzufügen (da dazu ALLE Operatoren neu geschrieben werden müssen).
Vielen Dank. Das wird gut!
Anpacken von Aufgaben und Fortschritt innerhalb von https://github.com/pytorch/pytorch/issues/28619
Hilfreichster Kommentar
Übrigens, warum müssen wir ein neues Konzept entwickeln, anstatt nur bei
layout
bleiben? Ich glaube nicht, dass spärliche Darstellungen ein gut definiertes Konzept eines Layouts wie "channels_last" haben, also müssen wir kein Produkt vonmemory_formats * layouts
(layouts
bezieht sich auf die aktuelle Verwendung ), aber nurmemory_format + layouts
was bedeutet, dass es in Ordnung sein sollte, dasselbe Argument wie früher zu verwenden? Für mich ist es sowohl kürzer als auch schöner und lässt uns vermeiden, die Unterschriften von Fabriken auf tausend Argumente auszudehnen.