ぽよメモ

レガシーシステム考古学専攻

macOS Catalinaでavrdudeを使ってProMicroに書き込もうとするとprogrammer is not respondingというエラーが出る

はじめに

 これは2019/11/4現在の情報です.macOS10.15よりも新しいバージョンでは当てはまらない可能性があります.何か更新があれば,コメントを頂けると幸いです.

[2019/12/12 追記]
この問題はmacOS Catalina 10.15.2で解消されました.
以下の内容はmacOS Catalina 10.15~10.15.1までのみで起きる問題です.回避するためにはmacOS 10.15.2へのアップデートを行なってください.

何が起きた?

 QMK Firmwareを知らない自作キーボード界隈は居ないと思いますが,ファームウェア

  1. Docker環境でビルド→avrdudeを直接叩いて書き込み
  2. make キーボード名:キーマップ名:avrdudeでビルド & 書き込み

のどちらの方法でも,ProMicroに大して書き込みが出来なくなってしまいました.具体的には下記の様なエラーが出ます(Docker環境でビルドしたhexファイルを書き込もうとしていました).

$ sudo avrdude -p atmega32u4 -c avr109 -P /dev/tty.usbmodem142101  -U flash:w:.build/choco60_pudding.hex

Connecting to programmer: .avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding

avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: ser_drain(): read error: Device not configured
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
Found programmer: Id = ""; type =
    Software Version = .; Hardware Version = .
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: error: buffered memory access not supported. Maybe it isn't
a butterfly/AVR109 but a AVR910 device?
avrdude: initialization failed, rc=-1
         Double check connections and try again, or use -F to override
         this check.

avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: error: programmer did not respond to command: leave prog mode
avrdude: ser_send(): write error: Device not configured
avrdude: ser_recv(): read error: Device not configured
avrdude: butterfly_recv(): programmer is not responding
avrdude: error: programmer did not respond to command: exit bootloader
avrdude: ser_close(): can't reset attributes for device: Device not configured

avrdude done.  Thank you.

最初はリセットスイッチの押し忘れ・タイミング外れなど色々と検討しましたが,最終的に下記のIssueにたどり着き,Catalinaの問題であることが分かりました.

github.com

純粋にOS側での問題があるらしく,QMK Firmwareやavrdude側でどうにかすることが難しい事が伝わってきます.が,コメントでVirtualBox経由だと上手くいったといった内容が見受けられるので,じゃあVagrantでビルド環境ごと隔離すれば良いじゃんと思い,試してみました.

環境

以下の様な環境で試しています.

手順

1. QMK Firmwareをクローン

 forkして使っている人や,既にローカルに持っている人はそれぞれ自分の環境に置き換えてください.最終的にローカルにリポジトリがあり,そのパスが分かれば良いです.ここでは適当にユーザのホームディレクトリ以下にcloneしたとして以降の手順を行います.

$ pwd
/Users/ユーザ名
$ git clone --recursive https://github.com/qmk/qmk_firmware

2. VagrantVirtualBoxのインストール

 Homebrew Caskでインストールします.Extension Packとバージョンを合わせる必要があります.もしVirtualBoxだけ先にインストールしており,Extension Packのインストールに失敗する場合,先にbrew cask upgrade virtualboxする必要があるかも知れません.

$ brew cask install \
    vagrant \
    virtualbox \
    virtualbox-extension-pack

Guest Additionsの自動更新プラグインをインストールします.

$ vagrant plugin install vagrant-vbguest

僕はこの辺りで一度再起動を挟みました.

3. ProMicroデバイスの情報を確認

 USBケーブルでProMicro(を実装した自作キーボード)とMacを接続します.VirtualBoxではUSBデバイスを仮想環境から扱うために,パススルーを許可するデバイスホワイトリスト方式で許可する必要があります.そのためにプロダクトIDやベンダーIDを控える必要があります.VboxManageコマンドを使って確認します.

$ VBoxManage list usbhost
Host USB Devices:

UUID:               b8bdff28-b707-43f1-8340-51e9943f74f7
VendorId:           0x2341 (2341)
ProductId:          0x8037 (8037)
Revision:           1.0 (0100)
Port:               1
USB version/speed:  0/Full
Manufacturer:       Arduino LLC
Product:            Arduino Micro
Address:            p=0x8037;v=0x2341;s=0x000008828f96a793;l=0x14210000
Current State:      Busy

# 以下略

VendorIdとProductIdをメモります.次に,リセットスイッチを押して書き込み可能状態にします.このとき,ProductIdが変化するので,これもまたメモります.

$ VBoxManage list usbhost
Host USB Devices:

UUID:               b8bdff28-b707-43f1-8340-51e9943f74f7
VendorId:           0x2341 (2341)
ProductId:          0x0037 (0037)
Revision:           0.1 (0001)
Port:               1
USB version/speed:  0/Full
Manufacturer:       Arduino LLC
Product:            Arduino Micro
Address:            p=0x0037;v=0x2341;s=0x000008828f96a793;l=0x14210000
Current State:      Busy

# 以下略

これを次の手順でVagrantfileに記述します.

4. Vagrantfileを用意

適当なディレクトリ(ここでは$HOME/qmk_vagrantとする)に,以下の内容をVagrantfileとして配置します.

# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|

  config.vm.box = "hashicorp/bionic64"
    
  config.vm.provider "virtualbox" do |v|
    # USBデバイスのパススルーを許可
    v.name = "VM to flash firmware into ProMicro"
    v.customize ["modifyvm", :id, "--usb", "on"]
    v.customize ["modifyvm", :id, "--usbehci", "on"]
    # 以下にパススルーを許可するデバイスの情報(vendoridとproductid)を書く
    # nameは単に識別のためなので特に気にしないで良い
    v.customize ["usbfilter", "add", "0",
                 "--target", :id,
                 "--name", "Arduino Micro (writable)",
                 "--vendorid", "2341",
                 "--productid", "0037",
                 "--remote", "no"]
    v.customize ["usbfilter", "add", "1",
                 "--target", :id,
                 "--name", "Arduino Micro",
                 "--vendorid", "2341",
                 "--productid", "8037",
                 "--remote", "no"]
  end

  # ここを環境に合わせて書き換える
  config.vm.synced_folder "/Users/ユーザ名/qmk_firmware", "/qmk_firmware"
  config.vbguest.auto_update = true

  # avrdudeとQMK Firmwareのビルドツール一式を揃える
  config.vm.provision "shell", privileged: false, inline: <<-SHELL 
    sudo apt-get update
    sudo apt-get install -y python3-pip avrdude
    /qmk_firmware/util/qmk_install.sh
   SHELL
end

5. ProMicroをMacから抜く

 USBケーブルで繋いでいたProMicroを抜きます.その方が安定しました.

 おそらくmacOS側でビジーだと,VirtualBoxのゲストOS内にうまくマウントできないのだと思います.

6. vagrant upで仮想環境を起動

 前の手順で用意したVagrantfileのあるディレクトリで以下のコマンドを実行します.

$ vagrant up

これで無事仮想環境が起動するのを待ちます.色々なインストールを含むので,多少時間がかかります.起動したらsshして仮想環境へ接続します.

$ vagrant ssh
# USBデバイスを確認
(guest) $ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

7. 再びUSBで接続し,書き込む

 再びUSBケーブルでMacとProMicroを接続します.以降の手順ではリセットスイッチを押す度にゲストOSが一瞬止まったように感じます.操作は反映されるので,焦って適当にコマンドを打たないようにしましょう.

 リセットスイッチを押して書き込み可能状態にしてlsusbしてみます.

(guest) $ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 003: ID 2341:0037 Arduino SA
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

/dev/ttyACM0が見えれば成功です.割とすぐ消えてしまうので,見えなくても焦らず再びリセットスイッチを押しましょう.

(guest) $ ls -l /dev/ttyACM0
ls: cannot access '/dev/ttyACM0': No such file or directory
# 1,2回見つからなくても何度か押せば見つかる
(guest) $ ls -l /dev/ttyACM0
crw-rw-rw- 1 root dialout 166, 0 Nov  3 15:30 /dev/ttyACM0

ここまで見えていればほぼ勝利です.ここから先は自分でavrdudeで書き込むのも,makeに任せるのもなんとかなります.

# /qmk_firmwareに移動
(guest) $ cd /qmk_firmware
# ファームウェアをビルドして書き込み
(guest) $ sudo make choco60:default:avrdude
QMK Firmware 0.7.54
Making choco60 with keymap default and target avrdude

avr-gcc (GCC) 5.4.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Size before:
   text    data     bss     dec     hex filename
      0   16012       0   16012    3e8c .build/choco60_default.hex

Copying choco60_default.hex to qmk_firmware folder                                                  [OK]
Checking file size of choco60_default.hex                                                           [OK]
 * The firmware size is fine - 16012/28672 (55%, 12660 bytes free)
Detecting USB port, reset your controller now...................
# ここでリセットスイッチを押してしばらく待つ
Device /dev/ttyACM0 has appeared; assuming it is the controller.
Waiting for /dev/ttyACM0 to become writable.

Connecting to programmer: .
Found programmer: Id = "CATERIN"; type = S
    Software Version = 1.0; No Hardware Version given.
Programmer supports auto addr increment.
Programmer supports buffered memory access with buffersize=128 bytes.

Programmer supports the following devices:
    Device code: 0x44

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9587 (probably m32u4)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file ".build/choco60_default.hex"
avrdude: input file .build/choco60_default.hex auto detected as Intel Hex
avrdude: writing flash (16012 bytes):

Writing | ################################################## | 100% 1.97s

avrdude: 16012 bytes of flash written
avrdude: verifying flash memory against .build/choco60_default.hex:
avrdude: load data flash data from input file .build/choco60_default.hex:
avrdude: input file .build/choco60_default.hex auto detected as Intel Hex
avrdude: input file .build/choco60_default.hex contains 16012 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 1.13s

avrdude: verifying ...
avrdude: 16012 bytes of flash verified

avrdude: safemode: Fuses OK (E:FB, H:D8, L:FF)

avrdude done.  Thank you.

手動でavrdudeコマンドを使って書き込む場合,以下の様にします.

$ sudo avrdude -p atmega32u4 -c avr109 -P /dev/ttyACM0 -U flash:w:hexファイルへのパス

直接 /dev/ttyACM0を指定するのですが,見つからない場合以下の様にエラーが出ます.

$ sudo avrdude -p atmega32u4 -c avr109 -P /dev/ttyACM0 -U flash:w:.build/choco60_pudding.hex
avrdude: ser_open(): can't open device "/dev/ttyACM0": No such file or directory

avrdude done.  Thank you.

リセットスイッチを押してからこのデバイスファイルが出現するまでラグがあるので,落ち着いて数回実行してください.

うまくいかないとき

 最初全くうまくいかなかったのですが,あるとき急に上手くいくようになったので正直これはかなり再現性の低い手順なのではないかと思っています.ただいくつかポイントはあるかなと思っています.

  1. macOS側でProMicroに触らない(ビジー状態にしない)
  2. vagrant upする前に一旦USB接続を切断し,ゲストOS起動後に接続し直す
  3. リセットスイッチを押してからは失敗しても2,3回やり直す
  4. とりあえず再起動する

正直macOS Catalinaにアップデートしない or WindowsLinux等の他のOSを使用する方が絶対良いと思います(真顔).

まとめ

この記事の手順は丸1日の試行錯誤を圧縮・整理した内容になっています.足りていない手順や余計な手順を含む可能性があることに留意してください.

 早く修正されることを祈っています.それでは良きキーボードライフを.

GoCon 2019 Autumnに参加してきた

