NVIDIA Jetson TX2を買ったのでセットアップ

届きました!

f:id:dfukunaga:20170902213325p:plain

意外と大きいです。一辺20cmくらいでしょうか。 ディスプレイ(HDMI)、キーボード、マウス、電源を接続します。USBポートは一基しかないので、キーボードとマウスを両方接続する場合は USBハブを使うか、付属のMicroUSB-USB変換ケーブルを使います。

f:id:dfukunaga:20170902213757p:plain

電源ボタンを押すとUbuntu 16.04のコンソールが開きます。 まずは以下のコマンドでドライバをインストールします。 (sudo時のパスワードはnvidia

$ cd NVIDIA-INSTALLER
$ sudo ./installer.sh
$ sudo reboot

再起動するとGUIが立ち上がります。 Wi-Fiもしくは有線でネットワークの設定を行っておきます。 (後で使うので、割り振られたIPアドレスを確認しておきます。)

ここから、JetPackのインストールです。

JetPackとは、CUDAやcuDNNを始め、Jetsonを使うのに必要そうなソフトウェアをまとめたものっぽいです。 筆者はMacbook Proを使っているので、VirtualBox上のUbuntu 16.04をホストとしてJetPackをインストールすることにしました。

こちらからUbuntu 16.04のイメージをダウンロードして、 VirtualBoxVMを作ります。JetPackのインストールに最低10GBのディスクスペースが必要とのことなのですが、 ディスクサイズは余裕を持って30GBに設定しました。

(2018/05/03追記) VirtualBox経由だとセットアップがうまくいかないケースがあります。(JetsonのOSを書き換える場合?) 直接UbuntuをインストールしたPCがあれば、そちらを使うことを推奨します。

VMを起動してこちらからJetPackをダウンロードします。 NVIDIA Developer Programへの登録が必要になります。ダウンロードしたら以下のコマンドで実行。

$ chmod +x JetPack-L4T-3.1-linux-x64.run
$ ./JetPack-L4T-3.1-linux-x64.run

途中の"Component Manager"画面でFlash OS Image to TargetのActionをno actionに変更します。 各種パッケージをインストールした後、 "Device Information - Jetson TX2"画面でJetsonのIPアドレスとユーザ名、パスワード(共にnvidia)を入力します。

新しいウィンドウが開き、Jetson側にいろいろインストールされていき、 Installation of target components finished, close this window to continue.と表示されたら終わりです。 /home/nvidia/NVIDIA_CUDA-8.0_SamplesにCUDAのサンプルが入っているので、 これがうまく動けば少なくともCUDAはちゃんと入っているはずです。

ちなみに、JetsonではNVIDIAGPUでおなじみのnvidia-smiコマンドは対応していないみたいです。 代わりにホームディレクトリにあるtegrastatsGPUの状態を確認します。

nvidia@tegra-ubuntu:~$ sudo ./tegrastats 
RAM 1103/7851MB (lfb 1501x4MB) cpu [0%@1728,off,off,0%@1728,0%@1727,0%@1728] EMC 0%@1600 APE 150 GR3D 0%@114
RAM 1103/7851MB (lfb 1501x4MB) cpu [0%@345,off,off,1%@345,0%@346,5%@345] EMC 8%@40 APE 150 GR3D 0%@114
RAM 1103/7851MB (lfb 1501x4MB) cpu [1%@345,off,off,1%@345,0%@345,5%@345] EMC 8%@40 APE 150 GR3D 0%@114
RAM 1103/7851MB (lfb 1501x4MB) cpu [1%@345,off,off,2%@345,0%@345,4%@339] EMC 8%@40 APE 150 GR3D 0%@114

GR3Dの右がGPU使用率だそうです。GPUメモリはシステムと共有らしいので一番左の列を見ればOKです。

ZeroMQ: 複数のEndpointに接続する

f:id:dfukunaga:20170419234028p:plain

ちゃんとドキュメントを読めという話かもしれないが、ZeroMQ のソケットが 複数のEndpointにbind/connectできることを最近知った。そこで、複数のEndpointに対してのbind/connectを 使ったメッセージングのパターンをいくつか考えてみた。

1. ローカル/リモート共用ソケット

f:id:dfukunaga:20170418233138p:plain

同じプロセス内でのソケット間通信をするなら、inprocを使ったほうが ローカルループバックに対するtcpを使うよりも圧倒的に速い。このパターンでは、 一つのソケットに同一プロセスからの接続用のアドレス(ローカル: inproc)と、 別マシンからの接続用のアドレス(リモート: tcp)の二つをbindし、接続するソケットの位置 によってconnectするアドレスを使い分ける。これにより、ローカルのソケット間通信はより高速に行われ、また、 受信する側では、ローカルから来たメッセージとリモートから来たメッセージを区別することなく処理を行うことができる。

2. ブロードキャストクラスタ

f:id:dfukunaga:20170418233145p:plain

各ノードでZMQ_PUBZMQ_SUBを一つずつ用意します。 ZMQ_PUBはbindしてZMQ_SUBは各ノードのZMQ_PUBにconnectすると(逆でもたぶんOK)、 ZMQ_PUBにsendすることで他の全ノードにメッセージが送信されるようになります。 複数のソケットを用意すれば同じことが実現できますが、一つのソケットで複数のEndpointにconnectした方が、 recv/pollするのが一つのソケットで済むので楽になるかと思います。 このパターンを実装するときはZMQ_SUBSCRIBEを設定するのを忘れずに。

3. P2Pクラスタ

f:id:dfukunaga:20170418233158p:plain

bindとconnectを併用することもできます。このパターンでは、各ノードにZMQ_ROUTERを一つ用意し、 一つのアドレスに対してbindするのと同時に、他のノードのZMQ_ROUTERにconnectします。 このように繋ぐことで、ZMQ_ROUTERのルーティング機能を使えばクラスタ内のどのノードへも メッセージを送信することができます。ZMQ_ROUTER同士を繋ぐときはidentityの設定が厄介ですが、 bindする前にZMQ_IDENTITYを設定し、connectする際にZMQ_CONNECT_RIDを設定しておけば問題ないでしょう。

以上、複数のEndpointへのbind/connectを用いたメッセージングパターンでした。

通信時のソースアドレスを取得する

f:id:dfukunaga:20170417005216p:plain

Linuxです。

なんて言えばよいのかわからなかったのですが、ip route get xxx.xxx.xxx.xxxと打ったときに出てくる ソース(送信元)アドレスをC/C++内で取得したかった。すぐに思いついた方法は、

  • popenを使ってip route getを実行し、標準出力を取得
  • 一時的にソケットを作って送信元IPアドレスを取得

です。以下のようなサンプルコードを書きました。

get route source address

どちらも同じ出力が得られますが、手元の環境で試したところ、 1000回実行するのにかかる時間がpopenを使った方法では約1秒、 ソケットを使った方法では約0.005秒だった。(コードの改善点はあるかもしれませんが。)

他にもっと良い方法があれば教えてください!

ソケットに結び付けられたアドレスを取得する

f:id:dfukunaga:20170412233423p:plain

Linuxです。

netstatしたときのLocal AddressとForeign AddressにあたるものをC/C++で取得したかった。 Local Addressはgetsockname()、Foreign Addressはgetpeername()を使えば取得できるらしい。 C++でサンプルを書くと以下のような感じ。

gist6c0628fe0ad9790fe7a1fbc5cba9b13c

vagrant@vagrant-ubuntu-trusty-64:~$ ./a.out 192.168.0.2 2551
Local Address:   192.168.0.1:46402
Foreign Address: 192.168.0.2:2551

ちゃんと取得できてますね!(↑のサンプルコードを使うときは対応するアドレスをlistenしてください)

ZeroMQのheartbeatを試してみた

ZeroMQのversion 4.2からheartbeat機能が追加されました。 元々コネクションの切断は検知してzmq_socket_monitor(3)で通知してくれたのですが、 TCPエラーを吐かずにコネクションが死んだり、Out of Memory等でプロセスが止まったりした場合は検知できないので heartbeat機能が追加されたようです。(参考:ZeroMQのRFC

heartbeatに関するソケットのオプションは以下の3つです。

  • ZMQ_HEARTBEAT_IVL
  • ZMQ_HEARTBEAT_TIMEOUT
  • ZMQ_HEARTBEAT_TTL

ZMQ_HEARTBEAT_IVLミリ秒の間隔でpingを送り、ZMQ_HEARTBEAT_TIMEOUTミリ秒経っても pongが返ってこなかったらコネクションが死んだとみなすという感じです。また、pongを返す側では ZMQ_HEARTBEAT_TTLミリ秒経っても次のpingが来なかったらコネクションが死んだとみなします。 ZMQ_HEARTBEAT_IVLを設定することで初めてheartbeatが動作します。

試しに以下のような設定をしてみます。基本的にどちらか片方(connect側もしくはbind側)のソケットにだけ設定しておけばOKだと思います。

int interval = 100, timeout = 1000, ttl = 1000;
zmq_setsockopt(sock, ZMQ_HEARTBEAT_IVL, &interval, sizeof(int));
zmq_setsockopt(sock, ZMQ_HEARTBEAT_TIMEOUT, &timeout, sizeof(int));
zmq_setsockopt(sock, ZMQ_HEARTBEAT_TTL, &ttl, sizeof(int));
zmq_setsockopt(sock, ZMQ_HANDSHAKE_IVL, &timeout, sizeof(int));

ZMQ_HEARTBEAT_TTLZMQ_HEARTBEAT_IVLよりも十分大きい値にしておく必要があります。 そうしないと接続が頻繁に切れてしまいます。ZMQ_HEARTBEAT_TIMEOUTも短すぎると 偽陽性が増えてしまうので注意です。

また、ZMQ_HANDSHAKE_IVLを設定しています。ZeroMQではソケットが接続した後に ソケットの設定情報を交換する(これをhandshakeと呼ぶ)そうなのですが、止まっているソケットに再接続した際に ZMQ_HEARTBEAT_TIMEOUTよりもZMQ_HANDSHAKE_IVLが優先されるため、両方共同じ値にしておきます。 ZMQ_HANDSHAKE_IVLのデフォルト値は30秒なので、これを設定しないと ZMQ_HEARTBEAT_TIMEOUTをどれだけ短くしても再接続時に30秒待ってしまいます。

実際にこんな感じのコードを書いて動かします。 bind/connectが完了した時点で、connect側のプロセスにSIGTSTPを送るとbind側のzmq_socket_monitor(3)から ZMQ_EVENT_DISCONNECTEDが通知されます。逆に、bind側のプロセスにSIGTSTPを送ると、connect側の zmq_socket_monitor(3)からZMQ_EVENT_DISCONNECTEDが通知された後に、再接続を試みます。 bind側のlistenが残っているからか、再接続は成功しますが、handshakeが失敗して再びZMQ_EVENT_DISCONNECTEDが 通知されるというような挙動になります。

また、heartbeatを有効にしても前回の調査同様、unbind/disconnect時には 相手側のソケットには何も通知されません。disconnectしても再接続しようとするし、unbind/disconnectとは何なのだろう…。 とりあえずソケットを切断するときは必ずcloseした方が良さそうです。

※ZeroMQ v4.2.1を使用

ZeroMQのzmq_socket_monitor(3)の挙動を調べた

ZeroMQのzmq_socket_monitor(3)の挙動を調べてみました。 bindするソケットとconnectするソケットを用意し、以下の順に処理して zmq_socket_monitor(3)でどのようなイベントが通知されるか確認しました。

  1. create (bind/connect socket)
  2. bind
  3. connect
  4. disconnect
  5. close (connect socket)
  6. create (connect socket)
  7. connect
  8. unbind
  9. close (bind socket)

上記の処理を1秒間隔で行い、イベントの出力を確認します。 使用したソースコードこちらに載せました。

bind側の出力

vagrant@vagrant-ubuntu-trusty-64:~$ ./a.out bind tcp://0.0.0.0:2551
[0.000] [main] Create...
[1.000] [main] Bind...
[1.010] [event] number: 8, value: 12, address: tcp://0.0.0.0:2551
[2.011] [event] number: 32, value: 13, address: tcp://0.0.0.0:2551
[4.014] [event] number: 512, value: 13, address: tcp://0.0.0.0:2551
[6.019] [event] number: 32, value: 13, address: tcp://0.0.0.0:2551
[7.001] [main] Unbind...
[7.009] [event] number: 128, value: 12, address: tcp://0.0.0.0:2551
[8.003] [main] Close...
[8.004] [event] number: 1024, value: 0, address:
[9.004] [main] Finish...

connect側の出力

vagrant@vagrant-ubuntu-trusty-64:~$ ./a.out connect tcp://192.168.0.1:2551
[0.000] [main] Create...
[2.000] [main] Connect...
[2.003] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[2.004] [event] number: 1, value: 12, address: tcp://192.168.0.1:2551
[3.002] [main] Disconnect...
[4.005] [main] Close...
[4.006] [event] number: 1024, value: 0, address:
[5.012] [main] Create...
[6.014] [main] Connect...
[6.015] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[6.016] [event] number: 1, value: 11, address: tcp://192.168.0.1:2551
[8.013] [event] number: 512, value: 11, address: tcp://192.168.0.1:2551
[8.014] [event] number: 4, value: 183, address: tcp://192.168.0.1:2551
[8.196] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.197] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.197] [event] number: 4, value: 125, address: tcp://192.168.0.1:2551
[8.316] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.317] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.317] [event] number: 4, value: 141, address: tcp://192.168.0.1:2551
[8.466] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.466] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.467] [event] number: 4, value: 186, address: tcp://192.168.0.1:2551
[8.652] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.653] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.654] [event] number: 4, value: 197, address: tcp://192.168.0.1:2551
[8.848] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.849] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.849] [event] number: 4, value: 110, address: tcp://192.168.0.1:2551
[8.957] [event] number: 2, value: 115, address: tcp://192.168.0.1:2551
[8.958] [event] number: 128, value: 11, address: tcp://192.168.0.1:2551
[8.959] [event] number: 4, value: 138, address: tcp://192.168.0.1:2551
[9.015] [main] Close...
[9.015] [main] Finish...

