Scala + MXNet = Microservice dengan neuron in prod


Ada sejumlah besar manual dan contoh di Internet, yang menjadi dasar Anda, para pembaca yang budiman, akan dapat “tanpa banyak kesulitan” dan dengan biaya waktu “minimal” menulis kode yang dapat membedakan kucing dari anjing dalam sebuah foto. Dan mengapa kemudian membuang waktu untuk artikel ini?

Menurut saya, yang utama dari semua contoh ini adalah kemungkinan yang terbatas. Anda mengambil contoh - bahkan dengan jaringan saraf dasar yang ditawarkan penulis - meluncurkannya, bahkan mungkin berhasil, dan apa selanjutnya? Bagaimana membuat kode sederhana ini mulai bekerja di server produksi? Bagaimana cara memperbarui dan memeliharanya? Di sinilah kesenangan dimulai. Saya tidak dapat menemukan deskripsi lengkap dari proses dari saat "Ya, insinyur ML melatih jaringan saraf" hingga "akhirnya kami meluncurkannya ke dalam produksi". Dan saya memutuskan untuk menutup celah ini.

Saya tidak akan berbicara tentang cara mengajar jaringan saraf hal-hal lucu baru yang akan menyenangkan Anda dan membantu Anda mendapatkan banyak uang kertas renyah. Ini adalah topik yang bagus untuk artikel terpisah. Sebagai contoh, saya akan menggunakan jaringan saraf yang dapat diunduh secara bebas. Tugas utama yang saya tetapkan sendiri adalah untuk memberikan deskripsi lengkap tentang proses memperkenalkan jaringan saraf ke dalam operasi.

Saya segera menjawab pertanyaan "Mengapa tidak dengan Python?": Kami menggunakan Scala untuk solusi produksi karena penulisan kode multi-utas yang lebih nyaman dan stabil.

Isi


1. Pernyataan masalah
2. Teknologi yang digunakan
3. Mempersiapkan wadah buruh pelabuhan dasar
4. Struktur proyek
5. Pemuatan jaringan saraf
6. Implementasi REST API
7. Pengujian
8. Merakit layanan microser berdasarkan gambar dasar
9. Memulai layanan microser pada server produksi dengan GPU
Kesimpulan
Referensi

1. Pernyataan masalah


Misalkan kita memiliki database besar foto dengan objek yang berbeda, dan kita perlu membuat layanan mikro yang akan menerima gambar dalam permintaan HTTP POST dan merespons dalam format JSON. Jawabannya harus berisi jumlah objek yang ditemukan dan kelas mereka, tingkat probabilitas bahwa ini persis objek kelas yang dideklarasikan, dan koordinat persegi panjang yang mencakup batas-batas setiap objek.

2. Teknologi yang digunakan


  • Scala 2.12.7 + set minimum perpustakaan tambahan, Sbt 1.2.6 dengan plugin Sbt-pack 0.12 untuk membangun kode sumber.
  • MXNet 1.3.1 (versi stabil terbaru pada saat penulisan), dikompilasi untuk Scala 2.12.
  • Server dengan kartu grafis Nvidia.
  • Cuda 9.0 dan Cudnn 7 diinstal pada server.
  • Java 8 untuk menjalankan kode yang dikompilasi.
  • Docker untuk kemudahan perakitan, pengiriman dan peluncuran microservice di server.

3. Mempersiapkan wadah buruh pelabuhan dasar


Untuk layanan Microsoft kami, Anda akan memerlukan gambar Docker dasar di mana jumlah minimum dependensi yang diperlukan untuk menjalankan akan diinstal. Untuk perakitan, kami akan menggunakan gambar dengan Sbt yang diinstal tambahan. Ya, kami akan membangun sumbernya sendiri bukan di lingkungan lokal, tetapi di wadah Docker. Ini akan memfasilitasi transisi lebih lanjut ke perakitan melalui CI, misalnya, melalui gitlab CI.

Struktur folder:

\ | ----- install | | ----- java8.sh | | ----- mxnet_2_12.sh | | ----- opencv.sh | | ----- sbt.sh | | ----- scala.sh | | ----- timeZone.sh | ----- scala-mxnet-cuda-cudnn | ----- Dockerfile.2.12-1.3.1-9-7-builder | ----- Dockerfile.2.12-1.3.1-9-7-runtime 