はじめに

 今回はGo Conference 2019 Autumnへ参加するにあたり,Wantedly株式会社様よりスカラシップによる支援を頂きました.大変感謝しております.また,運営の方々,大変楽しくためになるカンファレンスでした.次回以降も是非とも参加したいと思っております.本当にありがとうございました.
 なお,本記事では自分が面白いと思って聞いたセッションについて掲載しています.全体の総括ではありませんのでご了承ください.

初めて参加してみて

 Go Conの存在については以前から知っており面白そうだと感じては居ましたが,開催地が遠かったのもあり,今回が初参加となりました.
 これまでのことは存じ上げませんが,今回の会場はどこも登壇者と聴衆の距離が近く,一体となって盛り上げていくような感覚がありました.僕がスカラシップ用のバッジを身につけていたのもあるとは思うのですが,話したことない方からも「学生の方ですか?」とかお声かけ頂いたりして,大変ありがたかったです.

f:id:pudding_info:20191028234912j:plain
スカラシップ参加者用の緑色の園児ニアの証,tofu_on_fire

 セッションはどれも聞き応えがありましたが,ただの全体のまとめをしがない学生が書いても意味は無さそうです.そこで,今回は僕自身が聞いてこれから活かしていけそうだなと思った話や,純粋に興味があり面白かった話などに絞っていくつかピックアップしたいと思います.一応万全は期していますが,内容等に間違いがあればこっそり指摘して頂けると修正します.

面白かったセッション

全て自分で聞きに行ったセッションです.資料のURLはTwitter等で知ったものについては載せています.

1. Go GC algorithm 101

gocon.jp


 まずは最初のセッションである,Wantedlyの20卒内定者taxioくんの発表です.説明が非常に分かりやすく,知見として有益でした.一時期Pythonのプログラムのメモリリークに悩んでいたこともあって,自分でGCについて多少調べたりしていたのですが,入門として非常に優秀なスライドだと思います*1

 特に,GCというものは文章で書かれてもイマイチ理解出来ない部分があり,視覚的に図で示されるとなんとなく理解しやすいように感じます.まだまだ奥深い話が無限にある世界だと思うので,彼がもっと詳しくブログを書いてくれることを期待しています.

発表後に上がった質問

 さて,本Sessionの内容はもうスライドとしてまとまっており,これ以上僕が言うことは何もないので,会場で出た質問とそれに対する発表者による回答をざっと上げていきます.

  • Q: 世代別GCが美味しいケースはどこ?Goでは通常スタックに確保されるはずで,ヒープに短命なオブジェクトが漏れてくるケースは珍しいのでは?
    A: Rick氏の発表*2をもう少し見る必要がある
  • Q: 短命なオブジェクトでヒープに来る物はどんなもの?
    A: エスケープ解析などによって決まり,詳しくないのでなんとも言えない.
  • Q: Bitmap markingでCoWとの相性が上げられているが,GoでフォークしてCoWが活かされるケースはあるか
    A: 調べたがはっきりした言及がない.他の言語などで有効なことは分かっており,一応括弧付きで記載した.
  • Q: Concurrent Mark & Sweepを選んだ理由は?
    A: 最初はCopy GCフラグメンテーションを抑える)などが検討されていたが,Go 1.4でのCからGoでのセルフホストへの移行との兼ね合いもあり,実装コストと効果の面から選ばれた.
  • Q: JVMなどではGCアルゴリズムを変更できるが,Goでもそういうオプションを付ける展望はあるか?
    A: Issueなどで見たことはない

この中で個人的に気になったのは一つ目と二つ目で,これらはお互いに関係しているのですが,突き詰めると

という問いに収束します.ヒープにも短命なオブジェクトのアロケーションが頻繁に起きるなら,世代別GCも効果がありそうに思えますし,そうでないならむしろなぜそのような検討がされているのか気になるところです.

Goでヒープにアロケーションされるのはどのようなとき?

Google先生に聞いてみたところ,このブログがヒットしました.

hnakamur.github.io

この記事自体は

segment.com

これを参考にしているようです.本文からいくつか引用すると,

ポインタはスタック割り当ての阻害要因なので可能なら避ける。
ポインタを使うとほとんどの場合ヒープ割り当てになってしまう。

とか

スライスはサイズが動的でコンパイル時には未決定なのでバックストア(スライス内のポインタが参照する先)の配列がヒープ割り当てになる。

とか

戻り値で文字列やスライスを返す関数には注意。
例えば func (t Time) Format(layout string) string の戻り値のstringの値(正確にはバックストアの配列)はヒープ割り当てになる。

とか書いてあり,Goの文脈に置いても,それなりにヒープへのアロケーションはありそうという感想を持ちました.

結局Goで世代別GCは効くのか?

「go generational gc」とかで検索していると下記のGoogle Formに行き当たりました.

ここでは世代別GCの可能性について,

  • 世代別GCの良いところはSTWの時間を短く出来るところ
  • ただしGoは並列でGCをするので,STWの時間は世代のサイズに依存しない
  • GoではGC時間を最小化する代わりに全体の実行を一次停止するのではなく,異なるコアで並列に実行することの方が良いと想定している
  • とはいえGCが並列で行う必要のある仕事を減らすことで大きな価値をもたらす可能性もある
  • 今この仮説について検証を進めている

ということが述べられているようです(これが2017年).

これを踏まえて

blog.golang.org

を読んでみると(これが2018年),

  • 大きなヒープサイズに対してはそう悪くない
  • ただ私たちのベンチマーク上ではあまり良い結果を示さなかった
  • 世代についての仮説がGoに当てはまらないわけではなく,若いオブジェクトはスタック上で生きて死んでいくというだけ
  • そのため,他の言語よりも世代別GCの効果が弱い

