Pytorch: [RFC] Dukungan format memori (alias tata letak alias NHWC)

Dibuat pada 10 Apr 2019  ·  68Komentar  ·  Sumber: pytorch/pytorch

Pernyataan masalah

Operator CNN menggunakan urutan kanonik dimensi tensor dan menetapkan makna semantiknya. Untuk kasus 2D di PyTorch hari ini, input ke torch.nn.Conv2d harus berupa tensor 4d dalam urutan NCHW -.

Untuk alasan kinerja, seringkali bermanfaat untuk menyusun ulang dimensi secara berbeda sehingga memori yang diakses oleh operasi tertentu diletakkan secara berurutan dan lokalitas digunakan dengan lebih baik. Opsi paling umum adalah memindahkan dimensi ke ujung - NHWC. Mungkin ada format memori yang lebih kompleks yang menggabungkan satu dimensi ke dalam blok, mis.

Contoh perpustakaan yang menggunakannya meliputi:

  • cudnn memiliki kinerja yang lebih cepat pada Volta di NHWC
  • fbgemm dan qnnpack tidak mendukung NCHW.
  • libxsmm memang mendukung NCHW tetapi penalti kinerjanya kira-kira 50% (IIRC).

Tantangannya adalah bahwa mengubah tatanan dimensi itu sendiri mahal, sehingga dalam kasus ketika operasi beberapa CNNs dilakukan berturut-turut (misalnya conv(relu(conv))) ) itu bermanfaat untuk mengubah ke format memori yang berbeda sekali, melaksanakan operasi dan menyusun ulang mereka kembali.

Oleh karena itu, penting untuk membuat PyTorch mengetahui urutan dimensi yang berbeda dan dapat melewatkan tensor dengan format memori yang berbeda antara operasi baik dalam mode bersemangat maupun JIT. Selain itu, ada baiknya untuk memiliki pass optimasi JIT otomatis yang mencoba menerapkan heuristik atau teknik pencarian untuk mengetahui apakah mengubah format memori menguntungkan secara kinerja dan di mana dalam model itu masuk akal untuk melakukannya.

Kami berusaha untuk membangun API yang mampu mewakili:

  • Tensor dengan format memori yang berbeda (pada awalnya, hanya urutan dimensi) hadir di PyTorch di Eager dan JIT. Tata letak yang diblokir adalah prioritas yang lebih rendah tetapi masih bagus.
  • API yang diekspos pengguna untuk membuat kueri dan mengubah format memori
  • Operasi inti CNN mampu menangani tensor input dengan format memori yang berbeda dan merutekan ke implementasi yang lebih cepat
  • Kemampuan untuk menyimpulkan dan mengoptimalkan tentang format memori dalam pass JIT

Terminologi : masalah di atas sering disebut sebagai “layout” (mxnet), “data_format” (tf), “image_format” (keras), “order” (caffe2). Kami mengusulkan untuk menggunakan nama "format memori" atau "memory_format" di PyTorch. Sayangnya, nama "layout" diambil di PyTorch dengan nilai 'strided' vs 'sparse_coo', sehingga opsi penamaan tidak tersedia.

Operator yang terpengaruh

Operator berikut minimal harus sadar akan format memori. Selain menghasilkan hasil yang benar, mereka perlu memberikan kinerja terbaik dari pustaka yang mendasarinya DAN mempertahankan format memori keluaran untuk menyebarkan maksud pengguna yang ditentukan secara eksplisit.

  • lilitan
  • berbagai jenis penyatuan
  • norma batch, norma lapisan, norma contoh (umumnya, norma apa pun)
  • upsampling/interpolasi
  • putus fitur
  • softmax ke tingkat yang lebih rendah - dimensi dapat ditentukan secara manual di sana, tetapi implementasi yang efisien hanya ada untuk tata letak nchw implisit
  • lapisan
  • operasi elemen-bijaksana (unary dan biner)
  • konstruktor tensor yang mewarisi format memori, misalnya empty_like.

Perubahan API dan Perilaku

Tentukan konsep format memori di PyTorch:

  • Konstanta seperti torch.memory_format.channels_first . Mereka tidak memiliki tipe yang ditentukan dan dapat menjadi objek yang sebanding (kemungkinan dimulai dengan enum tetapi di masa depan mungkin objek lain untuk diinterop dengan konsep bernama tensor)

    • Alternatif: gunakan torch.channels_first secara langsung

  • Nilainya adalah channels_first dan channels_last (untuk memungkinkan lebih sedikit konstanta)
  • Untuk gambar 1D / tensor 3D, nilainya berarti NCW, NWC, untuk gambar 2D / tensor 4D - NCHW, NHWC, untuk gambar 3D / tensor 5D - NCDHW, NDHWC

Tambahkan metode berikut ke Tensor:

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

Catatan : tidak ada fungsi x.get_memory_format() untuk saat ini, hanya pemeriksaan eksplisit - ini memungkinkan kemungkinan implementasi yang lebih luas. Kami mungkin ingin menambahkannya.

Tata letak semantik tensor selalu tetap sama - NCHW! x.size() selalu mengembalikan (n,c,h,w)

Operasi mempertahankan perilaku format memori:

  • convolution, pooling, dll, (lihat di atas) mengembalikan output dalam format memori yang sama dengan input dan mengirimkan secara internal ke implementasi terbaik
  • operasi elemen-bijaksana unary mempertahankan format memori yang sama dan perlu dijalankan secepat pada tensor yang berdekatan
  • operasi elemen-bijaksana biner memberikan beberapa jaminan yang masuk akal untuk melestarikan format memori - kemungkinan dapat didefinisikan lebih luas tetapi minimum adalah:

    • NHWC + skalar → NHWC

    • NHWC + vektor kolom → NHWC

  • operasi mundur untuk operasi CNN inti mempertahankan format memori yang sama seperti di jalur maju. (mungkin perlu diterapkan secara eksplisit karena gradien yang masuk untuk output bisa dalam format memori yang berbeda)

Format memori adalah properti tensor yang dipertahankan melalui serialisasi/deserialisasi (jika tensor adalah parameter).

Implementasi bertahap

Tensor di PyTorch saat ini memiliki konsep langkah yang menentukan bagaimana tensor logis diletakkan di memori . Secara khusus setiap tensor memiliki vektor strides dengan panjang yang sama dengan sizes . Untuk mengindeks elemen dalam pengindeksan logis (i1, i2, .., ik) seseorang melakukan produk titik dengan langkah dan mencari memori di offset + i0*stride0 + i1*stride1 + ... * ik * stridek . Tensor yang berdekatan dengan demikian memiliki langkah yang merupakan produk kumulatif dari ukuran yang dibalik. Misalnya tensor 4D dengan ukuran (n,c,h,w) memiliki langkah (c*h*w, h*w, w, 1) .

Langkah dapat digunakan untuk mewakili format memori yang berbeda (yaitu penataan ulang dimensi) secara fisik sambil mempertahankan urutan NCHW default logis. Ini memberikan definisi yang efektif dari transformasi format memori sebagai:

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

Dalam format NHWC, vektor langkahnya adalah (c*h*w, 1, c*w, c) . Jadi dalam buffer memori bobot berada dalam urutan yang berdekatan untuk NHWC.

Langkah dapat digunakan untuk pengujian:

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

Kelebihan dari pendekatan ini:

  • Memanfaatkan konsep langkah PyTorch yang ada tanpa menambahkan ide tingkat atas baru atau parameter API
  • Mempertahankan perilaku logis tensor dalam urutan NCHW kanonik
  • Berfungsi untuk menyusun ulang dimensi input secara sewenang-wenang
  • Rutinitas serialisasi yang ada sudah mempertahankan langkah tensor
  • Kemampuan untuk menggunakan kembali banyak operasi untuk bekerja pada tata letak memori yang berbeda

Kontra :

  • Memanggil .contiguous() sama dengan beralih ke NCHW dan dapat terjadi secara tidak sengaja dari pengguna atau di dalam salah satu operasi

    • Audit eksplisit operator diperlukan untuk memastikan mereka mempertahankan format memori

  • Tidak berfungsi untuk format yang diblokir / ubin - diperlukan pendekatan yang berbeda

    • Dimungkinkan untuk mempertimbangkan untuk menambahkan mereka sebagai warga negara kelas satu di PyTorch, tetapi ini adalah perubahan yang jauh lebih besar

    • Alternatifnya adalah memperlakukannya sebagai pegangan buram, misalnya tensor MKLDNN

  • Karakteristik kinerja dari implementasi yang mendasarinya kurang jelas bagi pengguna akhir

