TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

【iOS開発】フレームレートに合わせて描画する

今まで音声解析の結果を画面に表示する際に、AudioQueueNewInputのコールバックで、UIViewのsetNeedsDisplayを呼ぶようにしていましたが、最新のXcodeだと、メインスレッドで呼ぶように警告がでるようになっていました。

元々UIViewの処理は、すべてメインスレッドで処理する必要がありましたが、AudioQueueNewInputのコールバックで処理しても特に問題なく動いていました。
しかし、作法に従った方がよいので、メインスレッドで処理する方法を調べました。

タイマーを使って定期的に描画する方法もありますが、フレームレートと同期して描画できた方がちらつきを抑えることができます。
調べたところ、以下のようにすればフレームレートと同期して描画できました。

MainView.h
@interface MainView : UIView
@property CADisplayLink* displayLink;
@end
MainView.m
- (id)initWithCoder:(NSCoder *)aDecoder {
    // ...
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

これで、フレームレートと同じ間隔で、setNeedsDisplayが呼ばれるようになります。

【iOS開発】iCloudにエクスポート/インポートする

しばらく更新していなかったiOSアプリを久しぶりに開発しています。

メールで、アプリで保存したファイルを取り出せないのかという質問/要望を何通かもらっていたので、iCloudにエクスポートできるようにしました。
実装方法をメモっておきます。

共通

ViewController.h

ViewController にUIDocumentPickerDelegateデリゲートを追加する。

@interface ViewController : UIViewController<UIDocumentPickerDelegate>

エクスポートする

ViewController.m

エクスポートをしたい任意の箇所に記述する。

        NSURL *url = [[NSURL alloc] initFileURLWithPath:filePath];
        UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithURL:url inMode:UIDocumentPickerModeExportToService];
        picker.delegate = self;
        [self presentViewController:picker animated:YES completion:nil];

エクスポート完了後の処理は、documentPickerメソッドに記述する。

- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
    if (controller.documentPickerMode == UIDocumentPickerModeExportToService) {
        NSLog(@"Exported");
    }
}

インポート

インポートをしたい任意の箇所に記述する。

        UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[(NSString *)kUTTypeAudio] inMode:UIDocumentPickerModeImport];
        picker.delegate = self;
        [self presentViewController:picker animated:YES completion:nil];

ファイルタイプには、kUTTypeAudioを指定しているが、用途に合わせて他のファイルタイプに変更する。
kUTTypeAudioは、MobileCoreServices.hに定義されているため、必要に応じてincludeする。

#import <MobileCoreServices/MobileCoreServices.h>

インポート完了後の処理は、documentPickerメソッドに記述する。

- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
    if (controller.documentPickerMode == UIDocumentPickerModeImport) {
        // ファイルコピー
        NSString *filename = url.lastPathComponent;
        NSString *filePath = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:filename];
        NSError *err = [[NSError alloc] init];
        BOOL result = [[NSFileManager defaultManager] copyItemAtPath:url.path toPath:filePath error:&err];

        if (result) {
            NSLog(@"Imported");
        }
        else {
            NSLog(@"Import failed. %@", err.localizedDescription);
        }
    }
}

インポートしたファイルがテンポラリディレクトリにコピーされるので、Documentsディレクトリにファイルをコピーしている。


エクスポートとインポートの両方を行う場合は、documentPickerメソッドで、controller.documentPickerModeによって条件分けをすればよい。

余談

最新のiPad miniデバッグしようとしたらXcodeのバージョンアップが必要で、XcodeをバージョンするにはOSのバージョンアップが必要で、開発環境を整えるのに一苦労でした。
Objective-cも結構忘れていたので、思い出すのに一苦労。

いまさらですが、新しいXcodeでは、automatically manages signingという機能が追加されていて、あのめんどくさい証明書の更新が自動になっていて感動しました。

将棋AIの進捗 その29(自己対局におけるノードの再利用)

先日の記事に書いたが、AlphaZeroは自己対局時にノードの再利用を行っている。
dlshogiでは、先手が探索した結果を後手が利用することになるため(逆も同様)、先手と後手の探索のバランスが崩れるため、ノード再利用を行わず各手番でハッシュをクリアしていた。

しかし、先日の記事で考察した通り、ルートノードをクリアすることで、ノードを再利用しても先手、後手の偏りはそれほど問題にならないと思われる。
そこで、実際にノードを再利用しない場合とする場合で、精度を比較してみた。

