Slide 1

Slide 1 text

Device Plugin開発入門 Daisuke Takahashi 2020-11-30 Kubernetes Meetup Tokyo #36

Slide 2

Slide 2 text

Profile – Daisuke Takahashi ● Twitter: @yaemonsan, GitHub: @shield-9 ● MSFS2020: 500 hrs (Loves B748) ● Work at: CIU (CyberAgent Group Infrastructure Unit), CyberAgent, Inc. ○ 2019年新卒入社 Infrastructure Engineer ○ AI Div.と兼務 ● In charge of: ○ 3DCGプロダクション / ML基盤 / その他、変わり種の 物理全般 ○ OpenStack環境 / K8s as a Serviceの開発・運用 2

Slide 3

Slide 3 text

はじめに ● このスライドは https://bit.ly/k8sjp-device ○ 文字ばっかりなので、手元でも開くことをお勧めします ● コード類は https://github.com/shield-9/k8s-coral-accelerators ○ 社内のレポジトリから雑にコピペしたので、おかしな部分を見つけたら直します ● 細部の実装は、各スライド内のリンクからチェックしてみてください 3

Slide 4

Slide 4 text

Device Plugins ⇒ 色々なハードウェアリソースをKubeletに見せるための仕組み 4 Source: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/

Slide 5

Slide 5 text

Device Plugins ⇒ 色々なハードウェアリソースをKubeletに見せるための仕組み 5 Source: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/ Felica + おうちKubernetesクラスタ

Slide 6

Slide 6 text

使い方 - NVIDIA GPUの場合 1. Node上のデバイスとしての準備 ○ 各Nodeにデバイスドライバ (NVIDIA Driver) などを入れる 2. デバイスを見つけて、Kubeletに認知してもらう ○ 各NodeにDevice Pluginのコンテナを立てる kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.7.1/nvidia-device-plugin.yml ※頑張れば、privilegedなDeamonSetで完結可能 (cf. GKE) 6

Slide 7

Slide 7 text

使い方 - NVIDIA GPUの場合 3. Device-attachedなコンテナを作成 apiVersion: v1 kind: Pod metadata: name: gpu-pod spec: containers: - name: cuda-container image: nvidia/cuda:9.0-devel resources: limits: nvidia.com/gpu: 2 # requesting 2 GPUs 7 Source: https://github.com/NVIDIA/k8s-device-plugin # ls /dev/nvidia* (かなり省略してます ) /dev/nvidiactl /dev/nvidia-uvm /dev/nvidia-uvm-tools /dev/nvidia0 /dev/nvidia1 作成したコンテナ内から、 GPU 2つ分のデバイスファ イルが見えている ⇒これがDevice Pluginの働き

Slide 8

Slide 8 text

今日の話 8 使いたいハードウェアの Device Pluginがない! このDevice Pluginに、 欲しい機能がない! 対応を検討させていただきます (「やる」とは言ってない ) Device Vendor

Slide 9

Slide 9 text

今日の話 9 使いたいハードウェアの Device Pluginがない! このDevice Pluginに、 欲しい機能がない! 対応を検討させていただきます (「やる」とは言ってない ) Device Vendor こんなとき... ① Kubernetes (コンテナ) を使わない ② そのデバイスを使わない ③ デバイスが必要な処理だけKubernetesの外でやる ④ 自力でDevice Pluginを書く こんなとき... ① Kubernetes (コンテナ) を使わない ② そのデバイスを使わない ③ デバイスが必要な処理だけKubernetesの外でやる ④ 自力でDevice Pluginを書く ※①~③が有効な場合もあります

Slide 10

Slide 10 text

今回自作するDevice Plugin ● 対象: Coral Edge TPU Accelerator ○ インターフェイス: PCIe Gen2 x1 ○ Google TPUのエッジ推論版 ○ 組み込みチップなどもラインナップにあり、ドキュメントも ある程度存在 10

Slide 11

Slide 11 text

実物の様子 ● M.2 B+M key用を購入 ○ PCIe Gen3 x16スロットに4枚搭載 ■ マザボのPCIe Bifurcation対応が必須 ■ 今回はx4x4x4x4で設定 (レーンの無駄遣い) ○ 【余談】lspci -nnk でデバイス名が出ないときは update-pciids を。DBに追加しときました ● 冷却には要注意 ○ 動作制限や強制停止あり ○ モニタリング可能: https://coral.ai/docs/pcie-parameters/ 11