と述べられているようです.

 結局,Goのコントリビューターたちは色々試した上で,今の実装に行き着いているようです.ただし特定のケースでは別の方法のほうが良いというようなことはあり得そうなので,質問でも上がっていたように「GCアルゴリズムを可変にする」というような仕組みがあっても面白いのかなと思いました.

2. Go で"超高速"な経路探索エンジンをつくる


 次にDeNAの井本さんのセッションで,経路探索のアルゴリズムとGoにおける実装上の高速化の話です.井本さんの説明が非常に丁寧でわかりやすくアルゴリズムの話・実装の話のどちらもしっかりと納得しながら話を聞くことができたように感じました.特に実装の話においては,Goにおけるsliceとmapの実装方法を説明した後に,改善の方法と結果を示すという方針で話が組み立てられており,わかりやすかったです.

 LT・セッション等で見かけがちな「でもそれそちらのドメインではの話でしょ?」みたいな高速化ではなく,本当に基礎的な,ちょっとしたGoのコードからでも実践できるようなプラクティスが述べられていることが特に素晴らしかったです.sliceやmapを一切使わずに書くことなんて普通無いと思いますし,それらを使う上で簡単に実践できるプラクティスが背景知識と共にまとまっているのは,知見として非常に素晴らしいと思います.

 また,これは会場の様子を映像と音声でお伝えできないのが悔やまれるのですが,登壇者である井本さんが非常に落ち着いた声でゆっくりと説明されており,聞き取りやすかったというのも大きかったです.人は緊張すると早口になったり,重要なところで噛んでしまったりするものです*3.落ち着いて話されているセッションというのは聞いている側も安心できるので,こういうところも見習っていきたいと思いました.

発表後に上がった質問

 結局mapが遅いので,見方を変えてmapを諦め二次元配列でのアクセスにするという話でmapの高速化の話は終わっていました.それについて,メモリ局所性に基づいた高速なハッシュマップの実装があるはず(?)というような質問が上がっていたのですが,僕自身の知識の無さであまり理解出来なかったため,省略します*4

アルゴリズムの重要さ

 今回はGoConということで,みんなGoの実装についての注目度の方が高いと言うことは間違いないと思います.実際僕もあまりアルゴリズムへの造詣が深いわけではないので,ぱっと使えて分かりやすいsliceやmapの使い方に目が行ってしまいがちです.
 しかし,最も重要なのは「どういうアルゴリズムを選択するか」という部分のはずです.この発表でも前半でアルゴリズムの選択について触れられていますが,実務では単に最速のアルゴリズムを探すわけではなく,実用性を考えて,要件に合うものを使うことになるはずです*5.今回のDeNAさんでの要件だと「渋滞の反映」みたいな,事前計算時間に対する要求があったはずです.何らかの制約がある中でアルゴリズムを選択するのは,どういったところでどういった経験・知識を積むと良いのか知りたいです.競技プログラミングも近い物だとは思いますが,やはり少し実務とは違う部分もあると思っているので.

3. Write Container Runtime in Go

最後に@tomocyくんの発表です.資料が上げられていないので, https://gocon.jp/sessions/write_container_runtime_in_go/ の説明欄から引用します.

昨今では、DockerやKubernetesは多くのプロダクションで採用されています。そしてコンテナランタイムはこれらの技術を支える、正に中心的な技術です。このトークではrunCなどの各コンテナランタイムのGoのコードを紹介しながら、コンテナランタイムについて話します。またOCIが定めるOCI runtimeについてを、自作のコンテナランタイムをそれに準拠させながら話します。このトークの後で実際に自身の手でコンテナランタイムの自作、OCI runtimeへの準拠ができるように、GitHubにログ付きのレポジトリーを公開する予定です。

このセッションではContainer Runtimeを""Goで""実装するという部分に重きが置かれており,実際に彼が実装したコードを示しつつ,OCI Runtime Specに沿って実装を進めていくという形で発表が行われました.
 実は彼もWantedlyスカラシッププログラムで来ていたのですが,自己紹介でおもむろに「実は今日登壇します」と言い出して,全員の度肝を抜いたという裏事情があります.僕は開催前からこのセッションを見る予定をしていたので,まさか同じスカラシップの学生が登壇者だとは思わず,驚きと焦燥でかなり動揺してしまいました……

2019/11/1 0:00 追記
tomocyくんが資料を上げてくれました!

彼のアフターレポートはこちら

medium.com

Container Runtime

コンテナランタイムの実装として最も一般に普及しているのは,おそらくDockerが使用しているruncです.これについては以下の資料が分かりやすかったです.

上記資料の4ページ目にあるように,Dockerのような高レベルランタイムは,runcという低レベルなコンテナランタイムのインターフェースであると言うことが出来ると思います.そのため,Dockerの仕様に沿ったコンテナランタイムを実装すればruncと差し替えて使うことも出来るわけです*6

OCI Runtime Spec

github.com

 コンテナランタイム実装について,標準的な仕様を定義しているのがLinux Foundation傘下にあるOpen Container Initiative(OCI)です.runcの開発元でもあります.

 ランタイムの実装者はこれを見ながら実装するのですが,実はこの仕様では5つの操作方法と,それに対して何を返し,何をするべきかが記述されているだけで,内部の実装をどう書くかには一切言及がされていません.その5つの操作方法が以下です.

操作名 コマンドと引数 何をするべきか
State state <container-id> コンテナの現在の状態を返す
Create create <container-id> <path-to-bundle> コンテナを作る(実行はしない)
Start start <container-id> コンテナに与えられているコマンドを実行する
Kill kill <container-id> <signal> コンテナにシグナルを送って止める
Delete delete <cintainer-id> Createされたコンテナを削除する

上記の5つのコマンドを実行できるCLIを用意すれば,OCI Runtime Specに則ったランタイムを作れる,というわけです.

