この記事は、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列を通信するようにします。
ソースコード
socketについて書かれた書籍は、あまりないので、ネットの技術ブログ等を読んで概要を掴んで、正確な情報は、Linux Programmer's Manual(翻訳)などを読んで理解するのが普通なのかなと思います。
参考記事・資料
#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;
}
#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公式(ソースからのビルド)
- mnnistのC++のサンプル(JackyTung/tensorgraph)
- saito's memo: Pythonで学習したネットワークをC++で実行する
※ Tensorflowでは初回の実行でメモリー確保等が行われ初回の動作が遅いので初回だけ空実行することで、それ以降は待ち時間なしで実行できるようになります。
おまけ(signal)
プログラムを終了させたいときには、ctrl+C
でプログラムを終了すると思いますが、それはsignalというOSの機能つかっています。
ctrl+Z
でプログラムを停止することもできます。
プログラムを起動してモデル読み込みなど初期化が終わった時点で、停止シグナルを自身に送ることで、停止します。
外部プログラムorコマンドからsignalを送ることで、プログラムを再開して待ち時間なしで実行することができます。
非常にprimitiveな方法ですが、デモ用等で非常に簡易で良いときにはことで良いこともあります。
通信によって直接データを渡すことができないですが、たとえば、カメラから直接読み込み実行することや、ファイル読み込みで簡易に済ますことが考えられます。
#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;
}
#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のインターフェースと類似しているようなので、慣れている人から便利と聞きました。