Dockerfile.2.12-1.3.1-9-7-runtime


Gambar ini akan digunakan untuk peluncuran microservice terakhir. Ini didasarkan pada gambar resmi dari Nvidia dengan CUDA 9.0 dan CUDNN yang telah diinstal 7. Dokumentasi untuk MXNet 1.3.1 mengklaim bekerja dengan CUDA 8.0, tetapi, seperti yang telah ditunjukkan, semuanya berjalan baik dengan versi 9.0, dan bahkan sedikit lebih cepat.

Selain itu, kami akan menginstal Java 8, MXNet 1.3.1 (kami akan membangunnya di bawah Scala 2.12), OpenCV 3.4.3, dan utilitas Linux untuk mengatur zona waktu pada gambar ini.

 #        Nvidia  cuda 9.0  cudnn 7 FROM nvidia/cuda:9.0-cudnn7-devel AS builder #    ENV MXNET_VERSION 1.3.1 ENV MXNET_BUILD_OPT "USE_OPENCV=1 USE_BLAS=openblas USE_CUDA=1 USE_CUDA_PATH=/usr/local/cuda USE_CUDNN=1" ENV CUDA_STUBS_DIR "/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs" ENV OPEN_CV_VERSION 3.4.3 ENV OPEN_CV_INSTALL_PREFIX /usr/local ENV JAVA_HOME /usr/lib/jvm/java-8-oracle/ ENV TIME_ZONE Europe/Moscow #     COPY install /install RUN chmod +x -R /install/* #   RUN apt-get update WORKDIR /install RUN ./timeZone.sh ${TIME_ZONE} RUN ./java8.sh RUN ./mxnet_2_12.sh ${MXNET_VERSION} "${MXNET_BUILD_OPT}" ${CUDA_STUBS_DIR} RUN ./opencv.sh ${OPEN_CV_VERSION} ${OPEN_CV_INSTALL_PREFIX} #     RUN apt-get autoclean -y && \ rm -rf /var/cache/* /install #       FROM nvidia/cuda:9.0-cudnn7-devel COPY --from=builder --chown=root:root / / 

Script timeZone.sh java8.sh dan opencv.sh cukup sepele, jadi saya tidak akan membahasnya secara rinci, mereka disajikan di bawah ini.

timeZone.sh


 #!/bin/sh #         TIME_ZONE=${1} #       apt-get install -y tzdata && \ ln -sf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && \ dpkg-reconfigure -f noninteractive tzdata 

java8.sh


 #!/bin/sh #  Java 8 apt-get install -y software-properties-common && \ add-apt-repository ppa:webupd8team/java -y && \ apt-get update && \ echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections && \ apt-get install -y oracle-java8-installer 

opencv.sh


 #!/bin/sh #   OpenCV     OPEN_CV_VERSION=${1} #        OPEN_CV_INSTALL_PREFIX=${2} OPEN_CV_TAR="http://github.com/opencv/opencv/archive/${OPEN_CV_VERSION}.tar.gz" #  OpenCV apt-get install -y wget build-essential cmake && \ wget -qO- ${OPEN_CV_TAR} | tar xzv -C /tmp && \ mkdir /tmp/opencv-${OPEN_CV_VERSION}/build && \ cd /tmp/opencv-${OPEN_CV_VERSION}/build && \ cmake -DBUILD_JAVA=ON -DCMAKE_INSTALL_PREFIX:PATH=${OPEN_CV_INSTALL_PREFIX} .. && \ make -j$((`nproc`+1)) && \ make install && \ rm -rf /tmp/opencv-${OPEN_CV_VERSION} 

Menginstal MXNet tidak sesederhana itu. Faktanya adalah bahwa semua rakitan perpustakaan ini untuk Scala dibuat berdasarkan versi kompilator 2.11, dan ini dibenarkan, karena perpustakaan menyertakan modul untuk bekerja dengan Spark, yang, pada gilirannya, ditulis dalam Scala 2.11. Menimbang bahwa kami menggunakan Scala 2.12.7 dalam pengembangan, perpustakaan yang dikompilasi tidak cocok untuk kami, dan kami tidak dapat turun ke versi 2.11. * Kami tidak bisa, karena banyaknya kode yang sudah ditulis pada Scala versi baru. Apa yang harus dilakukan Dapatkan banyak kesenangan mengumpulkan MXNet dari sumber untuk versi Scala kami. Di bawah ini saya akan memberikan skrip untuk membangun dan menginstal MXNet 1.3.1 untuk Scala 2.12. * Dan mengomentari poin utama.