測定条件

  • dlshogiの最新の学習済みモデルを使用して、ノードを再利用なしの場合とありの場合で自己対局を行い教師局面を生成する
  • 生成する局面は、250万局面
  • 生成した教師局面を使用して、初期モデルから学習して、floodgateの棋譜との一致率を測定する
  • 初期局面集を使用して、4手までルートノード訪問数に応じた確率で手を選択
  • Momentum SGD(lr=0.001)
  • バッチサイズ1024

測定結果

train loss policy一致率 value一致率
ノード再利用なし 3.7723558 0.06413719 0.5564077
ノード再利用あし 3.214862 0.11954415 0.5857407

考察

ノード再利用ありの場合、policy、valueともに大幅に一致率が高くなった。
ノード再利用により、より深くまで探索できるようになったことから、より精度が高くなったと考えられる。
先手と後手の偏りが問題になるなら、valueの精度は落ちるはずだが、そのようなことは起きていない。
自己対局におけるノード再利用は精度向上に寄与することがわかった。

備考

初期モデルから学習するとlossがnanになったが、GradientClippingを追加することで学習できるようになった。

余談

ノード再利用の実装は、かなり苦労した。
再現性の低い条件で、再利用するノードが未展開になる場合があり、バグを見つけるのに1週間くらい費やした。
リリースビルドだと最適化されているため見たい変数の状況がわからず、デバッグビルドだと遅すぎて再現できないため、デバッグ情報なしで最適化オフでビルドして、再現させて状態を確認して、assertを追加してを何回か行って原因を特定した。

また、前回の記事に書いたCUDAのエラーが発生して、学習がうまくできない状況になった。これも結局、ノード再利用によって、既存のバグが顕在化したことが原因だった。

ビルド済みChainerからソースビルド版に戻す

Chainer/CupyはWindowsは正式にサポートされていないが、問題なく動作する。
最近のバージョンでは、Windows向けにもビルド済みパッケージも提供されているので、以前は必要だったVisual Studioがなくてもインストールできる。

先日、開発環境をVisual Studio 2017に移行した際、Cupyをソースからインストールしようとすると、Visual Studio 2017でコンパイルエラーが起きることを書いた。
そのため、ビルド済みパッケージからインストールを行った。

しかし、長時間学習を行っていると、以前には発生しなかったエラーが発生するようになり、安定して学習ができなくなった。

発生するエラー
Traceback (most recent call last):
  File "train_rl_policy_with_value_using_hcpe_bootstrap.py", line 106, in <module>
    x1, x2, t1, t2, z, value = mini_batch(train_data[i:i+args.batchsize])
  File "train_rl_policy_with_value_using_hcpe_bootstrap.py", line 87, in mini_batch
    Variable(cuda.to_gpu(move)),
  File "C:\Anaconda3\lib\site-packages\chainer\backends\cuda.py", line 288, in to_gpu
    return _array_to_gpu(array, device_, stream)
  File "C:\Anaconda3\lib\site-packages\chainer\backends\cuda.py", line 336, in _array_to_gpu
    return cupy.asarray(array)
  File "C:\Anaconda3\lib\site-packages\cupy\creation\from_data.py", line 61, in asarray
    return core.array(a, dtype, False)
  File "cupy\core\core.pyx", line 2350, in cupy.core.core.array
  File "cupy\core\core.pyx", line 2403, in cupy.core.core.array
  File "cupy\cuda\memory.pyx", line 371, in cupy.cuda.memory.MemoryPointer.copy_from_host_async
  File "cupy\cuda\runtime.pyx", line 265, in cupy.cuda.runtime.memcpyAsync
  File "cupy\cuda\runtime.pyx", line 136, in cupy.cuda.runtime.check_status
cupy.cuda.runtime.CUDARuntimeError: cudaErrorIllegalAddress: an illegal memory access was encountered
Traceback (most recent call last):
  File "cupy\cuda\driver.pyx", line 192, in cupy.cuda.driver.moduleUnload
  File "cupy\cuda\driver.pyx", line 81, in cupy.cuda.driver.check_status
cupy.cuda.driver.CUDADriverError: CUDA_ERROR_ILLEGAL_ADDRESS: an illegal memory access was encountered
Exception ignored in: 'cupy.cuda.function.Module.__dealloc__'
(略)
Traceback (most recent call last):
  File "cupy\cuda\driver.pyx", line 192, in cupy.cuda.driver.moduleUnload
  File "cupy\cuda\driver.pyx", line 81, in cupy.cuda.driver.check_status