Slide 12

Slide 12 text

デバイスの仕様 ● Apex Driver (とGasket framework) を利用して認識 ○ Kernel 4.19で実装: drivers/staging/gasket ■ 一部ディストリビューションで不具合のあるモジュールが入っているので、とりあえずGoogle製のパッチ済 みモジュールで置き換え ○ /dev/apex_X が見えていれば正常 (Xは0からの連番) ● アプリケーションは libedgetpu1-std (と TensorFlow Lite) を通して利用 ○ 実装: google-coral/libedgetpu/driver/driver_factory.cc ■ /sys/class/apex/apex_X/ が存在すれば、/dev/apex_X にアクセス → コンテナからこれらのファイルが見えていればOK 12

Slide 13

Slide 13 text

Device Pluginの仕様 2つのgPRC serviceで構成 ● service Registration ○ PluginをKubeletに登録する ○ 登録したら役割終了 ○ (Kubelet視点だとPluginwatcher) ● service DevicePlugin ○ Device Pluginの実体 ○ 常に必要 Kubeletとの通信はUnixドメインソケットを利用 13 Source: https://github.com/kubernetes/kubernetes/issues/65772

Slide 14

Slide 14 text

service Registration (Pluginの登録) /var/lib/kubelet/plugins_registry/XXXX のソケットでListenする ● rpc GetInfo ○ ソケットを作ると、Kubeletが自動的に呼び出す ○ Pluginの登録のため、以下の情報を応答 ■ Pluginの種類とAPIバージョン (DevicePlugin or CSIPlugin) ■ 提供するリソース名 (coral.ai/edgetpu や nvidia.com/gpu など) ■ Pluginのエンドポイント (service DevicePluginのソケット) ● /var/lib/kubelet/device-plugins/XXXX だと、Kubelet再起動時に再登録される。両 service共通でも動く ● rpc NotifyRegistrationStatus ○ Pluginの登録が完了すると、 Kubeletが成否を通知する 14

Slide 15

Slide 15 text

service DevicePlugin (Pluginの実体) ● rpc ListAndWatch (stream) ○ Kubeletにデバイス一覧を逐次共有 ■ デバイスID (Plugin内で一意ならOK) ■ Healthy or Unhealthy ■ トポロジー情報 (任意) ● rpc Allocate ○ Podの作成途中で呼び出される ○ デバイスファイルのマウントのため、以下 の情報を応答 ■ ホスト上でのパス ■ コンテナ上でのパス ■ パーミッション (Cgroup) 15 ● rpc GetDevicePluginOptions ○ Pluginの登録時に呼び出される ○ 以下の2つの機能を使うかを応答 ● rpc PreStartContainer ○ Device-attachedなコンテナの開始前 に呼び出される (任意) ○ デバイスの初期化などを行う ● rpc GetPreferredAllocation ○ トポロジー情報に加えて、 NVLinkなど を考慮したい場合 (任意)

Slide 16

Slide 16 text

Edge TPU Device Pluginのファイル - cmd/coral_edgetpu/ - coral_edgetpu.go コマンドの起動・終了処理など - pkg/edgetpu/coral/ - beta_plugin.go service DevicePluginの実装 - manager.go gRPCサーバーの起動やデバイスの検出など - watcher.go service Registrationの実装 16

Slide 17

Slide 17 text