mxnet_2_12.sh


 #!/bin/sh #   MXNet     MXNET_VERSION=${1} #     ++  MXNet     MXNET_BUILD_OPT=${2} #       CUDA     CUDA_STUBS_DIR=${3} LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${CUDA_STUBS_DIR}" #       MXNet   apt-get install -y git build-essential libopenblas-dev libopencv-dev maven cmake && \ git clone -b ${MXNET_VERSION} --recursive https://github.com/dmlc/mxnet /tmp/mxnet && \ cd /tmp/mxnet && \ make -j $(nproc) ${MXNET_BUILD_OPT} && \ ln -s ${CUDA_STUBS_DIR}/libcuda.so ${CUDA_STUBS_DIR}/libcuda.so.1 && \ sed -rim 's/([a-zA-Z])_2.11/\1_2.12/g' $(find scala-package -name pom.xml) && \ sed -im 's/SCALA_VERSION_PROFILE := scala-2.11/SCALA_VERSION_PROFILE := scala-2.12/g' Makefile && \ sed -im 's/<module>spark<\/module>/<\!--<module>spark<\/module>-->/g' scala-package/pom.xml && \ make scalapkg ${MXNET_BUILD_OPT} && \ mkdir -p /usr/local/share/mxnet/scala/linux-x86_64-gpu && \ mv /tmp/mxnet/scala-package/assembly/linux-x86_64-gpu/target/mxnet-full_2.12-linux-x86_64-gpu-${MXNET_VERSION}-SNAPSHOT.jar /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-${MXNET_VERSION}-SNAPSHOT.jar && \ rm -rf /tmp/mxnet && rm -rf /root/.m2 

Bagian yang paling menarik dimulai dengan baris ini:

 ln -s ${CUDA_STUBS_DIR}/libcuda.so ${CUDA_STUBS_DIR}/libcuda.so.1 && \ 

Jika Anda menjalankan perakitan MXNet seperti dalam instruksi, kami akan mendapatkan kesalahan. Kompiler tidak dapat menemukan perpustakaan libcuda.so.1, jadi kami akan menautkan dari perpustakaan libcuda.so ke libcuda.so.1. Ini mungkin tidak mengganggu Anda, ketika Anda memulainya di server produksi, kami akan mengganti perpustakaan ini dengan yang lokal. Perhatikan juga bahwa jalur ke pustaka CUDA dari variabel lingkungan CUDA_STUBS_DIR telah ditambahkan ke LD_LIBRARY_PATH . Jika ini tidak dilakukan, maka perakitan juga akan gagal.

Pada baris ini, kami mengganti versi Scala 2.11 dengan 2.12 di semua file yang diperlukan menggunakan ekspresi reguler, yang dipilih secara eksperimental, karena tidak cukup hanya mengganti 2.11 di mana-mana dengan 2.12:

 sed -rim 's/([a-zA-Z])_2.11/\1_2.12/g' $(find scala-package -name pom.xml) && \ sed -im 's/SCALA_VERSION_PROFILE := scala-2.11/SCALA_VERSION_PROFILE := scala-2.12/g' Makefile && \ sed -im 's/<module>spark<\/module>/<\!--<module>spark<\/module>-->/g' scala-package/pom.xml && \ make scalapkg ${MXNET_BUILD_OPT} && \ 

Dan kemudian ketergantungan pada modul untuk bekerja dengan Spark dikomentari. Jika ini tidak dilakukan, perpustakaan tidak akan dirakit.

Selanjutnya, jalankan perakitan, seperti yang ditunjukkan dalam instruksi, salin perpustakaan yang dirakit ke folder bersama dan hapus semua sampah yang dipompa Maven selama proses membangun (jika ini tidak dilakukan, gambar akhir akan tumbuh sekitar 3-4 GB, yang dapat menyebabkan DevOps Anda S gugup).

Kami mengumpulkan gambar, berada di direktori root proyek (lihat. Struktur folder):

 your@pc$ docker build -f Dockerfile.2.12-1.3.1-9-7-runtime -t entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime . 