Potensi masalah terbesar adalah dengan maksud pengguna yang tidak jelas . Tidak ada cara untuk membedakan apakah pengguna benar-benar menginginkan format memori yang berbeda atau tensor input yang kebetulan dilalui dengan cara ini. Secara khusus, ini mengarah pada perubahan perilaku untuk operasi yang ada - hari ini konvolusi hanya dapat menghasilkan tensor bersebelahan NCHW bahkan jika inputnya sewenang-wenang, di dunia baru mungkin mengenali input sebagai NHWC dan dengan demikian akan mengembalikan NHWC juga. Itu tidak mengubah semantik tetapi mengarah ke masalah kinerja yang sulit di-debug. Solusi yang mungkin adalah menandai tensor secara eksplisit dengan flag memory_format yang ditentukan pengguna dan hanya mengikuti anotasi ini (selain langkah).

Untuk mengatasi masalah di atas, proposal awal adalah untuk memperkenalkan tag format memori "lunak" pada tensor yang merekam panggilan to(memory_format) dilakukan pada tensor. Operator perlu menyebarkan anotasi ini ke output. Anotasi adalah "lunak", jadi kami tidak akan membuat kesalahan keras pada anotasi yang tidak cocok, melainkan menghasilkan peringatan dalam mode pembuatan profil.

Implementasi operator

Tanda tangan operator yang ada tidak berubah. Operator dapat melakukan pengiriman hard-coded di dalam operator untuk mengarahkan ke implementasi yang lebih cepat. Jika implementasi tidak tersedia, bolak-balik melalui format memori yang berbeda dimungkinkan. Alternatif akan memunculkan pesan kesalahan.

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

Lebih disukai menggunakan simbol tunggal seperti 'conv' untuk merujuk ke operator di JIT IR daripada membuat operator terpisah seperti 'conv_nhwc'. Alasannya adalah kesederhanaan dan menjaga IR pada tingkat representasi semantik.

Operasi elemen-bijaksana

Kita harus memastikan bahwa operasi inti seperti elemen-bijaksana melestarikan format memori dan efisien.

Operasi unary dapat ditangani secara umum dengan memverifikasi apakah blok memori "padat" - yaitu apakah elemen menjangkau area tanpa celah dan setiap lokasi memori digunakan tepat satu kali. Itu dapat diverifikasi dengan algoritma sederhana

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

Perkakas kinerja

Untuk kinerja debugging, kita harus menambahkan dukungan ke profiler untuk:

  • melihat di mana dalam program terjadi penataan ulang memori yang sebenarnya - yaitu melacak panggilan ke .contiguous()
  • melacak implementasi mana yang dipanggil
  • mengeluarkan peringatan tentang perubahan format memori dalam misalnya operasi biner (di mana anotasi "lunak" berguna)

Fungsionalitas ini dapat dibangun menjadi alat pembuatan profil sesuai permintaan.

Penanganan Autograd

Masuk akal untuk mengharapkan bahwa backward pass harus dijalankan dengan format memori yang sama dengan forward. Itu tidak akan selalu terjadi secara otomatis karena gradien yang masuk mungkin berjalan sewenang-wenang. Jadi forward pass harus secara eksplisit mengenali format memori, menyimpannya dalam penutupan autograd dan menerapkan ke tensor grad sebelum fungsi mundur.

Kemungkinan implementasi:

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

Representasi di JIT

Proposal saat ini adalah untuk memiliki:

  • Belum ada penanganan kelas satu untuk format memori dalam anotasi tipe. Sebagai gantinya, kami dapat mempertahankan peta yang terlihat di samping dalam bentuk yang diperlukan untuk lintasan yang memanipulasi format memori
  • Inferensi pass (mirip dengan shape_inference) yang menghasilkan anotasi format per-Nilai
  • Lintasan transformasi format memori (manual atau otomatis) yang menemukan panggilan to(memory_format) perlu dimasukkan untuk kinerja yang optimal

Untuk tujuan penegakan, kami juga dapat menggunakan pernyataan seperti assert x.is_contiguous(channels_last) .

Catatan: Ada pertanyaan tentang di mana menyimpan informasi bahwa perangkat tertentu memiliki kombinasi format memori yang disukai (misalnya qconv pada rute x86 ke fbgemm yang hanya mengimplementasikan NHWC). Salah satu opsi adalah meletakkannya di tingkat pendaftaran op, namun, anotasi format memori terasa lebih seperti informasi sampingan. Kita bisa mulai dengan mempertahankan peta global di suatu tempat di JIT pass yang menunjukkan format memori yang disukai dan heuristik terkait. Jika tidak rapi - kita dapat beralih ke mekanisme berbasis pendaftaran.

Di luar: tata letak yang diblokir

Karena kami memutuskan untuk menambahkan pengepakan tensor yang lebih kompleks, menggunakan tensor PyTorch kelas satu untuk itu mungkin tidak masuk akal karena biaya implementasi dan kerumitan yang tinggi. Dua alternatif yang mungkin:

  • Representasi buram seperti binding tipe C khusus. Ini adalah opsi untuk memilih pengemasan dalam inferensi di mana keragaman lebih tinggi dalam hal pengoptimalan kinerja
  • Jenis tensor kelas satu seperti MKLDNNTensor dengan beberapa (tetapi tidak semua) operasi terikat pada jenis baru ini

Namun alternatif lain adalah mengimplementasikan dukungan asli untuk pemblokiran/pelapisan di kelas inti PyTorch Tensor.

Relasi tensor bernama

Proposal yang ada untuk NamedTensor disusun sebagai mekanisme pemeriksaan tipe pada tensor - saat ini tidak memberikan makna semantik apa pun pada nama dimensi. Jadi satu-satunya cara untuk menyimpulkan arti dari tensor aktivasi adalah dengan terus menggunakan format NCHW yang telah ditentukan. Itu membuat NamedTensor dan proposal saat ini ortogonal.

Jika kami ingin menentukan arti dari beberapa nama (seperti "saluran", "lebar"), operator dapat memanfaatkan informasi ini untuk mengarahkan ke implementasi yang lebih cepat. Ini akan menjadi perubahan semantik karena tensor input secara logis akan memiliki format memori NHWC (bukan NCHW seperti hari ini).

Seni sebelumnya

TensorFlow mendukung NHWC dan NCHW di tingkat operator, melalui parameter data_format ; nilai yang dapat diterima adalah (“NHWC”, “NCHW”) untuk input 4-d, (“NDHWC”, “NCDHW”) untuk input 5-d, atau channels_first / channels_last terlepas dari input kematraan. Terserah pengguna untuk menangani pengaturan parameter dengan benar, yaitu tidak dilacak secara otomatis oleh tensor.

Caffe2 memanggil parameter ini disebut order daripada data_format , tetapi masih diterapkan pada tingkat operator individu secara eksplisit.


Lampiran: Opsi lain dipertimbangkan

Pertanyaan lakmus: apa yang dicetak kode berikut: tensor_in_nhwc_layout.size(1) - jumlah saluran (karena defaultnya adalah NCHW di PyTorch) atau tinggi (karena itulah yang ada di tata letak NHWC di posisi 1).