どう実装するか

 runcと同様,tomocyくんはLinuxの機能(Namespace,cgroup,chroot/pivot_root…etc)をふんだんに利用したランタイムの実装をしたそうで,実際に起動したプロセスが,どのようにホストから隔離されてコンテナとして動作していくのかを順に追って説明してくれたので,仮想化の機能についてそう詳しくない僕でも話について行くことが出来ました.
 また,Goでの実装方法についても

  • urfave/cliで簡単にコマンドを実装する
  • Dependency Injectionを使ってテスタビリティを上げ,CLIを薄く保つ
  • _linuxなどのプレフィクスをファイル名に付けることでBuild ConstraintによってOSごとにビルドされるファイルを変える

などの基本的な,しかし実際によく使われている手法を紹介していました.本当に短いコードで色々示してくれるので,あれっもしかして簡単なのでは?とちょっと思わせてくるあたりが凄かったです.

車輪の再発明が好き」

 この発表で特に印象に残っているのは彼が言っていたこのセリフです.普通だと「それはもうあるし再実装するのは無駄だよ」とか周囲から言われて止められがちなのですが,彼はあえてそういった車輪の再発明をGoでやるということに情熱を傾けているそうです.昼食時にもGoのhttp.Clientを自前で実装し直してみている話とかを聞かせてくれました.

 普段僕は「あるものは使う」というスタンスなので,必要があればサードパーティーの実装を躊躇なく使いますし,言語の持つ便利な機能やモジュールがあれば,同等の機能を自分で実装することはほとんどありません.しかし,逆を言えば

  • そういった機能がどのように実現されているのか
  • 実現するにあたってどういうところに苦労するのか

といった部分をほとんど知らないままでいることになります.例えば今回のコンテナランタイムの話にしても「Namespaceとかchrootとか使ってるらしい」みたいなことは知っていても,どういう順番でそれらが使われていくのか,どのように使うのかといった情報はほとんど知りませんでした.こういったことが他の様々なものに対しても同様に起きていると考えると,自分の無知に絶望してしまいそうです*7

 たまにはそういった,よく使うけどどう動いているのか知らないみたいなものについて,コードリーディングしたり実際に手を動かしてみるなどしてみると,より自分の世界が広がるかなと感じました*8

発表後に上がった質問

  • Q: Dockerを動かすために必要な物にはどういうものがある?
    A: DockerはOCIあんまり守ってないので,イメージのディレクトリレイアウトも違うし,Dockerのためにいいかんじに動かすコマンドが必要
  • Q: 強いランタイムはどういう実装をしている?
    A: 例えばgVisorはセキュリティ特化で,一部ハイパーバイザのようなことをしているらしい.速度はトレードオフかもしれないが,リソース分離をしっかりやっている.評価軸に沿ったランタイム選定をすれば良い.
  • Q: Linux特有の機能を使いまくっていて良い.Mac使ってるみたいだけど,Linuxのやつをどう開発した?
    A: 開発にはエディタの機能を,動作検証はコンテナの中でやった

特に最後のLinuxの機能を使いまくっているのにMac使いってどういうことなんだと思っていたのですが,どうもVSCodeでは環境変数GOOSによって補完される内容が変わるらしく,これを使って実装していたとのこと.確かにGOOS設定できるようにしてくれというIssueがあり,実際に対応されているようです.

github.com

コンテナの検証をコンテナの中でやるというのも,会場の笑いを誘っていました(笑)

tomocyくんの名言(迷言)要約集

  • コンテナランタイムって作ってもあんまり感動がない
  • もっと感動が欲しい人はDockerコマンドから叩けるようにしよう
  • Dockerから使えるようにすればさすがにお母さんも感動してくれるはず

彼はおそらくとんでもない化け物だと思います(褒めてる).

Goコミュニティにどう貢献していくべきか

 残念ながら僕はスーパープログラマではないようなので,Go言語そのものや,Goでの開発に対してとてつもないインパクトのある何かを残せるとは思っていません*9.では,そういう自分が何をやっていけるかというと,やはり

  • Goをこういうシチュエーションで使っている
  • こういうところが便利でこういうところがつらい
  • こんな便利なものを作ってみたよ,実はGoで書いてるよ

みたいな,Goを使うことについての対外的な情報公開かなと思っています.結局ユーザがたくさん居ても情報が出てこないとその活発さは可視化されないし,コミュニティが活発で無いと新規ユーザも増えないと思っています.Goの恩恵をたくさん受けているので,恩返しというと恩着せがましいですが,コミュニティにとってより良い影響を与えられるように頑張っていきます.

まとめ

 Go Conferenceは実務でバリバリGo書いている人でなくても,得られる知見がたくさんあります.開催地が遠くてお金が……という場合にも,何社もスカラシップスポンサーとして学生を支援してくれています.ちょっとでも気になっているなら,次回の参加を検討されてみてはいかがでしょうか.

 次か次の次くらいのGo Conferenceでの登壇目指して精進していきます.同じく登壇を目指す方々,対戦よろしくお願いします.


合わせて読みたい

www.wantedly.com


*1:PythonGCは主に参照カウント方式を,補助的に世代別GCを使用しているのでGoのものとはかなり異なります

*2:blog.golang.org

*3:これは僕の話です

*4:行列演算の例で有名な同じ行の要素に連続してアクセスした方が早いという話に近い物だと思うのですが,短い質問時間の中では僕の理解が追いつきませんでした.

*5:実務でそんなにアルゴリズムをゴリゴリやったことがないので想像で書いています.場合によるとは思います.

*6:これを利用したのが(Docker 18.09までで)コンテナからGPUを使用できるようにしたnvidia-dockerです

*7:例えばGoのmapやsliceの詳しい実装についても井本さんのセッションでこの日初めて知りました

*8:そういう意味で,Wantedlyのスポンサーセッションで紹介されていたGopher's Code Reading Partyはとても良さそうです

*9:これは諦めていると言うことと同義ではありません

公開さえすればいいってもんじゃない