Biarkan saya mengingatkan Anda untuk berjaga-jaga jika titik di bagian akhir mengatakan bahwa kami sedang melakukan perakitan dalam konteks direktori saat ini.

Sekarang saatnya berbicara tentang build image.

Dockerfile.2.12-1.3.1-9-7-builder


 #         runtime-,    FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #    ENV SCALA_VERSION 2.12.7 ENV SBT_VERSION 1.2.6 #     COPY install /install RUN chmod +x -R /install/* #       RUN apt-get update && \ cd /install && \ ./scala.sh ${SCALA_VERSION} && \ ./sbt.sh ${SBT_VERSION} #   RUN rm -rf /install 

Sederhana saja, kita tidak perlu Scala dan Sbt untuk memulai layanan microser kami, jadi tidak ada gunanya menyeret mereka ke gambar dasar untuk diluncurkan. Oleh karena itu, kami akan membuat gambar terpisah yang hanya akan digunakan untuk perakitan. Skrip scala.sh dan sbt.sh cukup sepele dan saya tidak akan membahasnya secara rinci.

scala.sh


 #!/bin/sh #   Scala     SCALA_VERSION=${1} SCALA_DEB="http://www.scala-lang.org/files/archive/scala-${SCALA_VERSION}.deb" #  Scala apt-get install -y wget && \ wget -q ${SCALA_DEB} -O /tmp/scala.deb && dpkg -i /tmp/scala.deb && \ scala -version && \ rm /tmp/scala.deb 

sbt.sh


 #!/bin/sh #   Sbt     SBT_VERSION=${1} SBT_DEB="http://dl.bintray.com/sbt/debian/sbt-${SBT_VERSION}.deb" #  Sbt apt-get install -y wget && \ wget -q ${SBT_DEB} -O /tmp/sbt.deb && dpkg -i /tmp/sbt.deb && \ sbt sbtVersion && \ rm /tmp/sbt.deb 

Kami mengumpulkan gambar, berada di direktori root proyek (lihat. Struktur folder):

 your@pc$ docker build -f Dockerfile.2.12-1.3.1-9-7-builder -t entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-builder . 

Di akhir artikel ada tautan ke repositori dengan semua file ini.

4. Struktur proyek


Setelah selesai mempersiapkan untuk perakitan proyek, mari kita lakukan apa yang Anda putuskan untuk menghabiskan waktu untuk artikel ini.

Proyek microservice kami akan memiliki struktur berikut:

 \ | ----- dependencies | | ----- mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar | ----- models | | ----- resnet50_ssd_model-0000.params | | ----- resnet50_ssd_model-symbol.json | | ----- synset.txt | ----- project | | ----- build.properties | | ----- plugins.sbt | ----- src | | ----- main | | | ----- resources | | | | ----- cat_and_dog.jpg | | | ----- scala | | | | ----- simple.predictor | | | | ----- Config | | | | ----- Model | | | | ----- Server | | | | ----- Run | | ----- test | | | ----- scala | | | | ----- simple.predictor | | | | ----- ServerTest | ----- build.sbt | ----- Dockerfile 

Ini adalah struktur standar proyek Scala, dengan pengecualian direktori dan dependensi direktori.
Direktori dependensi berisi perpustakaan MXNet untuk Scala. Itu dapat diperoleh dengan dua cara:

  • membangun MXNet di mesin tempat Anda akan mengembangkan (perhatikan bahwa perpustakaan tidak lintas platform; jika Anda membangunnya di Linux, itu tidak akan berfungsi pada Mac OS),
  • atau tarik keluar dari gambar Docker yang kami buat sebelumnya. Jika Anda memutuskan untuk membangun MXNet di lingkungan lokal, maka skrip mxnet_2.12.sh akan membantu Anda.

Anda dapat menarik pustaka dari gambar Docker seperti ini:

 #   your@pc$ mkdir dependencies #  Docker-    your@pc$ docker run -it --rm -v $(pwd)/dependencies:/tmp/dependencies entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #          ab38e73d93@root$ cp /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar /tmp/dependencies/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar ab38e73d93@root$ exit #  , ! your@pc$ ls dependencies/ mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar 