Berdasarkan jawaban ini, beberapa opsi dimungkinkan:

  • Opsi A - Langkah (disajikan di atas). Tata letak tensor adalah representasi internal sepenuhnya. Implementasi-seperti itu paling mudah dilakukan dengan langkah.

    • .size(1) mengembalikan saya "saluran", tetapi memori internal ditata berbeda

    • pro: tidak mengubah kode model, model saya masih dapat melakukan aritmatika dimensi secara langsung. Sebenarnya tidak ada perubahan API publik

    • kontra: dalam implementasi langkah banyak operator memanggil .contiguous() dan secara tidak sengaja dapat mengembalikan tata letak kembali

    • kontra: Dari sudut pandang pengguna, memahami apa jaminan op return adalah yang terpenting. IMO ini menghilangkan pendekatan hanya langkah, karena menjadi sangat sulit untuk memahami format tempat operasi Anda akan dikembalikan, dan tidak ada API untuk mengatakan "abaikan langkah saya, sebenarnya cukup kembalikan hal yang bersebelahan dengan NCHW." Ini di luar batasan di atas.

  • Opsi B - Tensor NHWC eksplisit. Pengguna secara eksplisit memanipulasi tensor yang memiliki urutan dimensi berbeda tetapi tensor itu sendiri tidak tahu apa-apa tentangnya. Kami membutuhkan beberapa anotasi di tingkat operator untuk mengetahui apa yang diharapkan pengguna.

    • .size(1) mengembalikan "tinggi"

    • pro: tidak ada sihir dan sangat mudah ditebak

    • kontra: mengubah model dari satu tata letak ke tata letak lain menjadi operasi kompleks yang perlu melacak semua akses ke .size() dan .reshape() (atau Anda perlu membuatnya eksplisit di API?)

  • Opsi B' - Tensor NHWC eksplisit dengan flag layout . Sama seperti di atas, tetapi kami mengizinkan untuk melampirkan anotasi ke tensor untuk menandai tata letak semantiknya yang digunakan ops dalam implementasinya. Tidak perlu dalam anotasi tingkat operator - operator dapat melakukan pengiriman berdasarkan tanda tata letak input.
  • Opsi C - Bernama Tensor . ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading =h.2gbe5xpga3w9)

    • .size(1) mengembalikan "tinggi" tetapi kami meminta orang untuk TIDAK menggunakan API ini dan sebagai gantinya menggunakan .size('channel')

    • pro: sangat eksplisit dan apa yang diinginkan pengguna

    • con: tidak menyelesaikan masalah transisi, kita harus memaksa semua kode yang ditulis dengan kesadaran tata letak untuk menggunakan tensor bernama. Jika tidak - masalah yang sama seperti di atas berlaku

  • Opsi D-Layout adalah tipe tensor buram . Perlakukan NHWC seperti kami memperlakukan MKLDNN atau SparseTensor - jenis tensor terpisah dengan DispatchID yang berbeda. Ini seperti Opsi A tetapi dengan pengorbanan yang berbeda pada perilaku default - operasi yang tidak diterapkan akan gagal alih-alih kembali ke NCHW.

    • .size(1) masih mengembalikan "saluran"

    • pro: tidak ada sihir dan eksplisit, pengiriman terpisah memungkinkan operasi untuk memutuskan apa yang mereka inginkan

    • pro/kontra: semua operator yang diperlukan perlu diimplementasikan pada tata letak yang berbeda, jika beberapa operasi hilang, pengguna akan mendapatkan kesalahan eksplisit bahwa itu tidak didukung

    • kontra: kita mungkin perlu melarang banyak operasi di dalamnya, misalnya tampilan karena hasil yang diharapkan sulit diprediksi

internals mkldnn triaged

Komentar yang paling membantu

BTW kenapa kita harus membuat konsep baru daripada hanya berpegang pada layout ? Saya tidak berpikir bahwa representasi yang jarang memiliki konsep tata letak yang terdefinisi dengan baik seperti "channels_last", jadi kami tidak perlu mewakili produk memory_formats * layouts ( layouts mengacu pada penggunaan saat ini ), tetapi hanya memory_format + layouts berarti bahwa boleh saja menggunakan argumen yang sama seperti dulu? Bagi saya itu lebih pendek, lebih baik, dan akan membiarkan kita menghindari memperluas tanda tangan pabrik ke seribu argumen.

Semua 68 komentar

Ada satu masalah dengan empty_like ; semantik yang saat ini ditentukan adalah Anda membuang semua informasi langkah, jadi, tidak mungkin untuk mempertahankan tata letak dan menjadi BC.

@VitalyFedyunin mendaftar untuk mengimplementasikan bit .contiguous() dan torch.memory_layout

Satu pertanyaan - untuk tensor 4D x dengan ukuran (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)

Kami memiliki permutasi yang aneh

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

Sekarang kita periksa apakah itu bersebelahan untuk format NHWC. Mengikuti logika Anda seperti di bawah ini

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)

Untuk kedua kasus is_nhwc_contiguous(y) akan mengembalikan Benar?

Ini benar. Namun kami tidak dapat menyampaikan hanya pada langkah karena kami ingin menghindari konversi bolak-balik selama penyalinan, ke, dan operasi serupa.

Bagaimana jika langkah memiliki urutan yang sama dengan format memori? Mari kita gunakan tensor 4D sebagai contoh. Untuk mendeskripsikan tensor, kita memiliki sizes , strides dan stride_indexes :

ukuran dalam (n, c, h, w)
langkah dalam urutan fisik, yaitu

  • langkah (n, c, h, w) jika formatnya nchw
  • langkah (n, h, w, c) jika formatnya nhwc.

stride_indexes memetakan langkah ke ukuran nchw:

  • (0, 1, 2, 3) jika formatnya nchw,
  • (0, 2, 3, 1) jika formatnya nhwc.

Untuk format nchw ini sama seperti sebelumnya. Untuk nhwc, itu akan serupa.

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)

Ini juga dapat diperluas untuk mendukung format yang diblokir. Gunakan nChw16c sebagai contoh,

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)

Rincian lebih lanjut dapat dieksplorasi lebih lanjut nanti.

Untuk OP yang hanya menerima tensor bersebelahan nchw, Itu akan berhasil di sini.

Atau kita juga dapat mengubah prototipe sedikit, katakanlah

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

Jadi secara default, diasumsikan hanya nchw yang bersebelahan. Dengan cara ini Anda tidak perlu menulis ulang OP tersebut, itu akan diatur ulang ke nchw secara otomatis.

Kami berusaha untuk membangun API yang mampu mewakili:

  • Tensor dengan format memori yang berbeda (pada awalnya, hanya urutan dimensi) hadir di PyTorch di Eager dan JIT. Tata letak yang diblokir adalah prioritas yang lebih rendah tetapi masih bagus.
  • API yang diekspos pengguna untuk membuat kueri dan mengubah format memori
  • Operasi inti CNN mampu menangani tensor input dengan format memori yang berbeda dan merutekan ke implementasi yang lebih cepat
  • Kemampuan untuk menyimpulkan dan mengoptimalkan tentang format memori dalam pass JIT

Usulan yang bagus! Bolehkah saya mengungkapkan pemahaman saya melihat apakah itu benar (termasuk proposal untuk penanganan format MKL-DNN):

Izinkan saya untuk berpikir ada implementasi proposal ini sebagai kelas "format". Selama menyediakan kueri dan mengubah API sebagai virtual, kami dapat melakukan pewarisan/ekstensi yang sesuai dengan format kompleks MKL-DNN. Atau metode lain selama itu menyediakan kerangka kerja untuk menangani format, membagikan detail kecil itu kepada kami.

Tentang implementasi OP, setiap OP dapat memiliki format pilihan yang memaksimalkan kinerjanya dan format kompatibel yang berfungsi. Operator elemen-bijaksana (Atau lebih umum, OP yang dibatasi memori) seharusnya tidak memiliki preferensi. OP menghasilkan tensor hasilnya dengan objek "format", objek format ini menjamin kueri/pengubahan semantik yang kompatibel dengan ekspektasi pytorch default, serta dapat menangani format tertentu jika disebut serial fungsi yang dioptimalkan (seperti conv2d(ReLU(conv2d))) kasus)

@uyongw Saya ingin mengklarifikasi sedikit lebih banyak tentang contoh pertama Anda. Anda mengatur contoh sebagai, "Saya memiliki tensor NCHW, yang kemudian saya transpose dengan cara yang aneh (jadi sekarang terlihat seperti NWCH); sekarang saya ingin tahu apakah itu NHWC bersebelahan." Tapi itu cara pandang yang salah. Formulasi yang lebih baik adalah, "Saya memiliki tensor NHWC, yang kemudian saya transposisikan menjadi tensor NCHW."

Dengan kata lain, tidak ada makna intrinsik pada dimensi fisik tensor (ketika kita mengabaikan langkah). Kami hanya memberi makna kepada mereka ketika kami mempertimbangkan bagaimana kami merujuknya sehubungan dengan langkah.

Untuk mendeskripsikan tensor, kami memiliki ukuran, langkah, dan indeks_langkah