cupy.cuda.driver.CUDADriverError: CUDA_ERROR_ILLEGAL_ADDRESS: an illegal memory access was encountered
Error in sys.excepthook:

Original exception was:

毎回発生するわけではなく、再起動すると発生しなくなったりする。
しかし、合計3回発生したので、継続利用はできなそうなので、ソースビルド版に戻すことにした。

Visual Studio 2015と2017の共存

ソースビルドは、上記の通り、Visual Studio 2017ではできないため、Visual Studio 2015が必要になる。
Pythonは最新のAnacondaを使用しているため、Visual Studio 2017でビルドされているが、Cランタイム(VCRUNTIME140.dll)はバイナリ互換性があるため、CupyをVisual Studio 2015でビルドしても問題ない。

Visual Studio 2015と2017は両方インストールできる
インストール順番はどちらでもよさそうだが、念のため、2017をアンインストールしてから2015をインストールして2017をインストールした。

pipでインストール時にどちらのVisual Studioが使用されるかは、インストーラの作りに依存している。
cupyのインストーラは2017がインストールされていても、2015が使用された。

pip install cupy --no-cache-dir

これで、ソースからインストールできた。
続けて、Chainerもインストールする。

pip install chainer --no-cache-dir

ソースからビルドしたバージョンでは、上記のエラーは発生しなくなり、安定して学習できるようになった。


なお、Cythonを使って、C++のソースをビルドする場合は、自動で2017が選択される。
2015を使用したい場合、公式ドキュメントには特に書かれていないが、スタートメニューから「VS2015 x64 Native Tools Command Prompt」を選んでコマンドプロンプトを起動すると2015が使用された。

2019/4/24 追記

結局、ビルドしたバージョンでも同じエラーがでるようになり、原因がつかめていません。
ChainerやPythonバージョンを戻しても変わらず。困った。
一旦、別のGPUを使用して再現するか様子見する。

2019/4/27 追記

GPUを変えても発生するため、GPU故障も疑いましたが、関係なかったようです。
そして、やっと原因が特定できました。

Visual StudioとChainerのバージョンを戻しても発生しましたが、その際のエラーメッセージが、