Direktori models berisi file-file dari jaringan saraf yang terlatih, Anda dapat mengunduhnya secara bebas sebagai berikut:

 #   your@pc$ mkdir models #     your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-symbol.json -P models your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-0000.params -P models your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/synset.txt -P models 

Selanjutnya secara singkat tentang file yang tidak menarik, tetapi berperan dalam proyek ini.

project / build.properties


 #   Sbt,   sbt.version = 1.2.6 

project / plugins.sbt


 //    sbt-pack addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.12") 

src / main / resources / cat_and_dog.jpg


Gambaran yang begitu indah, di mana jaringan saraf kita akan mencari kucing dan anjing.


build.sbt


 enablePlugins(PackPlugin) name := "simple-predictor" version := "0.1" scalaVersion := "2.12.7" unmanagedBase := baseDirectory.value / "dependencies" //  (   ) libraryDependencies ++= Seq( "org.json4s" %% "json4s-native" % "3.6.1", "org.scalatest" %% "scalatest" % "3.0.5" % Test, "org.scalaj" %% "scalaj-http" % "2.4.1" % Test ) //       packMain := Map("simple-predictor" -> "simple.predictor.Runs") //    bat-,      ,   Linux packGenerateWindowsBatFile := false //    JVM packJvmOpts := Map("simple-predictor" -> Seq( "-Xms3g", "-Xmx5g")) 

simple.predictor.Config