Saya pikir stride_indexes adalah cara yang nyaman untuk memikirkan masalah, tetapi itu benar-benar berlebihan dengan langkah, karena semua yang Anda katakan adalah "Terapkan ini (terbalik?) permutasi ke langkah, dan kemudian perlakukan itu sebagai langkah yang benar.) @VitalyFedyunin dan saya berbicara tentang bagaimana mungkin masih merupakan ide yang baik untuk menyimpan informasi ini dalam beberapa cara, karena sulit untuk merekonstruksi informasi dari langkah itu sendiri. Tapi ini di luar cakupan proposal ini.

Jadi secara default, diasumsikan hanya nchw yang bersebelahan.

Yap, itulah bacaan saya tentang rencana tersebut.

@CaoZhongZ

Izinkan saya untuk berpikir ada implementasi proposal ini sebagai kelas "format". Selama menyediakan kueri dan mengubah API sebagai virtual, kami dapat melakukan pewarisan/ekstensi yang sesuai dengan format kompleks MKL-DNN. Atau metode lain selama itu menyediakan kerangka kerja untuk menangani format, membagikan detail kecil itu kepada kami.

Saya sebenarnya tidak berpikir itu adalah deskripsi proposal yang akurat. Dukungan tata letak memori yang didukung proposal di sini hanyalah tata letak yang dapat diekspresikan melalui langkah. Apa pun yang tidak dapat diungkapkan dengan cara ini (misalnya, tata letak blok) tidak akan berfungsi dengan cara ini, dan harus didukung oleh mekanisme "tata letak" kami yang lebih berat.

Dengan kata lain, tidak ada makna intrinsik pada dimensi fisik tensor (ketika kita mengabaikan langkah). Kami hanya memberi makna kepada mereka ketika kami mempertimbangkan bagaimana kami merujuknya sehubungan dengan langkah.

Sebagian setuju :-) Tapi tidak pada masalah khusus ini. Katakanlah saya sudah memiliki tensor nhwc. Lalu saya mengubahnya menjadi nwhc. Saya ingin mengubah lebih lanjut ke nhwc kemudian melakukan contiguous(). Tapi saya sudah mendapatkannya nhwc bersebelahan. Apakah tidak membingungkan?

Saya pikir stride_indexes adalah cara yang nyaman untuk memikirkan masalah, tetapi itu benar-benar berlebihan dengan langkah, karena semua yang Anda katakan adalah "Terapkan permutasi (terbalik?) ini ke langkah, dan kemudian perlakukan itu sebagai langkah yang sebenarnya.)

IMHO, itu tidak akan berlebihan dengan langkah, jika Anda memiliki langkah di nhwc (fisik). Karena Anda memerlukan pemetaan yang tepat dengan ukuran (logika). Kalau tidak, tidak ada cara untuk memberi tahu urutan sebenarnya.

BTW ada pendekatan yang lebih mudah dengan menggunakan pemetaan terbalik. Katakanlah, untuk nchw, itu (0, 1, 2, 3), untuk nhwc, itu (0, 3, 1, 2) bukannya (0, 2, 3, 1). Yang mengatakan stride_index itu sendiri selalu NCHW juga. Tapi masalahnya, itu tidak bisa diperluas ke format yang diblokir seperti nChw16c atau OIhw16i16o.

Format yang diblokir memerlukan serangkaian implementasi operator yang sama sekali berbeda; oleh karena itu, kami memilih untuk tidak mencampurnya dengan 'format memori', yang menurut definisi seharusnya ramah dengan semua operator yang ada dan bekerja dengan kinerja yang sama atau lebih baik.

Sebagian setuju :-) Tapi tidak pada masalah khusus ini. Katakanlah saya sudah memiliki tensor nhwc. Lalu saya mengubahnya menjadi nwhc. Saya ingin mengubah lebih lanjut ke nhwc kemudian melakukan contiguous(). Tapi saya sudah mendapatkannya nhwc bersebelahan. Apakah tidak membingungkan?