TL;DR

  • 研究で開発したツールを公開するのはとても良いこと
  • 頼むから必要な依存関係の情報を残してくれ
  • 頼むからREADMEを書いてくれ
  • 頼むからライセンスを明記してくれ
  • 頼むからDockerイメージになんでも突っ込まないでくれ

はじめに

 これはあくあたん工房お盆休みアドベントカレンダー7日目の記事です.

 今日は開発者向けではなく,研究者向けの話をします.主にコンピュータサイエンスの話になりますが,ソフトウェアを書く必要のある分野であればどれもそう変わらないだろうと思います.
 研究でソフトウェアを書いている人,それをGitHubで公開している/しようとしている人へ向けて書いています.僕の専攻分野,研究内容による影響でこの記事の内容には偏りがあります.全ての分野に当てはまるとは言えません.よろしくお願いします.

研究とソフトウェア

 コンピュータサイエンスの多くの研究では,規模の大小こそあれど,ソフトウェア(ないしスクリプト)を書いて実験を回すことになります.その過程でできたものをGitHub等で広く公開することも,最近では見られるようになってきました.論文に掲載された理論と実際の実装があれば,それを拡張することも,検証をおこなうことも容易になり,大変便利でありがたいです.しかし,そうして公開されるソフトウェアの多くは様々な要因が重なり,はっきり言うと使い物にならないことが多いです.
 色々とその原因はあるのでしょうが,僕個人は,研究における開発は研究者当人の環境で動けば問題ないことが多く,他環境での再現性には注意が払われないという事態にしばしば陥りがちであるためと考えています.これは以下の様な事態を引き起こします.

  • それを動かすための依存関係の情報が欠落している
  • 使用方法が書いておらず,入力するデータをどのような形式で用意すれば良いのかわからない.
  • ライセンスの表記がない

特に「依存情報の欠落」と「ライセンスの欠落」は致命的であり,このようなリポジトリは基本的に使用不可能に近くなってしまいます.卒業研究のような短期的に誰かが研究を行い,その後続の研究を誰か他の人が行うような事例の場合でもこれらの問題は顕在化し,しばしば技術/研究の継承を途切れさせてしまいます.
 今回はこれらについて,最近使用されることが多くなってきたPythonにフォーカスし,いくつかの対処法を紹介していこうと思います.

依存関係の管理

どうして依存関係管理が必要なのか

 使用するライブラリだけでなく,Python自体も,定期的な機能更新が行われています.しかし,テキストで記述されているソースコードから,使用されているライブラリ・Pythonのバージョンを推測することは容易ではなく,適していないバージョンのものを用いると,実行時にエラーが発生したり,予期しない結果になる恐れもあります.
 また,使用されているライブラリはスクリプトを解析すれば分かるかも知れませんが,OS側にインストールが必要なものがあるとか,特定のハードウェアが必要であるとか,そういった情報も,開発した当人にとっては当たり前かも知れませんが,利用者にとってはそうではありません.これが依存関係の適切な管理・および把握が必要な理由です.

なぜAnacondaは微妙なのか

 研究開発において,よくAnacondaが使用されています.

www.anaconda.com

はっきり言うとAnacondaは必要な依存関係を隠蔽し,開発環境をガラパゴス化させる大きな要因の一つであると考えています.
 Anacondaには,最初からビルド済みのデータ解析用パッケージが多数含まれており,パッケージのインストールのような特別な操作をわざわざしなくても最初から様々なパッケージの恩恵を受けることが出来ます.特にIntel MKLにリンク済みのnumpyなど,通常のインストールでは面倒なものを簡単にインストールできるため,大変人気です.しかし一方で,

  • どのパッケージを自分は実際に使っているのか
  • そのパッケージのどのバージョンを利用しているのか

といった情報からユーザを隔離してしまいがちです.仮想環境を作って運用していたとしても,その状態は明示的に残さない限りリポジトリには含まれないため,気をつけて運用する必要があります.
 深層学習・機械学習は進歩がめざましく,パッケージの更新も頻繁にあるため,以前のAPIが使用不能になることは多く見られます.特に直近ではTensorflowが2.0への移行を進めており,将来的にデフォルトのバージョン*1が切り替わる事になります.ある実装がどのバージョンに対応しているのか,という情報が必要不可欠であることがわかります.

対処法1:Minicondaと仮想環境を使う

 Windowsユーザは実質的にこれを選ぶしかありません.MinicondaはAnacondaの軽量版であり,自分でパッケージを選択してインストールする必要があります. まずは1つの研究プロジェクト,ないしソフトウェアリポジトリごとに1つの仮想環境を作成してください.これにより,研究ごとのPython環境が隔離されるため,異なるプロジェクト間で異なるバージョンのパッケージを要求されても対応できるようになったり,どのパッケージが必要となっているのかが明白になったりします.
condaコマンドを使って仮想環境を作成できます.

# 仮想環境の作成
$ conda create -n 仮想環境名 "python==使うバージョン"
$ conda install -n 仮想環境名 numpy
# 環境へ入る
$ conda activate 仮想環境名
# 環境から抜ける
$ conda deactivate

このように作成した仮想環境の情報を明示的に残すためにはconda env exportというコマンドを使用できます.

# env.ymlとして環境の情報を出力する
$ conda env export -n 仮想環境名 -f env.yml

こうして出力されたenv.ymlには以下の様なフォーマットでパッケージが記録されています.

name: 仮想環境名
channels:
  - defaults
dependencies:
  - パッケージ名=バージョン=ビルド番号
  - pip:
    - パッケージ名==バージョン
prefix: /path/to/仮想環境名

このファイルを共有しておけば,OS,アーキテクチャを揃えれば他の環境でもconda env create -f env.ymlで同等の環境を復元することが出来ます.ただし,新しいパッケージをインストールしても自動的には反映されないため,変更したら再びexportする必要があります.