Traceback (most recent call last):
  File "train_rl_policy_with_value_using_hcpe_bootstrap.py", line 109, in <module>
    x1, x2, t1, t2, z, value = mini_batch(train_data[i:i+args.batchsize])
  File "train_rl_policy_with_value_using_hcpe_bootstrap.py", line 88, in mini_batch
    return (Variable(cuda.to_gpu(features1)),
  File "C:\Anaconda3\lib\site-packages\chainer\backends\cuda.py", line 288, in to_gpu
    return _array_to_gpu(array, device_, stream)
  File "C:\Anaconda3\lib\site-packages\chainer\backends\cuda.py", line 336, in _array_to_gpu
    return cupy.asarray(array)
  File "C:\Anaconda3\lib\site-packages\cupy\creation\from_data.py", line 61, in asarray
    return core.array(a, dtype, False)
  File "cupy/core/core.pyx", line 2371, in cupy.core.core.array
  File "cupy/core/core.pyx", line 2418, in cupy.core.core.array
  File "cupy/core/core.pyx", line 2415, in cupy.core.core.array
  File "cupy/cuda/pinned_memory.pyx", line 212, in cupy.cuda.pinned_memory.alloc_pinned_memory
  File "cupy/cuda/pinned_memory.pyx", line 286, in cupy.cuda.pinned_memory.PinnedMemoryPool.malloc
  File "cupy/cuda/pinned_memory.pyx", line 306, in cupy.cuda.pinned_memory.PinnedMemoryPool.malloc
  File "cupy/cuda/pinned_memory.pyx", line 303, in cupy.cuda.pinned_memory.PinnedMemoryPool.malloc
  File "cupy/cuda/pinned_memory.pyx", line 177, in cupy.cuda.pinned_memory._malloc
  File "cupy/cuda/pinned_memory.pyx", line 178, in cupy.cuda.pinned_memory._malloc
  File "cupy/cuda/pinned_memory.pyx", line 29, in cupy.cuda.pinned_memory.PinnedMemory.__init__
  File "cupy/cuda/runtime.pyx", line 238, in cupy.cuda.runtime.hostAlloc
  File "cupy/cuda/runtime.pyx", line 144, in cupy.cuda.runtime.check_status
cupy.cuda.runtime.CUDARuntimeError: cudaErrorIllegalAddress: an illegal memory access was encountered
Traceback (most recent call last):
  File "cupy/cuda/driver.pyx", line 192, in cupy.cuda.driver.moduleUnload
  File "cupy/cuda/driver.pyx", line 81, in cupy.cuda.driver.check_status
cupy.cuda.driver.CUDADriverError: CUDA_ERROR_ILLEGAL_ADDRESS: an illegal memory access was encountered
Exception ignored in: 'cupy.cuda.function.Module.__dealloc__'
Traceback (most recent call last):
  File "cupy/cuda/driver.pyx", line 192, in cupy.cuda.driver.moduleUnload
  File "cupy/cuda/driver.pyx", line 81, in cupy.cuda.driver.check_status
cupy.cuda.driver.CUDADriverError: CUDA_ERROR_ILLEGAL_ADDRESS: an illegal memory access was encountered
(略)

となっており、pinned_memoryの確保に失敗しているようでした。

PinnedMemoryPoolに関するエラー情報がないか調べたところ、メモリプールは断片化される可能性があるので、free_all_blocksを呼ぶとよいことがわかりました。
Redirecting to Google Groups
concat_examples with various batch size causes memory leaks · Issue #4911 · chainer/chainer · GitHub

そこで、

chainer.cuda.cupy.get_default_pinned_memory_pool().free_all_blocks()

を定期的に呼ぶようにしたことろ、エラーが発生しなくなりました。
(完全に対策できたかはまだ様子見ですが)

なぜVisual Studioのバージョンアップ契機で発生するようになったかは謎ですが、同時にWindows Updateとかも行っているので他に契機があったのかもしれません。100%発生するわけでもないので、今までたまたま動いていたというだけかもしれません。

2019/4/28 追記

free_all_blocks()を呼ぶようにしても発生したので、さらに原因を調査したところやっと原因がわかりました。
自己対局プログラムにバグがあり、マルチスレッドのタイミングに依存して教師データの指し手が無効な値となったデータが混じっていたことが原因でした。
今までも不正なデータがごく稀に混じっていましたが顕在化していなかっただけでした。

発生頻度が上がったのは、プログラムにノードを再利用する修正を加えたためで、マルチスレッドの微妙なタイミングが変わったため発生しやすくなったためでした。
不正なデータを除くと、エラーは発生しなくなりました。

2週間以上、バージョンの問題を疑って時間を無駄にしてしまいましたorz
CUDAのメモリエラーは不正なデータがないか疑うべきという教訓とします。

WindowsでLeela Chess Zeroをビルドしてデバッグする

人の書いたソースを調べる際、動かせるようにしてからデバッガでステップ実行すると理解しやすい。

Leela Chess Zeroのソースをたまに参照していたが、詳細に実装を調べるために、デバッガで動かせるようにした。
公式のビルドの説明の通りだが、手順をメモしておく。

GitHubからソースをクローンする
git clone https://github.com/LeelaChessZero/lc0.git

ビルドシステムのmesonのバグなのか、Cドライブに配置しないとビルドでエラーが発生した。

Intel MKLをインストールする

Intelのサイトから、MKLをダウンロードしてインストールする。
インストーラは2種類あるが、フルインストール版はFortranもインストールされるので、不要であればカスタムインストール版を選んだ方がよい。

なお、OpenBLASでもビルドできるようになっているが、ビルドはできたがlibopenblas.dllでエラーが発生して実行できなかった。
Intel MKLでは問題が起きないので、Intel MKL版でビルドした。

OpenCLにも対応しているが、ソース調査が目的なのでOpenCLは使用しない。

mesonをインストールする

pipでmesonをインストールする。

pip install meson
build-cl.cmdを編集する

[編集前]

meson.py build --backend vs2017 --buildtype release ^
-Dmkl_include="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\include" ^
-Dmkl_libdirs="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\lib\intel64" ^
-Dopencl_libdirs="C:\Program Files (x86)\AMD APP SDK\3.0\lib\x86_64" ^
-Dopencl_include="C:\Program Files (x86)\AMD APP SDK\3.0\include" ^
-Ddefault_library=static

[編集後]

meson build --backend vs2017 --buildtype release ^
-Dmkl_include="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\include" ^
-Dmkl_libdirs="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\lib\intel64" ^
-Ddefault_library=static

meson.pyに拡張子があると実行できないので削除する。
openclは使用しないのでオプションから削除する。

2019/12/28追記

最新のリポジトリでは、拡張子は修正されている。
また、OpenCLを無効にする場合は、オプションに「-Dopencl=false」が必要になっている。

MSVS projectを作成する

コマンドプロンプトを起動して、

"C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\bin\mklvars.bat" intel64

を実行する。
git cloneしたディレクトリに移動して、

build-cl.cmd

を実行する。

プロジェクトファイルが作成された時点でビルドを行うかプロンプトが表示されるので、終了する。
そのままビルドしてもよいが、Visual Studioで開いて実行する際に再度ビルドすることになる。

プロジェクトを開いてビルドする

buildディレクトリに、lc0.slnが作成されているので、Visual Studio 2017で開いてビルドする。

ネットワークを取得する

Leela Chess Zeroのプロジェクトのサイトからネットワークをダウンロードして、buildディレクトリに配置する。
ネットワークは実行速度が速い方がよいため、Blocks 10、Filters 128のものを選択する。

デバッガで実行する

Visual Studio 2017で、lc0のプロジェクトをスタートアッププロジェクトに設定して、デバッグ実行する。
コンソールに以下のように入力する。

isready
position startpos
go

探索が開始するので、デバッガで中断する。

デバッグ->ウィンドウ->並列スタックを表示して、実行中のスレッドのスタックを調べる。
f:id:TadaoYamaoka:20190415190020p:plain


停止するタイミングによって、スタックの状態が異なるが、そこからステップ実行することで処理の流れは把握できる。

Leela Chess Zeroの探索のメイン処理は、search.ccのSearchWorker::ExecuteOneIteration()に記述されている。

void SearchWorker::ExecuteOneIteration() {
  // 1. Initialize internal structures.
  InitializeIteration(search_->network_->NewComputation());

  // 2. Gather minibatch.
  GatherMinibatch();

  // 3. Prefetch into cache.
  MaybePrefetchIntoCache();

  // 4. Run NN computation.
  RunNNComputation();

  // 5. Retrieve NN computations (and terminal values) into nodes.
  FetchMinibatchResults();

  // 6. Propagate the new nodes' information to all their parents in the tree.
  DoBackupUpdate();

  // 7. Update the Search's status and progress information.
  UpdateCounters();
}

UCTの探索の処理は、名前が分かりにくいがGatherMinibatch()で行っている。

調べた内容のメモ

  • 探索とネットワークの推論は、dlshogiの実装とほぼ同じで直列化されていた。
  • 不要になったノードのガベージコレクションを専用スレッドで行っている。
  • ucb1のベストノードをキャッシュしている(バーチャルロスの数と比較して再計算が不要な場合再利用している)
  • ノードのハッシュは使用していない(木構造をノードのリンクで保持している)
  • 探索はマルチスレッド(オプションのThreadsで変更できる)
  • ノードを更新する際は、そのノードのみをロックする
  • NNの結果は、履歴局面のハッシュをキーにしてキャッシュしている。

技術書典6の報告と委託販売のお知らせ

技術書典6無事終了しました。
来てくださった方、ありがとうございました。
コンピュータ将棋将棋選手権、応援してますと声をかけてくださったりしてうれしかったです。

新刊は150部用意していったのですが、用意した部数はちょうどでした。
数部残ったので世界コンピュータ選手権に持っていきます。

Twitterでご要望いただいていましたので、委託販売も登録しました(手数料の分若干上乗せしていますm(__)m)。
tadaoyamaoka.booth.pm


ということで、参加された皆さんお疲れ様でした!
次回も、書きたいネタができたらまた参加したいと思います。

【お知らせ】技術書典6

すでに当ブログで告知していますが、技術書典6で「ディープラーニングを使った将棋AIの作り方~強化学習編~」という本を出します。
内容は、AlphaZero Shogiの仕組みと実装についてです。
電子版のコードも付属します。価格は500円です。

チェック数が印刷部数より多くなってしまったので、電子版のみのDLカード(価格は同じ)も用意していきます。
すぐにはなくならないと思いますが、紙の本が欲しい方は(今回入場制限で13時までは有料なのが心苦しいですが)すこし早め来てもらえればと思います。

既刊は電子版のみになります。1作目のPolicy編が200円。2作目の大規模学習、高速化編が300円です。

かんたん後払いにも対応します。利用には事前にアプリのインストールが必要です。

あと、マイナビ出版「こ21」で、書籍「将棋AIで学ぶディープラーニング」の電子版が限定価格1500円で販売されます。
こちらもよろしくお願いいたします。