Sulit untuk memahami contoh Anda karena Anda menggunakan beberapa istilah sehari-hari dan presisi diperlukan. Inilah cara saya menafsirkan apa yang Anda katakan:

  • Tensor "nhwc" sesuai dengan proposal ini, "Tensor yang tata letak fisiknya NHWC, tetapi dilangkahi sehingga tata letak logisnya adalah NCHW."
  • Untuk "mengubah tensor (tensor yang tata letak logisnya adalah NCHW) ke (tata letak logis) NWHC" adalah dengan menjalankan y = x.permute(0, 2, 3, 1) , karena Anda mengubah tata letak logis , bukan tata letak fisik. (Saya menduga ini bukan yang Anda maksud, karena dalam posting asli Anda, Anda menyebutkan permutasi x.permute(0, 3, 1, 2)
  • Untuk selanjutnya mengubah tensor (tata letak logis) NWHC ke (tata letak logis) NHWC adalah dengan menerapkan permutasi z = y.permute(0, 2, 3, 1) . Jadi sekarang Anda memiliki tensor yang tata letak logisnya bertepatan dengan tata letak fisik. Ini berarti bahwa jika kita meminta z.contiguous() kita akan mendapatkan true (dan, yang membingungkan, z.contiguous(memory_layout=NCHW) akan benar juga.) Tapi itu TIDAK akan NHWC bersebelahan.

Saya tidak berpikir ini adalah contoh yang ada dalam pikiran Anda, dalam hal ini Anda harus lebih tepat tentang apa yang Anda maksud dengan "permute".

IMHO, itu tidak akan berlebihan dengan langkah, jika Anda memiliki langkah di nhwc (fisik). Karena Anda memerlukan pemetaan yang tepat dengan ukuran (logika). Kalau tidak, tidak ada cara untuk memberi tahu urutan sebenarnya.

Ini adalah inti dari proposal: kita hak istimewa NCHW sebagai tata letak yang logis, selalu. Jadi, jika saya memiliki tensor 4D yang tidak saya ketahui, saya berasumsi bahwa tata letak logisnya adalah NCHW. Itu menghilangkan ambiguitas. Jika Anda ingin berurusan dengan tensor yang tata letak logisnya bukan NCHW, saya pikir API seperti yang dinyatakan membuat hidup Anda agak sulit.

@dzhulgakov

Operasi mempertahankan perilaku format memori

Jika tensor NHWC fisik dapat terjadi murni melalui langkah, ini secara teknis melanggar BC, kecuali jika Anda membuatnya hanya mempertahankan format memori ketika tag format memori ada (tetapi sepertinya Anda tidak ingin ini memiliki makna semantik, jadi saya saya tidak yakin apa yang disarankan proposal saat ini.) Saya tidak yakin apakah ini benar-benar melanggar kode siapa pun dalam praktiknya.

Jika tensor NHWC fisik dapat terjadi murni melalui langkah, ini secara teknis melanggar BC, kecuali jika Anda membuatnya hanya mempertahankan format memori ketika tag format memori ada (tetapi sepertinya Anda tidak ingin ini memiliki makna semantik, jadi saya saya tidak yakin apa yang disarankan proposal saat ini.) Saya tidak yakin apakah ini benar-benar melanggar kode siapa pun dalam praktiknya.

Dengan asumsi kita dapat membuat format memori 'lengket'. Tensor berformat op over memori akan menghasilkan tensor berformat memori. Itu akan memecahkan masalah BC.

Namun, kita perlu mendefinisikan perilaku operasi biner (atau lebih banyak anggota) ketika tensor memiliki format memori yang berbeda.

@ezyang Oh saya baru tahu ada salah ketik di jawaban saya di atas. (Saya minta maaf untuk itu. Namun contoh aslinya masih benar.) Biarkan saya menyatakannya kembali seperti di bawah ini:

  1. Saya memiliki tensor NCHW (secara fisik, bersebelahan).
  2. Lalu saya mengubahnya ke NWHC (secara logis).
  3. Saya ingin mengubahnya lebih lanjut ke NHWC dengan panggilan contiguous() diikuti.
  4. Gunakan sebagai NHWC (secara fisik).

Tapi saya sudah mendapatkannya NHWC bersebelahan setelah langkah 2. Kemudian saya dapat melewati langkah 3 dan menggunakannya sebagai NHWC langsung pada langkah 4. Tapi ini pasti tidak benar karena urutan fisik tensor tidak berubah sama sekali.

Format yang diblokir memerlukan serangkaian implementasi operator yang sama sekali berbeda; oleh karena itu, kami memilih untuk tidak mencampurnya dengan 'format memori', yang menurut definisi seharusnya ramah dengan semua operator yang ada dan bekerja dengan kinerja yang sama atau lebih baik.

Ya kita bisa mengaktifkan NHWC sebagai langkah pertama. Namun saya tidak benar-benar berpikir format yang diblokir benar-benar sesuatu yang sama sekali berbeda. Itu dapat diekspresikan secara alami (dengan beberapa abstraksi yang bagus). Jika ada deskripsi format umum, maka orang lain dapat mendaftarkan format baru dengan pemblokiran/langkah sewenang-wenang.

Terlebih lagi jika kami telah memblokir dukungan, kami tidak perlu repot-repot membuat beberapa konstruksi tersembunyi untuk menjalankan semua yang mendasarinya, yang menciptakan dunia implisit di dalam dan dari/ke antara dua dunia dapat menjadi masalah.

Pokoknya mungkin terlalu jauh untuk memikirkan format yang diblokir. Tetapi saya akan berpikir jika memungkinkan, lebih baik membuat desainnya dapat diperluas.

Tapi saya sudah mendapatkannya NHWC bersebelahan setelah langkah 2. Kemudian saya dapat melewati langkah 3 dan menggunakannya sebagai NHWC langsung pada langkah 4. Tapi ini pasti tidak benar karena urutan fisik tensor tidak berubah sama sekali.

Oke, saya mengerti contoh Anda sekarang. Anda mungkin benar-benar berhenti di langkah 2 dan menggunakannya seolah-olah itu adalah tensor NCHW; dalam hal ini, Anda akan salah menafsirkan W sebagai C, dll. Ini jelas merupakan kerugian dengan implementasi berbasis langkah ( @dzhulgakov , kita mungkin harus menambahkan ini ke proposal). Proposal memiliki beberapa ketentuan untuk kasus ini:

Untuk mengatasi masalah di atas, proposal awal adalah untuk memperkenalkan tag format memori "lunak" pada tensor yang merekam panggilan ke (memory_format) terakhir yang dilakukan pada tensor. Operator perlu menyebarkan anotasi ini ke output. Anotasi adalah "lunak", jadi kami tidak akan membuat kesalahan keras pada anotasi yang tidak cocok, melainkan menghasilkan peringatan dalam mode pembuatan profil.

Tag format memori lunak akan memungkinkan Anda membedakan dari tensor NCHW yang Anda permutasi, versus tensor yang sebenarnya, secara fisik, NHWC. Tetapi tag lunak dalam bentuknya saat ini tidak mengikat, jadi saya tidak yakin seberapa berguna sebenarnya untuk kasus ini.

Cara lain untuk menyelesaikan masalah adalah dengan tensor bernama. Dengan tensor bernama, kita dapat menggunakan nama pada dimensi (logis) untuk mengetahui apakah kita melihat tensor sebagai NCHW (default yang diasumsikan) atau yang lainnya.

Namun saya tidak benar-benar berpikir format yang diblokir benar-benar sesuatu yang sama sekali berbeda. Itu dapat diekspresikan secara alami (dengan beberapa abstraksi yang bagus). Jika ada deskripsi format umum, maka orang lain dapat mendaftarkan format baru dengan pemblokiran/langkah sewenang-wenang.

Ada lebih banyak komentar tentang topik di sini: https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374

@ezyang Terima kasih atas jawabannya. Ya, tag format lunak dapat membantu. Kekhawatirannya adalah mungkin tidak cukup fleksibel karena urutan dimensi dapat berubah-ubah. Juga itu sendiri tidak dapat dihitung. Nama tensor memiliki arti semantik untuk setiap dimensi, tetapi mungkin perlu beberapa fasilitas lagi untuk mendukung saya ragu.

Secara pribadi saya akan berpikir ini dapat diselesaikan dengan memperkenalkan peta dari urutan langkah (fisik) ke urutan ukuran NCHW (logis). Seperti yang saya usulkan di atas, untuk NCHW hampir sama dengan desain saat ini; untuk NHWC, sizes masih NCHW, strides akan di urutan (N, H, W, C). Dan kita menggunakan stride_index = (0, 2, 3, 1) untuk menentukan indeks dimensi langkah.

Selain itu, kombinasi strides dan stride_index dapat digunakan untuk mewakili format tensor apa pun. Hal ini dapat memberikan keleluasaan kepada orang lain untuk mendaftarkan format data baru.

@ezyang

Operasi mempertahankan perilaku format memori

Jika tensor NHWC fisik dapat terjadi murni melalui langkah, ini secara teknis melanggar BC, kecuali jika Anda membuatnya hanya mempertahankan format memori ketika tag format memori ada (tetapi sepertinya Anda tidak ingin ini memiliki makna semantik, jadi saya saya tidak yakin apa yang disarankan proposal saat ini.) Saya tidak yakin apakah ini benar-benar melanggar kode siapa pun dalam praktiknya.

Ketika operasi aritmatika dan ambang batas dipindahkan ke TensorIterator, itu secara teknis melanggar SM (karena format memori operan dulu tidak dipertahankan, dan TensorIterator mempertahankannya). Status quo sekarang sangat tidak konsisten - ambang batas mempertahankan tata letak, semua operasi unary lainnya tidak, obor.where tidak, operasi aritmatika mempertahankan tata letak jika kedua operan memiliki tata letak yang sama, tetapi akan default ke "nchw" atau tensor yaitu contiguous dalam pemahaman saat ini jika ada ketidakcocokan, saya tidak yakin apa yang terjadi untuk penyiaran.
Anda juga membuat poin bagus tentang empty_like dan sejenisnya menjaga tata letak bukan BC. Mungkin juga perlu argumen tata letak, seperti is_contiguous dalam proposal

x.is_contiguous(torch.memory_format.channels_first)

@ezyang @ngimel

Ada satu masalah dengan empty_like; semantik yang saat ini ditentukan adalah Anda membuang semua informasi langkah, jadi, tidak mungkin untuk mempertahankan tata letak dan menjadi BC.

Anda juga membuat poin bagus tentang empty_like dan sejenisnya menjaga tata letak bukan BC.

Jika kita tidak bergantung pada langkah untuk mengekspresikan urutan fisik, empty_like tidak perlu mematahkan BC. Ada 3 macam info dimensi dalam tensor:

  • bentuk: ukuran
  • urutan logika: info pesanan direkam dalam langkah (biasanya digunakan untuk mendukung transpose atau permute)
  • tatanan fisik: NCHW atau NHWC (dapat dialamatkan sebagai stride_index seperti yang saya usulkan).

Saat ini tatanan fisik sama dengan bentuk/ukuran. Jadi kami hanya menjatuhkan urutan logika secara bertahap. Pertimbangkan kita memisahkan bentuk dan tatanan fisik, kita juga bisa hanya menjatuhkan urutan logika tetapi mempertahankan bentuk dan tatanan fisik untuk empty_like . Itu berarti size() dan stride_index() akan dipertahankan, tetapi stride() akan direset. Khususnya, empty_like dari tensor NHWC akan mengembalikan tensor bersebelahan NHWC dengan info bentuk yang sama yang ditentukan.

@uyongw Saya tidak yakin itu akan menjadi ide yang baik untuk mengubah empty_like ; sekarang semantiknya cocok dengan numpy's empty_like .

Status quo sekarang sangat tidak konsisten - ambang batas mempertahankan tata letak, semua operasi unary lainnya tidak, obor.where tidak, operasi aritmatika mempertahankan tata letak jika kedua operan memiliki tata letak yang sama, tetapi akan default ke "nchw" atau tensor yang berdekatan di pemahaman saat ini jika ada ketidakcocokan, saya tidak yakin apa yang terjadi untuk penyiaran.

@ngimel , ya, ini tidak terlalu konsisten sekarang. Saya pikir bagian dari upaya untuk merepresentasikan format memori adalah membuat operator kami ke kondisi yang konsisten

@zou3519 numpy's empty_like yang Anda tautkan memiliki argumen order yang secara default " cocok dengan tata letak prototipe sedekat mungkin.". Bukan itu yang dilakukan empty_like di pytorch saat ini (itu mengembalikan "nchw"- tensor yang berdekatan, bahkan jika prototipe tidak bersebelahan)

Oh, begitu, aku membacanya terlalu cepat. Dalam hal ini akan menyenangkan untuk memiliki numpy kecocokan kosong_like kami juga dan itu akan (mungkin?) Baik untuk memiliki tata letak memori di sini juga

@ zou3519 Ya apa yang saya coba katakan adalah untuk menjaga semantik saat ini (drop urutan logis sebagai @ezyang dan @ngimel disebutkan) dan dalam waktu yang sama melestarikan tata letak fisik seperti default numpy ini. Dengan demikian untuk prototipe NCHW perilakunya akan sama seperti sebelumnya. Untuk prototipe NHWC, perilakunya akan tetap kompatibel, yaitu, tensor baru akan bersebelahan NHWC, bukan NCHW bersebelahan jika Anda tidak mengubah implementasi saat ini.

Dua pertanyaan:

  • Apa yang terjadi jika tensor NHWC ditambahkan ke tensor NCHW?
  • Bagaimana dengan mengatasi kelemahan (B) dengan membuat metode seperti t.channel_dim() pada tensor yang mengembalikan nilai integer yang menunjukkan di mana dimensi secara fisik? Pendekatan ini bahkan mungkin diperlukan untuk memungkinkan format lain, seperti format blok, dipilih tanpa perubahan jaringan.

Jika kita membahas kontra (B) dengan poin terakhir, maka (B) tampaknya lebih baik bagi saya. Ini secara intuitif jelas dan kesalahan logis harus mudah dideteksi. Semua operasi yang ada dapat bekerja pada tensor juga, karena terlihat seperti tensor bersebelahan lainnya. Operasi yang dapat memahami semantik (analog dengan proposal tensor bernama) juga akan berfungsi seperti yang diharapkan.

@zou3519 numpy's empty_like yang Anda tautkan memiliki argumen order yang secara default " cocok dengan tata letak prototipe sedekat mungkin.". Bukan itu yang dilakukan empty_like di pytorch saat ini (itu mengembalikan "nchw"- tensor yang berdekatan, bahkan jika prototipe tidak bersebelahan)

Kami berencana untuk mempertahankan format dalam kasus seperti itu (untuk tensor yang diformat memori)

Apa yang terjadi jika tensor NHWC ditambahkan ke tensor NCHW?
Operasi dengan tensor berformat memori akan mengembalikan tensor berformat memori. Jika kedua tensor diformat memori, format output akan ditentukan oleh tensor pertama.

Dua hal yang akan saya tambahkan:

Kami berencana untuk mempertahankan format dalam kasus seperti itu (untuk tensor yang diformat memori)

Kita perlu mengaudit penggunaan yang ada, karena sering kali operator akan memanggil empty_like dan kemudian menganggapnya NCHW bersebelahan. Dan saya tidak tahu bagaimana kita akan berurusan dengan kode pihak ketiga. Sepertinya kita memerlukan default yang berbeda dari numpy jika kita ingin mempertahankan BC.

Operasi dengan tensor yang diformat memori akan mengembalikan tensor yang diformat memori. Jika kedua tensor diformat memori, format output akan ditentukan oleh tensor pertama.

Saya juga akan menambahkan, jika Anda benar-benar peduli dengan format keluaran Anda -- berikan tensor keluaran.

Setuju dengan empty_like, ada beberapa kasus di mana hasil dari empty_like/zeros_like dll diasumsikan nchw-contiguous (bersebelahan secara fisik harus saya katakan, dalam banyak kasus itu bukan operasi gambar).
Melewati tensor keluaran bukanlah pilihan dalam banyak kasus, karena fungsi dengan out kwarg tidak dapat dibedakan.

Banyak masalah kami berasal dari inkonsistensi tata letak keluaran yang diharapkan. Kami tidak dapat menyelesaikan semuanya sekaligus, tetapi kami dapat mencoba mengunci status saat ini (setidaknya untuk langkah) dan menyelesaikannya satu per satu. Jadi inilah proposalnya.

Python API

Perkenalkan torch.memory_format baru

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

Tensor akan membutuhkan konversi format memori eksplisit

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

Untuk 'menandai' mereka dengan format tertentu:

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)

