9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

組み込みDL向けのプロセス間通信

Posted at

この記事は、LeapMind Advent Calendar 2019の20日目の記事です。

要約

C言語でプロセス間通信にとしてsocketとsignalを使い、初期化を事前に行うことで、Deep Learningのモデルの結果を得るまでの短縮する方法について書きます。

自己紹介

私は普段はPythonメインのDeep Learning エンジニアです。今回はC言語を使います。組み込みDLの開発を行うLeapMindではC/C++を使うエンジニアは多いですが、私自身はC/C++を使った開発はほとんどないです。習熟度は高くないので、間違いなどありましたら、ご指摘いただきたいです!

※ 私自身はC/C++を使うことはほとんどないですが、LeapMindでは、組み込み・エッジコンピューティングの開発も行っています。

目的

組み込みDL向けのプロセス間通信というタイトルにしましたが、ただのプロセス間通信するだけです!
本当はC/C++でmnistを動かそうと思いましたが、分量が多くなるのでinference部分はダミーにしています(後述)。

さて、製造業などの組み込み機器だと数十msecなどの短い時間で、処理を行いたいことがあります。
小さい入力であれば、DLのモデルであっても数十msecで実行できることがありますが、
単純にTensorFlowで作られたDLのモデルの読み込み処理を行うと、起動までで数秒単位でかかってしまうことがあります。

そういう場合には、事前のサービスとして立ち上げておき、リクエストを送るようにすることで、
待ち時間をなくすことができます。

実際には、DLでよく使われるPythonにも当然socketモジュールは存在しますし、よりリッチなことができるライブラリーが
多数存在しますが、組み込みに近い環境では、Pythonをイントールすることはできない or したくないということがあります。
そのため、C/C++などでの開発を求められることがあります。

※ 今回はマシン内での通信のみを想定しています。外部と通信する場合には、セキュリティーのこともしっかり考える必要があります。

環境

今回使う環境は、以下の環境にしました。

  • Ubuntu==16.04
  • gcc==5.4.0

※ 別のディストリビューションやMacでもそのまま動くはずですし、winsock2を使えばWindowsでも同じことができます。

手段

プロセス間通信を行う方法は複数あります。
今回は、socket通信を使ったプロセス間通信にします。
また、おまけで、signalを使った簡易な方法を書きたいと思います。

処理の流れ

下図のようなclient、serverの通信処理になります。
inferenceでは3x3の行列を入力し、列ごとに和をとるに計算します。
3x3の行列(9個のfloat列)をclientより送信し、3個のfloat列を通信するようにします。

Untitled Diagram.png

ソースコード

socketについて書かれた書籍は、あまりないので、ネットの技術ブログ等を読んで概要を掴んで、正確な情報は、Linux Programmer's Manual(翻訳)などを読んで理解するのが普通なのかなと思います。

参考記事・資料

server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT 52579
#define SIZE 3


void inference(float* input, float* output)
{
    for (size_t i=0; i<SIZE; ++i)
    {
        for (size_t j=0; j<SIZE; ++j)
        {
            output[i] += input[SIZE*i + j];
        }
    }
}


int main(void)
{
    // create socket
    int sock;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket");
        return 1;
    }

    // struct about the connection destination
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    // bind
    if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) != 0)
    {
        perror("bind");
        return 1;
    }

    // listen
    int backlog = 1;
    if (listen(sock, backlog) != 0)
    {
        perror("listen");
        return 1;
    } else
    {
        printf("listen\n");
    }

    while(1)
    {
        int status;

        // accept socket
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int connected_socks = accept(sock, (struct sockaddr *)&client, &len);

        if (connected_socks == -1)
        {
            perror("accept");
        } else
        {
            printf("accepted\n");
        }

        // recieve
        float input[SIZE*SIZE] = {};
        status = recv(connected_socks, input, sizeof(input), 0);

        if (status == -1)
        {
            perror("recv");
        } else
        {
            printf("received\n");
        }

        // print recieved data
        printf("[ ");
        for (size_t i=0; i<SIZE; ++i)
        {
            for (size_t j=0; j<SIZE; ++j)
            {
                printf("%f ", input[SIZE*i + j]);
            }
        }
        printf("]\n");

        // inference
        float output[SIZE] = {};
        inference(input, output);

        // send
        status = send(connected_socks, output, sizeof(output), 0);
        
        if (status == -1)
        {
            perror("send");
        } else 
        {
            printf("send\n");
        }

        close(connected_socks);
    }

    close(sock);
    return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 52579