一番左側の列は経過時間を表しています。 numberはZeroMQで定義されているイベント番号で、以下のようになっています。

/*  Socket transport events (tcp and ipc only)                                */
#define ZMQ_EVENT_CONNECTED 1
#define ZMQ_EVENT_CONNECT_DELAYED 2
#define ZMQ_EVENT_CONNECT_RETRIED 4

#define ZMQ_EVENT_LISTENING 8
#define ZMQ_EVENT_BIND_FAILED 16

#define ZMQ_EVENT_ACCEPTED 32
#define ZMQ_EVENT_ACCEPT_FAILED 64

#define ZMQ_EVENT_CLOSED 128
#define ZMQ_EVENT_CLOSE_FAILED 256
#define ZMQ_EVENT_DISCONNECTED 512
#define ZMQ_EVENT_MONITOR_STOPPED 1024

この結果からわかることは、

  • bindした直後にbind側でZMQ_EVENT_LISTENINGが通知される
  • connectした直後にbind側でZMQ_EVENT_ACCEPTED、connect側でZMQ_EVENT_CONNECT_DELAYEDZMQ_EVENT_CONNECTEDが通知される
  • disconnectしてもbind側、connect側共に何も通知されない
  • unbindするとbind側にはZMQ_EVENT_CLOSEDが通知されるが、connect側には何も通知されない
  • connect側をcloseするとbind側にZMQ_EVENT_DISCONNECTEDが通知される
  • bind側をcloseするとconnect側にZMQ_EVENT_DISCONNECTEDが通知され、connect側は再接続を試みる
  • connect側が再接続を試みている間は、ZMQ_EVENT_CLOSEDZMQ_EVENT_CONNECT_RETRIEDZMQ_EVENT_CONNECT_DELAYEDが通知され続ける
  • ソケットをcloseすると、ZMQ_EVENT_MONITOR_STOPPEDが通知される

ちなみに、プロセスを強制的に落とした場合はcloseしたときと同じ挙動になり、 ZMQ_EVENT_DISCONNECTEDが通知されます。

今回は通知するイベントをZMQ_EVENT_ALLにしましたが、 実際に使うときには適切なイベントを選択して通知した方が良いかと思います。

※ZeroMQ v4.2.1を使用