Sekarang tentang empty_like dan sejenisnya:

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

Karena sebenarnya:

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

Jika kita ingin mempertahankan 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

Demikian pula:

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

Itu berarti kita dapat secara perlahan mendefinisikan setiap fungsi memory_format default ke keadaan dunia saat ini, mengklasifikasikannya dan memperhatikan bagaimana kita mengubahnya di masa depan.

Jika Anda menentukan tensor, TensorOptions saat ini diabaikan (dalam kasus terbaik mereka melempar pengecualian adalah misalnya ketidakcocokan opsi perangkat yang diteruskan dengan perangkat tensor out ).

Format memori seharusnya ringan, sehingga setiap permutasi akan kehilangannya.

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)

Tidak yakin tentang padding, akan menghargai bantuan di sini.

Namun kita dapat membuat tensor 'tag' x.to(memory_format=torch.memory_format.nhwc) dengan format yang tepat dan mengembalikan diri

Multiprosesor

Akan mempertahankan 'tag' format memori

Blokir format memori

API di atas tidak bergantung pada dimensi/langkah/ukuran, yang berarti kami dapat memperluas fungsionalitas di masa mendatang dengan tetap menggunakan API yang sama.

API internal

Operator akan dapat bercabang berdasarkan format memori

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

Jika kita melakukan memory_format sebagai TensorOptions, kita dapat memikirkan percabangan pada tingkat pengiriman (mirip dengan perangkat, tata letak)

Umpan balik kecil dari proposal

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

terlalu membatasi (karena kami juga ingin menangani 1D dan 3D selain 2D), dan channels_first/channels_last dari proposal asli lebih mengakomodasi untuk tujuan ini.

Setuju, kita perlu penamaan yang lebih baik. channels_first terdengar hampir benar kecuali batch berjalan lebih dulu =)

Saya suka proposal terbaru Anda. Apakah penanganan .contiguous() akan berubah? Apakah Anda memerlukan .contiguous(memory_format=<...>)? Jika demikian, dan banyak operasi hanya memanggil .contiguous(), mereka masih dapat memformat memori dengan tidak benar. Banyak operasi saat ini juga mengalokasikan output sebagai empty_like(), yang akan memiliki efek yang sama. Apakah rencananya akan memperbarui ini untuk mendeteksi format memori dari input dan membuat panggilan bersebelahan dan kosong_seperti yang benar?

Adapun saat ini pengguna kami (dan semua perpustakaan) mengharapkan .contiguous() untuk mengembalikan tensor bersebelahan memori dengan langkah dalam urutan menurun.

Kita tidak bisa melanggar kontrak ini. Namun, kabar baiknya adalah: segera setelah kami mendukung opsi memory_format, JIT akan dapat memahami kapan lebih efisien untuk memanggil .contiguous(memory_format=...) daripada format klasik.

@VitalyFedyunin Apakah kita berasumsi bahwa operasi seperti di bawah ini tidak diperbolehkan?

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)

Satu varian lagi adalah:

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 - mengapa pengguna memanggil .permute(0,2,3,1) sejak awal? Semua tensor dalam proposal ini memiliki ukuran semantik (n,c,h,w), yang berarti bahwa ukuran(1) mengembalikan saluran Anda. Itulah yang diasumsikan oleh perpustakaan standar PT hari ini dan apa yang diasumsikan dalam proposal ini juga. Jadi seseorang kemungkinan besar tidak akan pernah memanggil .permute sama sekali

Dapatkah manajer konteks berguna untuk memungkinkan pengguna mengganti format memori tensor yang dialokasikan dalam lingkup manajer ke format tertentu ?

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

Saya tidak suka ide manajer konteks, karena akan melonggarkan kontrol memory_format.

Sebagai contoh:

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

Ketika memory_format eksplisit memperjelas:

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

Jika perlu, kami dapat menambahkan sintaks untuk mengizinkan:

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

@raghuramank100 tidak perlu mengubah.

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

Akan melakukan semua pekerjaan kotor untuk Anda, menjaga agar redup tetap sama seperti di x.

Jadi:

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)

Dan Anda dapat terus menangani nhwc dalam format ini

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

@VitalyFedyunin Itu masuk akal.