Objek ini menyimpan variabel global yang nilainya dibaca dari variabel lingkungan atau ditetapkan secara default.

 package simple.predictor import org.apache.mxnet.Context import scala.util.Try object Config { //    REST API val host: String = env("REST_HOST") getOrElse "0.0.0.0" //    REST API val port: Int = env("REST_PORT") flatMap (p => Try(p.toInt).toOption) getOrElse 8080 // URL,     POST-   val entryPoint: String = env("REST_ENTRY_POINT") getOrElse "/predict" //  ,       val threshold: Float = env("PROBABILITY_MORE") flatMap (p => Try(p.toFloat).toOption) getOrElse 0.5f //        val modelPrefix: String = env("MODEL_PREFIX") getOrElse "models/resnet50_ssd_model" //    (    ...-0000.params) val modemEpoch: Int = env("MODEL_EPOCH") flatMap (p => Try(p.toInt).toOption) getOrElse 0 //   ,     ,    512 val modemEdge: Int = env("MODEL_EDGE") flatMap (p => Try(p.toInt).toOption) getOrElse 512 //  ,   CPU ( ).  production  GPU val context: Context = env("MODEL_CONTEXT_GPU") flatMap { isGpu => Try(if (isGpu.toBoolean) Context.gpu() else Context.cpu()).toOption } getOrElse Context.cpu() private def env(name: String) = Option(System.getenv(name)) } 

simple.predictor.Run


Objek Run adalah titik masuk ke aplikasi.

 package simple.predictor import java.net.InetSocketAddress //     import simple.predictor.Config._ object Run extends App { //     REST- val model = new Model(modelPrefix, modemEpoch, modemEdge, threshold, context) val server = new Server(new InetSocketAddress(host, port), entryPoint, model) //   Ctrl + C    Runtime.getRuntime.addShutdownHook(new Thread(() => server.stop())) //      try server.start() catch { case ex: Exception => ex.printStackTrace() } } 

5. Pemuatan jaringan saraf


Jaringan saraf dimuat dalam konstruktor kelas simple.predictor.Model .

simple.predictor.Model


 package simple.predictor import java.awt.image.BufferedImage import org.apache.mxnet._ import org.apache.mxnet.infer.ObjectDetector import simple.predictor.Model.Prediction class Model(prefix: String, epoch: Int, imageEdge: Int, threshold: Float, context: Context) { //       val initShape = Shape(1, 3, imageEdge, imageEdge) val initData = DataDesc(name = "data", initShape, DType.Float32, Layout.NCHW) //           val model = new ObjectDetector(prefix, IndexedSeq(initData), context, Option(epoch)) //         ,       JSON private def toPrediction(originWidth: Int, originHeight: Int)(predict: (String, Array[Float])): Prediction = { val (objectClass, Array(probability, kx, ky, kw, kh)) = predict //        val x = (originWidth * kx).toInt val y = (originHeight * ky).toInt val w = (originWidth * kw).toInt val h = (originHeight * kh).toInt val width = if ((x + w) < originWidth) w else originWidth - x val height = if (y + h < originHeight) h else originHeight - y Prediction(objectClass, probability, x, y, width, height) } //     ,         ,     threshold def predict(image: BufferedImage): Seq[Prediction] = model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) } object Model { //   case class Prediction(objectClass: String, probability: Float, x: Int, y: Int, width: Int, height: Int) } 

Di bagian Anda mengatakan bahwa dalam jaringan saraf itu akan bekerja dengan NDArray dengan dimensi 1 x 3 x 512 x 512, di mana 1 adalah jumlah gambar yang akan terkandung dalam NDArray, 3 adalah jumlah warna, dan 512 x 512 - ukuran gambar (nilai imageEdge = 12 diatur dalam objek simple.predict.Config , ini adalah ukuran sisi gambar yang digunakan untuk melatih jaringan saraf). Semua deskripsi data ini diteruskan ke ObjectDetector .

Bagian lain yang menarik adalah .

Setelah menjalankan gambar melalui jaringan saraf, hasilnya adalah dari tipe Seq[Seq[(String, Array[Float])]] . Koleksi pertama hanya berisi satu hasil (format data ditentukan oleh jaringan saraf tertentu), maka setiap elemen koleksi berikutnya adalah tupel dari dua elemen:

  1. nama kelas ("kucing", "anjing", ...),
  2. array dari lima angka floating point: yang pertama adalah probabilitas, yang kedua adalah koefisien untuk menghitung koordinat x , yang ketiga adalah koefisien untuk menghitung koordinat y , yang keempat adalah koefisien untuk menghitung lebar persegi panjang, dan yang kelima adalah koefisien untuk menghitung ketinggian persegi panjang.

Untuk mendapatkan koordinat aktual dan ukuran persegi panjang, Anda perlu mengalikan lebar asli dan tinggi gambar dengan koefisien yang sesuai.
Saya membiarkan diri saya sedikit menyimpang tentang topik NDArray . Ini adalah array multidimensi yang dibuat MXNet dalam konteks tertentu (CPU atau GPU). Saat membuat NDArray, objek C ++ terbentuk, objek yang dengannya operasi dilakukan dengan sangat cepat (dan jika dibuat dalam konteks GPU, itu hampir seketika), tetapi Anda harus membayar untuk kecepatan tersebut. Akibatnya (setidaknya dalam versi MXNet 1.3.1) Anda perlu mengelola memori yang dialokasikan secara NDArray untuk NDArray , dan jangan lupa untuk membongkar objek-objek ini dari memori setelah Anda selesai bekerja dengannya. Jika tidak, akan ada kebocoran memori yang signifikan dan cukup cepat, yang sangat tidak nyaman untuk dipantau, karena program untuk profil JVM tidak melihatnya. Masalah memori diperburuk jika Anda bekerja dalam konteks GPU, karena kartu video tidak memiliki banyak memori dan aplikasi macet dengan cepat kehabisan memori.

Bagaimana mengatasi masalah kebocoran memori?

Dalam contoh di atas, pada model model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) , metode imageObjectDetect digunakan untuk menjalankan gambar melalui jaringan saraf, yang menerima input BufferedImage . Semua konversi ke dan dari NDArray dilakukan di dalam metode, dan Anda tidak perlu memikirkan masalah alokasi memori. Di sisi lain, sebelum mengubah BufferedImage ke NDArray dilakukan pada ukuran 512 x 512 dan gambar dinormalisasi menggunakan metode objek objek tipe BufferedImage . Ini terjadi sedikit lebih lama daripada ketika menggunakan OpenCV, misalnya, tetapi ini memecahkan masalah membebaskan memori setelah menggunakan NDArray .

Anda dapat, tentu saja, menggunakan OpenCV dan mengontrol memori sendiri, untuk ini Anda hanya perlu memanggil metode NDArray dispose , tetapi untuk beberapa alasan Anda lupa menyebutkan ini dalam dokumentasi MXNet resmi untuk Scala.

MXNet juga memiliki cara yang tidak terlalu nyaman untuk mengontrol kebocoran memori yang terjadi karena NDArray . Untuk melakukan ini, jalankan aplikasi dengan parameter JVM Dmxnet.traceLeakedObjects=true . Jika MXNet memperhatikan NDArray yang tidak digunakan tetapi tergantung pada memori, Anda akan mendapatkan pengecualian yang menunjukkan baris kode mana NDArray naas NDArray .

Saran saya: bekerja langsung dengan NDArray, hati-hati memonitor memori dan menulis normalisasi sendiri, setelah sebelumnya menentukan algoritma apa yang dilakukan insinyur ML ketika melatih jaringan saraf, jika tidak hasilnya akan sangat berbeda. ObjectDetector memiliki metode objectDetectWithNDArray mana Anda dapat melewati NDArray . Untuk menerapkan pendekatan yang lebih universal untuk memuat jaringan saraf, saya sarankan menggunakan objek org.apache.mxnet.module.Module . Di bawah ini adalah contoh penggunaan.

 import org.apache.mxnet._ import org.apache.mxnet.io.NDArrayIter //      val model: Module = { val model = Module.loadCheckpoint(modelPrefix, modelEpoch, contexts = contexts) model.bind( forTraining = false, inputsNeedGrad = false, forceRebind = false, dataShape = DataDesc(name = "data", Shape(1, 3, 512, 512), DType.Float32, Layout.NCHW)) model.initParams() model } // NDArray  1  3  512  512 val image: NDArray = ??? //  dataBatch      val iterator = new NDArrayIter(IndexedSeq(image)) val dataBatch = iterator.next() image.dispose() //   val result: Seq[Array[Float]] = model.predict(dataBatch) map { ndArray => val array = ndArray.toArray ndArray.dispose() array } dataBatch.dispose() 

6. Implementasi REST API


Kelas simple.predictor.Server bertanggung jawab untuk mengimplementasikan REST API. Server didasarkan pada server Java yang termasuk dalam Java.

simple.predictor.Server


 package simple.predictor import java.net.InetSocketAddress import com.sun.net.httpserver.{HttpExchange, HttpServer} import javax.imageio.ImageIO import org.json4s.DefaultFormats import org.json4s.native.Serialization class Server(address: InetSocketAddress, entryPoint: String, model: Model) { //   HTTP-,     java private val server = HttpServer.create(address, 0) //      URL server.createContext(entryPoint, (http: HttpExchange) => { //   HTTP-     val header = http.getRequestHeaders val (httpCode, json) = if (header.containsKey("Content-Type") && header.getFirst("Content-Type") == "image/jpeg") { //          ,      200 val image = ImageIO.read(http.getRequestBody) val predictionSeq = model.predict(image) (200, Map("prediction" -> predictionSeq)) } else (400, Map("error" -> "Invalid content")) //       400 //    JSON    val responseJson = Serialization.write(json)(DefaultFormats) val httpOs = http.getResponseBody http.getResponseHeaders.set("Content-Type", "application/json") http.sendResponseHeaders(httpCode, responseJson.length) httpOs.write(responseJson.getBytes) httpOs.close() }) def start(): Unit = server.start() def stop(): Unit = server.stop(0) } 

7. Pengujian


Untuk memeriksa, mulai server dan mengirim gambar uji src / main / resources / cat_and_dog.jpg. Kami akan mengurai JSON yang diterima dari server, memeriksa berapa banyak dan objek apa yang ditemukan jaringan saraf dalam gambar, dan melingkari objek dalam gambar.

simple.predictor.ServerTest


 package simple.predictor import java.awt.{BasicStroke, Color, Font} import java.awt.image.BufferedImage import java.io.{ByteArrayOutputStream, File} import java.net.InetSocketAddress import javax.imageio.ImageIO import org.scalatest.{FlatSpec, Matchers} import scalaj.http.Http import org.json4s.{DefaultFormats, Formats} import org.json4s.native.JsonMethods.parse import simple.predictor.Config._ import simple.predictor.Model.Prediction import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global class ServerTest extends FlatSpec with Matchers { implicit val formats: Formats = DefaultFormats "Service" should "find a cat and a dog on photo" in { //      val model = new Model(modelPrefix, modemEpoch, modemEdge, threshold, context) val server = new Server(new InetSocketAddress(host, port), entryPoint, model) //      Future(server.start()) Thread.sleep(5000) //         val image = ImageIO.read(getClass.getResourceAsStream("/cat_and_dog.jpg")) val byteOS = new ByteArrayOutputStream() ImageIO.write(image, "jpg", byteOS) val data = byteOS.toByteArray //      ,     200 val response = Http(s"http://$host:$port$entryPoint").header("Content-Type", "image/jpeg").postData(data).asString response.code shouldEqual 200 //  JSON-, ,       val prediction = parse(response.body) \\ "prediction" prediction.children.size shouldEqual 2 //     , ,     ,    val objectClassList = (prediction \\ "objectClass").children map (_.extract[String]) objectClassList.head shouldEqual "cat" objectClassList.tail.head shouldEqual "dog" //   ,   val bBoxCoordinates = prediction.children.map(_.extract[Prediction]) //   ,     val imageWithBoundaryBoxes = new BufferedImage(image.getWidth, image.getHeight, image.getType) val graph = imageWithBoundaryBoxes.createGraphics() graph.drawImage(image, 0, 0, null) graph.setColor(Color.RED) graph.setStroke(new BasicStroke(5)) graph.setFont(new Font(Font.SANS_SERIF, Font.TRUETYPE_FONT, 30)) bBoxCoordinates foreach { case Prediction(obj, prob, x, y, width, height) => graph.drawRect(x, y, width, height) graph.drawString(s"$obj, prob: $prob", x + 15, y + 30) } graph.dispose() //         ImageIO.write(imageWithBoundaryBoxes, "jpg", new File("./test.jpg")) } } 

, .



8.


, . Docker , .

Dockerfile


 #       Sbt FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-builder AS builder #       RUN mkdir /tmp/source /tmp/source/dependencies COPY project /tmp/source/project COPY src /tmp/source/src COPY build.sbt /tmp/source/build.sbt #     MXNet,       RUN ln -s /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar /tmp/source/dependencies/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar && \ cd /tmp/source/ && sbt pack #      FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #   LD   Cuda   Java ENV LD_LIBRARY_PATH /usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs:/usr/local/share/OpenCV/java #            /opt/app/models ENV MODEL_PREFIX "/opt/app/models/resnet50_ssd_model" #            RUN mkdir -p /opt/app COPY --from=builder --chown=root:root /tmp/source/target/pack /opt/app COPY models /opt/app/models #      ENTRYPOINT /opt/app/bin/simple-predictor 



 # ,    ,   Dockerfile your@pc$ docker build -f Dockerfile -t entony/simple-predictor:1.0.0 . #   docker hub your@pc$ docker push entony/simple-predictor:1.0.0 

9. production- GPU


, docker hub, Nvidia, 8080 Docker, Cuda 9.0 Cudnn 7.

 #     Docker hub your@server-with-gpu$ docker pull entony/simple-predictor:1.0.0 #     your@server-with-gpu$ docker run -d \ -p 8080:8080 \ -e MODEL_CONTEXT_GPU=true \ -e MXNET_CUDNN_AUTOTUNE_DEFAULT=0 \ --name 'simple_predictor' \ --device /dev/nvidia0:/dev/nvidia0 \ --device /dev/nvidiactl:/dev/nvidiactl \ --device /dev/nvidia-uvm:/dev/nvidia-uvm \ -v /usr/lib/x86_64-linux-gnu/libcuda.so.1:/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs/libcuda.so.1:ro \ -v /usr/lib/nvidia-396/libnvidia-fatbinaryloader.so.396.54:/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs/libnvidia-fatbinaryloader.so.396.54:ro \ entony/simple-predictor:1.0.0 

Docker- --device Cuda- -v .

MODEL_CONTEXT_GPU GPU-, MXNET_CUDNN_AUTOTUNE_DEFAULT ( , , , ).

:

 #  your@server-with-gpu$ curl -X POST -H 'Content-Type: image/jpeg' --data-binary '@src/main/resources/cat_and_dog.jpg' http://0.0.0.0:8080/predict #  { "prediction":[ { "objectClass":"cat", "probability":0.9959417, "x":72,"y":439, "width":950, "height":987 }, { "objectClass":"dog", "probability":0.81277525, "x":966, "y":100, "width":870, "height":1326 } ] } 

Kesimpulan


MXNet , - . , , , production.

, , MXNet , Python production Scala, Java ++.

, .

, . . Terima kasih atas perhatian anda

Referensi


Source: https://habr.com/ru/post/id439226/


All Articles