対処法2:pipenvで自動的に管理する

 Pipenvはrequestsなどの有名パッケージの作者であるKenneth Reitz氏による,新しい依存関係管理ツールです.pipと似たインターフェースを備え,PipfileおよびPipfile.lockにより依存関係のバージョンを固定しています.

# pipenvのインストール
$ pip install --user pipenv
# 仮想環境の作成.PipfileおよびPipfile.lockが作成される
# 明示的に使用するPythonを指定する場合
# pipenv install --python /path/to/python
# または
# pipenv install --python バージョン
# ただし,pyenv等でこれらのバージョンが使用可能になっている必要がある
$ pipenv install
# パッケージのインストール
$ pipenv install numpy
# 仮想環境に入ることなく,仮想環境内のコマンドを呼ぶ
$ pipenv run python main.py
# 仮想環境に入る
$ pipenv shell

今後はpipenv installuninstallを行うごとにPipfileおよびPipfile.lockが更新されるため,これらをリポジトリに含めておけば良いです.Anaconda/Minicondaと違い,pipと同じパッケージを参照するため,MKL等は自分で導入する必要があります.

詳細な使い方は以下の参考リンクをご覧ください.

narito.ninja

対処法3:setup.pyを書く

 これは上記対処法1・2と共存できます.完璧にバージョンを指定するのではなく緩いバージョン制約を設けること,そしてそのパッケージをその仮想環境へインストールする事が出来るようになります.

 まずプロジェクトのディレクトリ構造を以下の様にします.今回作るプロジェクトで使用するPythonモジュール群をpackage_aディレクトリ以下に置くものとします.

.
├── LICENSE
├── README.md
├── setup.cfg
├── setup.py
└── package_a
    ├── __init__.py
    └── hoge.py

setup.pyを書きます.以下の内容だけで良いです.

from setuptools import setup

setup()

setup.cfgを書きます.

[metadata]
name = パッケージ名
version = バージョン
# バージョンをPythonスクリプト内の変数から取ることも出来る
# package_aの__init__.pyに`version = "1.0.0"`などと宣言しておけば以下で参照できる
# version = attr: package_a.version
author = 著作者
author_email = 著作者の連絡先
description = 説明
url = https://github.com/yourname/yourrepo
# ライセンスファイルへのパスを指定
license_file = LICENSE

[options]
# Pythonバージョンの制約.以下だと3.5以上4.0未満
pythonrequire = >=3.5,4.0>
# 含めるパッケージの選択.この場合は自動で探索
packages = find:
# 列挙する場合は普通にインストールするパッケージを羅列すれば良い
# ただしトップレベルパッケージのみ書いてもそれ以下のサブパッケージは含まれない
# packages =
#    package_a

# 依存関係の列挙
install_requires =
    # 以下なら1.0以上,2.0未満のhogeを要求する
    hoge>=1.0,<2.0
    fuga

[options.packages.find]
# 自動探索から外すものを列挙
exclude =
    tests
    piyo

こうしておけば,setup.pyと同じ階層でpip install -e .(pipenvの場合はpipenv install -e .)でEditableモードとしてインストールできます.package_aからのパスでのimportができ,コードを編集すればそれがそのまま反映されるため,何度もインストールする必要がありません.

# インポートする
from package_a import hoge

hoge.~~~

対処法4:Dockerイメージ化する

 最近ではDockerが使用されることも多いです.この場合,OSレベルから依存関係を構築出来るため,より高い再現性を得られます.また,Dockerイメージを直接配布できるため,他環境で何度も環境を作るといった手間を省けるというメリットもあります.一方で,Linuxのコンテナ技術を利用するため,そのほかのプラットフォーム特有の機能を利用していたり,特定ハードウェアにロックインされている場合,GUIが必須の場合は注意してください.

CUDAを利用する場合はnvidia/cudaのイメージを使うと良いです.下記の様なDockerfileと呼ばれるフォーマットのファイルを記述します.

FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu18.04

RUN apt-get update -y && \
    apt-get install \
        python3 \
        python3-pip

RUN pip install \
    tensorflow==1.14 \
    numpy==1.16

COPY . /opt/hoge

CMD ["python3", "/opt/hoge/main.py"]

ピュアなPythonイメージ(FROM python:バージョン)を利用することもできます.CUDAが不要な場合はその方がイメージサイズが小さく,オススメです.Debianベースなので同じようにapt-getが使えます.

これも詳しくは参考リンクをご覧ください

moritomo7315.hatenablog.com

Dockerアンチパターン

やってほしくない例です.

バージョン指定していないイメージ
FROM python

RUN pip install tensorflow

このようなものはビルドごとに最新のものを使ってしまうため,異なるイメージが作成されてしまいます.バージョンはしっかりと指定してほしいです.

FROM python:3.6

RUN pip install tensorflow==1.14
なんでもイメージ

あらゆるライブラリ全部網羅!これさえあればOK!みたいなイメージは,結局のところ無駄にリソースを消費し,依存関係をややこしくするだけです.

  • 初心者がとっかかりに使う
  • Kaggleなどで汎用的に使える環境のために使う

のは良いかも知れませんが,再現性のための環境の配布でこういったことを行うのは明らかに悪手です.Web上ではよく見かけますが,こうしたイメージを使うならDockerでなくてもいいじゃんという気持ちです.

データ自体が入ったイメージ

大容量データセットをイメージ自体に含めるのは最悪なのでやめてください.イメージをローカルに展開しただけでシステムの容量を一気に持って行かれるのは大変困ります.
Dockerにはホストのディレクトリをマウントする機能があるため,データセットとイメージの配布は別々に行うべきです.

# カレントディレクトリのdatasetディレクトリをコンテナの/datasetにマウントして起動する
$ docker run -v $PWD/dataset:/dataset hoge python ~~
docker commitして作ったイメージ

dockerには,起動したコンテナ内での作業を永続化してイメージを作成するためのcommitという機能があります.いくつかのWebサイトには

  1. dockerコンテナを起動
  2. 何か作業
  3. git commit代わりにdocker commit
  4. Dockerhubへpush