Dari sudut pandang pengguna, penamaan metode (jika tetap seperti ini) tampaknya menyesatkan bagi saya karena "ke" sudah merupakan cara yang disarankan untuk mentransfer Tensor ke perangkat yang berbeda.

Juga, bagaimana dengan sesuatu seperti Numpy untuk mengonversi array C_ORDER dan F_ORDER?

numpy.asfortranarray()
numpy.ascontiguousarray()

Seseorang dapat dengan mudah membayangkan sesuatu seperti:

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

@VitalyFedyunin : Saya mengerti bahwa konversi ke memory_format yang berbeda menghilangkan kebutuhan pengguna untuk mengubah secara manual. Namun, setelah fungsi ini tersedia di obor, apa yang akan terjadi jika pengguna memanggil fungsi dalam urutan yang saya uraikan di atas? Setidaknya kita harus memiliki pesan peringatan/kesalahan yang menyatakan bahwa transformasi tata letak gagal.

@VitalyFedyunin : Saya mengerti bahwa konversi ke memory_format yang berbeda menghilangkan kebutuhan pengguna untuk mengubah secara manual. Namun, setelah fungsi ini tersedia di obor, apa yang akan terjadi jika pengguna memanggil fungsi dalam urutan yang saya uraikan di atas? Setidaknya kita harus memiliki pesan peringatan/kesalahan yang menyatakan bahwa transformasi tata letak gagal.

Ini hanya mungkin jika kita mengimplementasikan tensor bernama. Karena sekarang:

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

Tidak ada yang bisa memberi tahu saya apakah saya baru saja membuat nchw atau nhwc.

Mungkin saya salah memahami proposal aslinya, tetapi bukankah tag format memori yang direkam seharusnya memperjelas situasi ini?

@VitalyFedyunin Masuk akal, kita perlu memastikan bahwa ini dikomunikasikan kepada pengguna akhir saat API ini stabil.

@dzhulgakov @VitalyFedyunin Setelah meninjau #19975, saya memiliki beberapa kekhawatiran baru tentang tag format memori yang direkam dalam tensor. Masalah dasar saya adalah, bagaimana kita memutuskan apakah operasi harus mempertahankan tag memori? Awalnya, saya berpikir bahwa hanya operator "sadar tata letak alternatif" yang perlu memiliki kecerdasan ini. Tetapi melihat patch Vitaly, saya pikir beberapa operator inti juga perlu disesuaikan. Misalnya, pertimbangkan x[0] ; jika x sebelumnya adalah tensor NHWC, maka saya harus mengeluarkan tensor HWC setelah melakukan ini. Saya cukup yakin bahwa tambalan Vitaly tidak menangani ini dengan benar, dan saya yakin itu akan sangat membingungkan pengguna. Mungkin satu-satunya operator yang terpengaruh adalah mereka yang bermain-main dengan langkah (dalam hal ini, tidak terlalu banyak dari mereka dan kita dapat mengauditnya secara manual), tetapi sepertinya hal yang harus kita lakukan. Bagaimana menurutmu?

Tunggu, tensor masih tetap terindeks dalam urutan: 0-dim N; 1-redup C; 2-redup H; 3rd-dim W. Jadi x[0] mengembalikan tensor dengan 0-dim C; 1-redup H; 2nd-dim W. Terlepas dari apakah x adalah tata letak memori channels_first atau channels_last.

Kalau tidak, memory_format tidak masuk akal dan kita hanya perlu mengubah tensor.

Maksud saya adalah bahwa tag format memori tidak dipertahankan. Jika tensor input diberi tag channels_last , tensor baru diberi tag any

cc @zou3519 , logika propagasi tata letak di sini mengingatkan saya banyak propagasi dimensi bernama dalam karya tensor bernama.

Saya masih mengejar proposal ini. Tapi @ezyang kita bisa melacak logika propagasi tata letak dengan menyebarkan bendera per dimensi (atau nama) dan kemudian itu akan setara dengan memiliki tensor bernama dengan konvensi nama

Akan lebih baik jika kita dapat menyelaraskan logika tag memori dan logika tensor bernama dengan tepat, bahkan jika kita memilikinya sebagai dua jalur implementasi terpisah di awal.

Fase 1

Memperluas fungsionalitas dua fungsi tensor .is_contiguous dan .contiguous (baik python dan c++ api).

Catatan: Kami memiliki beberapa keluhan tentang fungsi .to(memory_format) , dan memutuskan untuk tidak mendukungnya.

  1. .contiguous sekarang mendukung argumen khusus kata kunci opsional - memory_format , yang dapat berupa torch.contiguous_format atau torch.channels_last .

    • Menggunakan torch.contiguous_format akan mempertahankan perilaku .contiguous() .

    • Memanggil x.contiguous(memory_format=torch.channels_last) mengembalikan tensor baru yang mempertahankan tata letak semantik (NCHW) yang sama, tetapi memiliki pola alokasi memori yang berbeda.

      x.contiguous(memory_format=torch.channels_last) mengharapkan tensor input menjadi 3d, 4d atau 5d; dan gagal sebaliknya.

  2. .is_contiguous sekarang mendukung argumen khusus kata kunci opsional - memory_format , yang dapat berupa torch.contiguous_format atau torch.channels_last .

    • x.is_contiguous(memory_format=torch.contiguous_format) mempertahankan fungsionalitas yang sama dengan x.is_contiguous() dan tetap tidak berubah.

    • x.is_contiguous(memory_format=torch.channels_last) mengembalikan nilai true jika A) tensor input bersebelahan dalam memori DAN B) dialokasikan dalam memori dalam format NWHC (atau serupa untuk 3d,5d).

Catatan: Pada akhir fase satu x.is_contiguous(memory_format=torch.channels_last) akan menghitung status Tensor pada setiap panggilan. Fungsi ini akan diperbarui nanti.

Fase 2

Pertahankan format memori untuk operasi tertentu:

  1. Operator elemen-bijaksana unary mempertahankan format memori channels_last.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.sin()
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  2. Operator berbasis elemen biner ( add , sub , mul , div ) mempertahankan format memori channels_last.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b * torch.randn(H,W)
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  3. Operasi apa pun yang melebihi ukuran, langkah, dan peredupan akan mengatur ulang format memori.

    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
    

Tetap ragu-ragu

  1. Hasil operasi reshape (dan sejenisnya), jika output 'channels_last' terbaca

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

    Catatan: Saat ini memory_format tidak dipertahankan

  2. Hasil operasi NHWC + NCHW. Apakah itu NHWC?

    Catatan: Saat ini NHWC + NCHW -> NHWC dan NCHW + NHWC -> NHWC

Bagaimana dengan operasi seperti cat/split? Ini akan berguna bagi mereka untuk mempertahankan format memori.

@ezyang - tentang pengindeksan saya pikir kita harus berhenti di suatu tempat. Tata letak memori yang berbeda tidak sepenuhnya transparan dan beberapa operasi harus diizinkan untuk mengabaikannya. Saya berpendapat bahwa x[0] harus diizinkan untuk menghapus tag, termasuk x[0].unsqueeze(0)

Seperti yang disebutkan Raghu, cat/split harus mempertahankan tag jika memungkinkan karena ini adalah penggunaan yang cukup umum. Saya pikir aturan umum adalah bahwa selama operasi tidak mengubah peringkat atau menyusun ulang sumbu secara aneh, kita harus mempertahankan tag. Jika peringkat berubah - semua taruhan dibatalkan.

Saya setuju dalam beberapa kasus kita akan kehilangan tag. Tapi saya tidak setuju tentang x[0] . Bagi saya itu sepertinya cara yang sangat umum untuk beralih dari NCHW ke CHW .

Setelah beberapa percakapan tentang betapa membingungkannya memiliki Tensor untuk membawa (atau tidak) 'tag' channels_last, kami memutuskan untuk mengambil risiko memperkenalkan bc-breaking change dan auto-promote tensor ke format channels_last.

Apa artinya bagi API:

Tensor 3d,4d,5d apa pun dengan langkah seperti N,1,H,[W,[D]] akan secara otomatis mendapatkan format memori channels_last.

Untuk membuatnya berfungsi, kami akan mengambil tindakan pencegahan khusus untuk menjamin bahwa operator pada channels_last tensor yang mengeluarkan channels_last tensor setidaknya akan memiliki kinerja yang serupa dengan operator pada tensor yang berdekatan.