【実装】起動・終了 cmd/coral_edgetpu/coral_edgetpu.go main() func main() { cem := edgetpumanager.NewCoralEdgeTPUManager("/dev", "coral-edgetpu.sock") for { err := cem.Start() // Edge TPUの検出 (Plugin動作に必要な権限の確認 & 起動時点のデバイス一覧取得) time.Sleep(5 * time.Second) } go func() { // 特定のシグナルを受け取ったら、終了処理 sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) <-sigs cem.Stop() // gRPCサーバーの停止 & 残骸ソケットファイルを掃除 }() cem.Serve(*pluginMountPath) // Device Pluginを起動 } ※起動~コンテナの起動を追っていきます。以降、終了系は割愛 ※全体的にかなり省略しているので、完全なコードはレポジトリ参照 17 Source: https://github.com/kubernetes/kubernetes/issues/65772

Slide 18

Slide 18 text

【実装】Pluginの構造体 pkg/edgetpu/coral/manager.go type coralEdgeTPUManager struct { devDirectory string // "/dev" devices map[string]pluginapi.Device // デバイスの一覧 devicesMutex sync.Mutex // devices更新時の競合防止 socket string // ソケットのファイル名 stop chan bool // 終了処理用 wg sync.WaitGroup // 終了処理用 devicesUpdated chan bool // デバイス一覧の更新時用 grpcServer *grpc.Server registrationStatus chan watcherapi.RegistrationStatus // Kubeletへの登録状態 endpoint string // ソケットのパス } 18

Slide 19

Slide 19 text

【実装】Pluginの起動 pkg/edgetpu/coral/manager.go Serve() func (cem *coralEdgeTPUManager) Serve(pluginMountPath string) error { // 省略: 指定パスにソケットを作ってListen // 省略: gRPCサーバーを作成 watcherapi.RegisterRegistrationServer(cem.grpcServer, &watcherServiceV1{cem: cem}) // service Registrationを登録 pluginapi.RegisterDevicePluginServer(cem.grpcServer, &pluginServiceV1Beta1{cem: cem}) // service DevicePluginを登録 cem.grpcServer.Serve(lis) // gRPCサーバーを開始 kubeletCheck := time.NewTicker(5 * time.Second) // 5秒に一度ループ waitKubelet: // KubeletがPluginを登録するまで待つ for { select { case status := <-cem.registrationStatus: // Kubeletに登録されたらループを脱出 kubeletCheck.Stop() break waitKubelet case <-kubeletCheck.C: // Kubeletへの登録待ち } } cem.devicesUpdated <- true // 起動時点でのEdge TPUの一覧をKubeletに通知 (起動後は別途) 19

Slide 20

Slide 20 text

【実装】Pluginの登録 pkg/edgetpu/coral/watcher.go func (s *watcherServiceV1) GetInfo(ctx context.Context, req *watcherapi.InfoRequest) (*watcherapi.PluginInfo, error) { return &watcherapi.PluginInfo{ Type: watcherapi.DevicePlugin, Name: resourceName, Endpoint: s.cem.endpoint, SupportedVersions: []string{pluginapi.Version}, }, nil } func (s *watcherServiceV1) NotifyRegistrationStatus(ctx context.Context, status *watcherapi.RegistrationStatus) (*watcherapi.RegistrationStatusResponse, error) { if s.cem.registrationStatus != nil { s.cem.registrationStatus <- *status } if !status.PluginRegistered { glog.Error("Registration failed: ", status.Error) } return &watcherapi.RegistrationStatusResponse{}, nil } (ここはあまり面白くない…) 20

Slide 21

Slide 21 text

【実装】Pluginの起動後 pkg/edgetpu/coral/manager.go Serve() (Pluginの起動後…) edgeTPUCheck := time.NewTicker(edgeTPUCheckInterval) // Pluginの起動完了後は、Edge TPUの一覧を定期的に確認 defer edgeTPUCheck.Stop() for { select { case <-edgeTPUCheck.C: foundNew, err := cem.discoverEdgeTPUs() // Edge TPUの一覧を取得 healthChanged := cem.updateDevicesHealth() // Edge TPUの状態を更新 if foundNew > 0 || healthChanged { cem.devicesUpdated <- true // 一覧や状態に変更があればKubeletに通知 } } } return nil } 21

Slide 22

Slide 22 text

【実装】デバイスの検出 pkg/edgetpu/coral/manager.go discoverEdgeTPUs() func (cem *coralEdgeTPUManager) discoverEdgeTPUs() (int, error) { var devices []string foundNew := 0 files, err := ioutil.ReadDir(cem.devDirectory) for _, f := range files { if apexRe.MatchString(f.Name()) && f.Mode()&os.ModeSymlink == 0 { // /dev/apex_[0-9]+の形式のファイルを探す devices = append(devices, f.Name()) // apex_[0-9]+の部分を内部IDとして利用 } } for _, dev := range devices { if _, exists := cem.devices[dev]; !exists { // 新しく発見したデバイスを一覧に加える cem.setDeviceHealth(dev, pluginapi.Unhealthy) // とりあえずUnhealthy扱いで、別途状態チェック foundNew += 1 } } return foundNew, nil } 22

Slide 23

Slide 23 text

【実装】デバイス一覧の通知 pkg/edgetpu/coral/beta_plugin.go func (s *pluginServiceV1Beta1) ListAndWatch(emtpy *pluginapi.Empty, stream pluginapi.DevicePlugin_ListAndWatchServer) error { for { select { case <-s.cem.devicesUpdated: // デバイスの一覧が更新された時に発火 resp := new(pluginapi.ListAndWatchResponse) for _, dev := range s.cem.devices { resp.Devices = append(resp.Devices, &pluginapi.Device{ID: dev.ID, Health: dev.Health}) } if err := stream.Send(resp); err != nil { // Kubeletに最新のデバイス一覧を通知 s.cem.stop <- true return err } } } } 23

Slide 24

Slide 24 text

【実装】デバイスの割り当て pkg/edgetpu/coral/beta_plugin.go func (s *pluginServiceV1Beta1) Allocate(ctx context.Context, requests *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) { resps := new(pluginapi.AllocateResponse) for _, rqt := range requests.ContainerRequests { resp := new(pluginapi.ContainerAllocateResponse) for _, id := range rqt.DevicesIDs { resp.Devices = append(resp.Devices, &pluginapi.DeviceSpec{ HostPath: path.Join(s.cem.devDirectory, id), ContainerPath: path.Join(s.cem.devDirectory, id), // コンテナ上でもホストと同一パスにマウント Permissions: "mrw", // 権限フルオープン }) } resps.ContainerResponses = append(resps.ContainerResponses, resp) } return resps, nil } 24

Slide 25

Slide 25 text

動かしてみる AKE (CyberAgent社内のKaaS)でEdge TPUが動いている様子をチラ見せします vim pod.yaml kubectl exec -it coral-edgetpu-example -- bash ls /dev/apex* cd classification/ python3 classify_image.py \ --model models/mobilenet_v2_1.0_224_inat_bird_quant.tflite \ --labels models/inat_bird_labels.txt --input images/parrot.jpg # CPU版 python3 classify_image.py \ --model models/mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite \ --labels models/inat_bird_labels.txt --input images/parrot.jpg # Edge TPU版 25

Slide 26

Slide 26 text

完成! 27

Slide 27

Slide 27 text

諸注意 ● 今回はLinux系のOSに限定した話 ○ WindowsとかmacOSとかは割と事情が異なる ● 同じデバイスでも、ロットによって細かい仕様が変わりうる ○ 初回利用時の強制ファームウェア更新で ベンダーIDすら変わることも… (実話) ○ 遅くとも本番投入前には、ベンダーへ仕様確認を! ● デバイスの処理時間は必ずしも一定ではない ○ リセットや初期化などは、データシートに最大何ミリ秒か書いてあることも ○ 守らないと、「たまに動かない」システムが出来上がる … 28

Slide 28

Slide 28 text

Device Pluginのこれから@CyberAgent ● ML基盤用にNVIDIA DGX A100を導入 → ● Multi-instance GPU (MIG) ○ NVIDIA A100 GPUの新機能 ○ 1つのGPUを最大7分割できる ○ メモリ/キャッシュ/コアのHWレベルでのIsolation ■ ⇔従来のvGPU (ソフトウェアによる分割) ● 公式Device Pluginは対応済みだが、機能不足 ○ 現状: あらかじめMIGの分割をする想定 ○ 理想: リソース要求に応じて動的に再分割 ■ 「単純な再分割はフラグメント化を招くのでは…?」など、考え ることは多い 29

Slide 29

Slide 29 text

まとめ ● Device Pluginの活用で、Kubernetes上で出来ることが一気に広がる ○ Lチカ (?)、ドローン操縦、高度なネットワーキング、 HPC、センサー (Edge)、etc… ● ハードウェア仕様を把握できれば、簡単に自作が可能 ○ ドキュメントがなくても、挙動を観察すればどうにかなることもある ■ 実際、Felicaリーダーのときは、先人達が見つけた隠しコマンドに助けられた ● ぜひ皆さんのニーズに合ったデバイスを動かしてみてください 30