Model Server dengan Python Multiprocess dan SocketServer

Model server adalah server yang melayani berbagai task machine learning. Saya tertarik untuk membuat ini untuk keperluan riset tapi mungkin juga bisa jadi komersial. Tidak semua perusahaan punya sumberdaya ahli NLP dan machine learning untuk membuat model yang bagus.    Menyewa konsultan juga bukan pilihan karena khawatir data dan strategi perusahaan bocor ke luar.  Web API bisa jadi alternatif yang cocok.

Beberapa alternatif lib untuk  model server adalah  TensorServing dan Clipper.  Saya lihat kok terlalu rumit untuk model sederhana yang saya gunakan, jadi saya mau coba buat sendiri. Ternyata memang susah, hampir dua minggu saya utak-atik baru berhasil hehe. Mungkin sebaiknya memang menggunakan lib tersebut. Berikut yang saya kerjakan (mungkin banyak yang tidak efisien karena saya baru belajar, silakan koreksi kalau ada alternatif lain).

Pertama karena saya ingin membuat Keras model + web service, dan saya sudah pernah mencoba flask (posting saya tentang flask),  maka saya mengikuti langkah-langkah di https://blog.keras.io/building-a-simple-keras-deep-learning-rest-api.html.   Pada posting tersebut model disimpan dalam variabel global. Ada masalah pada lib yang saya gunakan, ternyata tidak hanya model tapi kita perlu menyimpan juga graph tensorflow-nya. Berhasil saat dicoba, tapi begitu menggunakan uwsgi+Nginx kok jadi error. Ternyata variabel global yang digunakan di Flask tidak thread safe,  jadi selalu hilang. Alternatifnya adalah meload model setiap ada request, tapi tentu ini tidak feasible karena akan lambat dan menghabiskan memory.

Selanjutnya saya baca-baca tentang thread di Python, ternyata cukup lengkap, dan saya tertarik dengan multiprocessing, karena basisnya proses bukan thread harusnya  isi variabel bisa tetap tersimpan karena benar-benar terpisah. Idenya adalah kita buat multiprocess untuk membuat proses terpisah yang menyimpan model yang nanti diakses oleh web service (flask).    Bagus juga pemisahan model server dengan web server karena nanti kalau mau dibuat scalable, kita bisa lakukan dengan replikasi docker.

[Update Apr 2018:]
Cara yang lebih mudah (tanpa buat thread, tanpa queue) adalah dengan socketserver. Letakkan load model di main lalu proses klasifikasinya di handle.  Kenapa tidak terpikir dari awal ya? [endupdate]

Bagaimana menghubungkan antar proses yang memiliki model dan proses utama? saya menggunakan queue.  Ada dua queue, masing-masing untuk input dan output. Selanjutnya bagaimana menghubungkan antara  proses utama dengan web service? saya gunakan socket. Nah kesalahan saya adalah bermain di lib socketnya langsung. Ternyata susah, misalnya saya baru tahu bahwa saat kita mengirim, pihak penerima tidak tahu kapan harus berhenti dan harus dihandle manual (protocol TCP). Berhasil sih, tapi codenya jelek. Baru saat saya cari bagaimana membuat server yang bisa melayani multi client saya melihat lib socketserver  yang jauh lebih gampang digunakan hehe.

Berikut code servernya

import socketserver
from multiprocessing import Process, Queue
import os
import json

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} input:".format(self.client_address[0]))
        print(self.data)
        dataStr = self.data.decode('utf-8')
        q_input.put(dataStr)
        msg = q_output.get()  # baca output
        self.request.sendall(str.encode(msg))

def klasifikasi(q_input, q_output):

    # ========= load model disini, hanya satu kali saat method ini diload
    print("\nload model mulai.")
    # ========= load model

    # loop forever
    while True:
        msg = q_input.get()  # blocking menunggu data masuk
        print("input masuk, isinya=" + msg)
        hasil = model.klasifikasi(msg)  # proses klasifikasi atau apapun disini
        print("proses selesai, kirim ke queue output")
        q_output.put(json.dumps(hasil))

if __name__ == "__main__":
    q_input = Queue()
    q_output = Queue()
    p = Process(target=klasifikasi, args=(q_input, q_output))
    p.daemon = True
    p.start()  # mulai server 

    HOST, PORT = "localhost", 6200

    print("Server mulai...")
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()

Selanjutnya server dapat ditest dengan command line seperti ini:

echo Golkar dan PDIP bergabung dalam pilkada 2019  | netcat localhost 6200

Atau dengan Ptyhon seperti ini, nanti tinggal dimasukkan ke dalam Flask

import socket

HOST = 'localhost'
PORT = 6200    # portnya harus sama
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Golkar dan PDIP bersatu dalam Pilkada 2019')
    data = s.recv(1024)
    print('Received', repr(data))
    s.close

Code ini belum diujicoba dengan akses bersamaan (apakah perlu ada id di dalam setiap request? bagaimana kalau pesan lebih dari 1024 byte?), jadi mungkin masih harus diperbaiki. Tapi setidaknya sudah bisa digunakan dengan flask+uswsgi+nginx.

Tinggalkan Balasan

Isikan data di bawah atau klik salah satu ikon untuk log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout /  Ubah )

Foto Google

You are commenting using your Google account. Logout /  Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout /  Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout /  Ubah )

Connecting to %s