Dalam kasus skenario terburuk:
1) Pengguna dapat memanggil .contiguous() pada output.
2) Kami akan menulis kode promosi otomatis sedemikian rupa sehingga hampir sepele untuk mengubah perilaku ini.

Efek samping dari promosi mobil tersebut adalah:

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

Di sisi lain dapat menyelesaikan kasus (setelah modifikasi ringan):

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

Dari konversi yang lambat, sesuai permintaan @ezyang

Natalia Gimelshein [14:19]
Jadi saya anggap tidak akan ada konsep 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]
z akan menjadi channel_last

Vitaly Fedyunin [8:25]
jika x1 bukan channels_last di salah satu varian yang diusulkan (kecuali kami mengubah fungsi chunk untuk tidak mengembalikan tampilan), jadi konvolusi akan mengonversinya ke format bersebelahan (channels_first) dan kembali bersebelahan juga

Vitaly Fedyunin [9:12]
@ngimel terima kasih atas umpan baliknya, saya pikir kita bisa keluar dengan definisi yang lebih bermakna dari channels_last untuk mencakup sebagian besar kasus ketika operasi seperti tampilan terlibat. Akan membuat Anda tetap dalam lingkaran.

Natalia Gimelshein [9:36]
membalas sebuah utas:
Jadi sepertinya ada masalah, ya? Chunking lintas dimensi saluran adalah hal yang relatif umum, misalnya dalam jaringan seperti awal. Jadi, jika tensor adalah saluran chunked tensor pertama, output konvolusi akan menjadi saluran-pertama (yang merupakan perilaku intuitif, dan kemungkinan besar apa yang diinginkan pengguna), jika tensor adalah saluran chunked-terakhir maka output konvolusi akan sekali lagi menjadi saluran pertama?

Natalia Gimelshein [9:39]
membalas sebuah utas:
Tetapi hanya karena perilaku penambahan non-komutatif dan y menjadi argumen pertama dan saluran terakhir, bukan? Apa hasil dari x1+y ? Apakah kita memiliki aturan propagasi tata letak untuk operasi biner di suatu tempat?

Vitaly Fedyunin [10:44]
1) Ya, itu adalah masalah yang akan kita selesaikan dengan proposal alternatif. Saya akan melakukan beberapa tes sekarang dan akan menuliskannya minggu ini (dalam satu atau dua hari).
2) x1+y - juga harus menghasilkan channels_last jika tidak akan membingungkan, dan ya, kami akan menuliskan aturan propagasi tata letak.

Saya pikir pengamatan yang saya lakukan kepada @VitalyFedyunin ketika kami mengobrol tentang hal ini secara langsung (tapi saya rasa saya tidak ingat untuk menuliskan ini di mana pun), adalah bahwa ada tingkat kebebasan dalam konvolusi, yaitu ketika mendapat argumen yang tata letak memorinya tidak cocok dengan apa pun yang ia ketahui cara mengimplementasikannya secara efisien, tata letak mana yang harus digabungkan? Untuk alasan SM, sambungkan ke saluran terlebih dahulu diperlukan, tetapi kami telah membuat keputusan sewenang-wenang di sini--bisa dibilang Anda juga dapat menyambungkan saluran terakhir. Mungkin kita harus memiliki semacam sakelar lokal utas yang mengatakan apa defaultnya?

Tapi sepertinya ada banyak detail di sini untuk diurai, dan saya tidak yakin apakah itu akan berhasil pada akhirnya.

Jadi kekaburan konvolusi (dan operator yang sadar tata letak lainnya, dalam hal ini, misalnya upsampling yang baru-baru ini saya lihat dimulai dengan memanggil .contiguous() pada input - jadi apa artinya?) adalah alasan utama untuk memperkenalkan tag, iirc.

Ya, jadi saya baik-baik saja dengan membuka desain tag lagi, tapi kemudian kita
harus serius memecahkan masalah bagaimana menyebarkan tag ini,
bahkan ketika Anda kehilangan tata letak (seperti halnya dengan chunking
pada saluran). Saya jauh lebih suka membuat "tata letak saat ini" beberapa
semacam manajer konteks, daripada membuatnya bergantung pada data.

Kutipan dari pesan ngimel 19-06-2019 12:43:45 -0700:

Jadi kekaburan konvolusi (dan operator yang sadar tata letak lainnya, dalam hal ini, misalnya upsampling yang baru-baru ini saya lihat dimulai dengan memanggil .contiguous() pada input - jadi apa artinya?) adalah alasan utama untuk memperkenalkan tag, iirc.

BTW kenapa kita harus membuat konsep baru daripada hanya berpegang pada layout ? Saya tidak berpikir bahwa representasi yang jarang memiliki konsep tata letak yang terdefinisi dengan baik seperti "channels_last", jadi kami tidak perlu mewakili produk memory_formats * layouts ( layouts mengacu pada penggunaan saat ini ), tetapi hanya memory_format + layouts berarti bahwa boleh saja menggunakan argumen yang sama seperti dulu? Bagi saya itu lebih pendek, lebih baik, dan akan membiarkan kita menghindari memperluas tanda tangan pabrik ke seribu argumen.

opsi tata letak dipertimbangkan (periksa lampiran), tetapi kami menemukan itu akan menyebabkan banyak duplikasi kode serta melarang konversi otomatis tensor ke format memory_yang berbeda dengan cepat

setelah semua memory_format adalah cara untuk stride tensor dan untuk dengan mudah memilih kernel dan output yang dioptimalkan yang merupakan properti dari strided tensor, bukan kelas yang sama sekali berbeda

Dalam beberapa hal tata letak yang jarang juga merupakan cara untuk dengan mudah memilih kernel yang dioptimalkan untuk array yang sebagian besar nol Bisakah Anda menguraikan bagian "serta melarang konversi otomatis ke format memory_format yang berbeda dengan cepat"?

Ini mungkin pertanyaan yang naif, tetapi mengapa PyTorch mempertimbangkan API ini versus hanya mengekspos opsi untuk menggunakan NHWC di ops itu sendiri, yang secara langsung akan memanggil kernel CuDNN yang mendasarinya jika tersedia?

Sepertinya untuk kasus penggunaan umum (mencampur operasi gambar seperti konv dan penggabungan dengan arsitektur LM) ini akan menjadi solusi yang mudah. Sebagai pengembang, yang saya inginkan hanyalah Conv2d(..., nhwc=True) . Apakah ada alasan mengapa ini tidak masuk akal?

@rewonc kami telah mempertimbangkan pendekatan serupa (menambahkan opsi ke operator alih-alih menurunkan kernel dari striding), dan merasa sulit untuk menerapkannya karena alasan berikut:

  • Pendekatan ini akan membutuhkan kernel untuk melakukan restriding tensor yang berdekatan untuk menerapkan kernel NHWC.
  • Operator berikutnya harus mengatur ulang input lagi (ke contiguous) kecuali jika ia juga memiliki opsi nhwc=True .
  • Untuk memiliki NHWC di seluruh jaringan, setiap operator membutuhkan opsi nhwc=True .

PS. Jika Anda khawatir tentang fungsi CudNN Ex , kami ingin mengekspos cudnn_batch_norm_nhwc dan operator serupa.

Hai @VitalyFedyunin , kami melihat tensor bernama didukung di PyTorch 1.3. Bisakah itu menyelesaikan (atau menyelesaikan sebagian) masalah pada dukungan format NHWC (atau bahkan diblokir)? Apakah ada rencana untuk memajukan status NHWC berdasarkan nama tensor?

Kami bergerak maju dengan dukungan terakhir saluran, saya akan menerbitkan peta jalan minggu ini di sini dan di saluran yang kendur. Kami tidak mempertimbangkan untuk menambahkan format yang diblokir dalam waktu dekat (karena akan memerlukan penulisan ulang SEMUA operator).

Terima kasih. Itu bagus!

Menyelesaikan tugas dan kemajuan di dalam https://github.com/pytorch/pytorch/issues/28619

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

NgPDat picture NgPDat  ·  3Komentar

ikostrikov picture ikostrikov  ·  3Komentar

soumith picture soumith  ·  3Komentar

a1363901216 picture a1363901216  ·  3Komentar

eliabruni picture eliabruni  ·  3Komentar