みたいな例を見かけますが,いたずらにイメージの肥大化を招き,再現性のある環境を得ることが難しくなるだけなので辞めてください.
Dockerfileは再現性を得るためにわざわざビルド時に実行する処理を記述しているのであって,これを無視して実行結果だけを保存していくだけでは,目的を達成できません.

対処法5:READMEに書く

結局上記のどの対処法を使っても,これが必要になるという話ではあります.詳しくは次節に書きます.

その他の依存関係

 対処法1~3では,OSにインストールする必要のあるライブラリについては一部を除きサポートできません*2.READMEなどに記述しておくべきでしょう.Dockerではこれらもまとめて管理できるため,出来るならDockerfileを用意することをオススメします.

使い方を残す

 せっかく公開されていて動かすこともできるのに,どのように使用して良いか分からない!みたいな事例もあります.わかりやすい使い方が載っているかどうかだけでも,そのリポジトリが活用される可能性は大きく向上すると思います.

対処法1:READMEを書く

 一番単純な方法ですが,意外とやらない人は多く居るという気がしています.書くのが面倒,情報を更新していくのが面倒,など事情は理解出来ますが,後々の自分のためにもある程度書いておいた方が良いと思います.

  • 書いておいて欲しいこと
    • 概要:このソフトウェアを使って何を達成できるのか
    • 環境:どのOS・ライブラリを使うのか,その構築方法
    • 使い方:どういう風に使うのか
    • 設定:どういう設定項目があるのか
    • ライセンス
    • 引用のためのbibtexフォーマットのテキスト
  • 書いていなくても良いなと思うこと
    • 連絡先(何かあればissueに書けば良い)
    • Known issue(issueに書いておけば良い)

特に,ユーザが知りたいのは

  • 何を入力して
  • 何が得られるのか

という情報です.この2点がしっかり分かるだけでも,そのリポジトリに対する評価は大きく変わってくると思います.

対処法2:コマンドラインオプションを作り込む

 python main.py -hなどでヘルプメッセージを表示できるようにする,という意味です.これをしっかりと書いておけばそれ自体がドキュメントとなるため,その出力をREADMEに貼るだけでマシになります.
 Pythonではargparseというモジュールによって実装されています.簡単な使い方は下記で紹介されています.

qiita.com

より複雑なコマンドを作成する場合は拙作のCLIフレームワークもご検討ください.

poyo.hatenablog.jp

対処法3:exampleを作っておく

 どういうデータを用意すれば良いのか,その形式はどのようにすれば良いのか,などを簡単なサンプルを使って説明できるとより良いと思います.が,正直面倒だと思うので,ライブラリとして開発した上で,それを使用する実験スクリプトを全てexampleに突っ込むという構成がオススメかなと思っています.

$ tree -L 3
.
├── LICENSE
├── README.md
├── package_a
│   ├── __init__.py
│   └── hoge.py
└── examples
    └── ex1
        ├── __init__.py
        └── main.py

実際の実験スクリプトを分離しつつ,使用例を示せるので楽です.ただし,あくまでこれはオプションであり,exampleだから分かりやすくしないと!と必死になっては時間が溶けるだけなので注意しましょう.
 また,実験の再現性に重点を置いて,論文中の図表と実行されるスクリプトとを対応付けるとよりわかりやすいかも知れません.他にも,どの順で何を実行するかを示したシェルスクリプトなどがあると,どの順でコードを追いかけていけば良いかも分かってくるため,実験実施中に使用したスクリプトを併記するのも良いかも知れません.

ライセンスを明記する

ライセンスが無いとどうなる?

 GitHubの規約では,明示的にライセンスが示されていないリポジトリの扱いは以下の様になっています*3

You're under no obligation to choose a license. However, without a license, the default copyright laws apply, meaning that you retain all rights to your source code and no one may reproduce, distribute, or create derivative works from your work.

つまり,ライセンスを一切選択しない場合,

  • 通常通り著作権法が適用される
  • 全ての権利は著作者に帰属し,複製や再配布は許可されない
  • そのような状態のコードを勝手に利用することは著作権法違反となる恐れがある

ということです.当然,このような状態のリポジトリを第三者が扱うことは限り無く難しくなってしまい,公開されていて見れるのに使えない,という状況に陥ってしまいます.

OSSライセンスの種類

大雑把に分けてコピーレフトかそうでないか,という2種類があります.代表的な物をいくつかあげます.

利用者に対して「このコードを組み込んで作ったりこれ自体を改変したソフトウェアは同じく公開してください」という通知をするのが概ねコピーレフトライセンスの概要であり,そういったものを要求せず.著作者の表示のみなど軽い条件を求めるのがそれ以外のものです.詳しいことは各ライセンスについて調べてみてください.
 難しいことを考えたくない,いくらでも使ってくれ,というならMITやApache 2.0を採用すれば良いのでは無いかと思います.

ライセンスってどうやって書けば良いのか分からない

全てのファイルの先頭にコメントでライセンスを埋め込むことも出来ますが,より簡潔にするならリポジトリのルートにLICCENSEファイルを置くと良いです*4

詳細は以下のhelpをご覧ください.

help.github.com

まとめ

 OSS開発になじみのある人ならば,この辺りのことはほとんど呼吸のように行っている可能性がありますが,研究と開発は違います.その辺りは理解しているつもりですが,あまりにも使えないリポジトリと遭遇するので……
 上記の内容が守られていれば,より利用されやすくなり,引用も増えて嬉しいのではないでしょうか.よろしくお願いします.


*1:pip install tensorflowで入るバージョンを指して言っています

*2:condaはそれらも一部サポートしていますが完全ではありません

*3:ただし,Forkする権利はGitHubの規約によって依然として存在します. help.github.com

*4:パッケージングする場合はLICENSEファイルを忘れず含めるようにしてください