#define SIZE 3


int main(void)
{
    int status;

    // create socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    // struct about the connection destination
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = inet_addr("127.0.0.1");

    // connect server
    status = connect(sock, (struct sockaddr *)&server, sizeof(server));
    if (status == -1)
    {
        perror("connect");
    } else {
        printf("connected\n");
    }

    // input
    float input[SIZE*SIZE] = {
        0.f, 1.f, 2.f,
        3.f, 4.f, 5.f,
        6.f, 7.f, 8.f
    };

    // send
    status = send(sock, input, sizeof(input), 0);
    if (status == -1)
    {
        perror("send");
    } else {
        printf("send\n");
    }

    // recieve
    float output[SIZE] = {};
    status = recv(sock, output, sizeof(output), 0);
    if (status == -1)
    {
        perror("recv");
    } else {
        printf("received\n");
    }

    // print received data
    printf("[ ");
    for(size_t i=0; i<SIZE; ++i)
    {
        printf("%f ", output[i]);
    }
    printf("]\n");

    // close socket
    close(sock);

    return 0;
}

実行方法

コンパイル


gcc -o server server.c
gcc -o client client.c

実行


$ ./server
listen
accepted
received
[ 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 ]
send
./client
connected
send
received
[ 3.000000 12.000000 21.000000 ]

C/C++によるDL

TensorFlowにはC/C++ APIがあります。少し癖がありますが、ドキュメントを読めば比較的簡単に使うことができるはずです(コンパイルするのがちょっと大変だけど...)。こちらを紹介だけでもかなりの分量になります。記事や参考となるソースコードは多数存在するので、それらを参考にいただければと思います。

※ Tensorflowでは初回の実行でメモリー確保等が行われ初回の動作が遅いので初回だけ空実行することで、それ以降は待ち時間なしで実行できるようになります。

おまけ(signal)

プログラムを終了させたいときには、ctrl+Cでプログラムを終了すると思いますが、それはsignalというOSの機能つかっています。
ctrl+Zでプログラムを停止することもできます。

プログラムを起動してモデル読み込みなど初期化が終わった時点で、停止シグナルを自身に送ることで、停止します。
外部プログラムorコマンドからsignalを送ることで、プログラムを再開して待ち時間なしで実行することができます。
非常にprimitiveな方法ですが、デモ用等で非常に簡易で良いときにはことで良いこともあります。

通信によって直接データを渡すことができないですが、たとえば、カメラから直接読み込み実行することや、ファイル読み込みで簡易に済ますことが考えられます。

run_by_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


int main(void)
{
    printf("Initialize...\n");
    // run some initialization here
    sleep(1);

    pid_t c_pid = getpid();
    printf("Send SIGCONT to PID: %d to run\n", c_pid);

    while(1)
    {
        raise(SIGTSTP);
        printf("run \n");
    }

    return 0;
}
send_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>


int main(int argc, char *argv[]) {
    printf("send continue signal to PID:%s\n", argv[1]);
    kill(atoi(argv[1]), SIGCONT);
}

実行方法

コンパイル


gcc -o run_by_signal run_by_signal.c
gcc -o send_signal send_signal.c

実行

run_by_signalを実行すると、初期化を行いすぐに停止する。
その後send_signalを実行することで、run_by_signalが再開され、処理が実行され(ここではrunを出力するだけ)、またすぐに停止する。

send_signalを実行するときには、run_by_signalのプロセスIDを指定する。

$ ./run_by_signal
Initialize...
Send SIGCONT to PID: 17088 to run

[1]+  Stopped                 ./run_by_signal
$ ./send_signal 17088
send continue signal to PID:17088
run

[1]+  Stopped                 ./run_by_signal

killコマンドを使って、send_signalと同じこともできます。

こちらも最終的にsignalも正確な情報をLinux Programmer's Manual(翻訳)などを読んで理解するのが普通なのかなと思います。

まとめ・感想

「Pythonばっかりなので、たまにはC言語を使いたいな。」と思い書いてみました。
プロセス間通信にとしてsocketとsignalを使い、初期化を事前に行うことで、Deep Learningのモデルの結果を得るまでの短縮する方法について書きました。

TensorflowをC++ APIを使ったことはありますが、CのAPIは無いので試してみたいです。
またPytorchにもC++ APIがあるようなので、こちらも近いうちに試してみたいとおもいます。
Pythonのインターフェースと類似しているようなので、慣れている人から便利と聞きました。

参考文献一覧

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?