SyntaxHighlighter

2023年11月27日月曜日

SQLite3 のトランザクションと Rails

最近は開発よりもマネージメントの仕事が多いのですが、久しぶりに技術的な話で書くネタができたので書いておきます。

SQLite3 のトランザクションは他のデータベースと少し異なり特殊なので、色々とはまりがちですが、その内容についてまとめておきます。
Rails での話も少し書いておきます。

SQLite3 の分離レベル

SQL の分離レベルは、 Wikipedia の記事にあるように、 Read Uncommitted, Read Committed, Repeatable Read, Serializable の4種類があります。

SQLite3 では、 Read Uncommitted になる例外もありますが、基本的には常に Serializable です。
後述するトランザクションのモードとして、 Deferred, Immediate, Exclusive が指定できますが、それとは関係なく、常に Serializable になるようです。
つまり、どのトランザクションのモードを選んでも、 Dirty Read や Phantom Read などは起こらず、トランザクション中は、常に整合性の取れた読み取りができるかエラーになるかのいずれかになります。

SQLite3 のトランザクションのモードと、エラーになるケース

SQLite3 では、トランザクションのモードを BEGIN TRANSACTION の際に指定することができます。

公式のページに書かれていますが、 Deferred, Immediate, Exclusive の3つを指定でき、デフォルトでは Deferred が使われます。
細かな定義については公式のページに記載があるので割愛しますが、それぞれで、「ロックを取得するタイミング」、「取得するロックの種類」が異なります。
これらは、既に実行中のトランザクションが別で存在する場合に、「相手のトランザクションの終了をどのタイミングで待つか」や「何をしたときにエラーにするか」が異なります。

例えば、標準の設定である Deferred を選んだ場合、 BEGIN DEFERRED TRANSACTION を実行したタイミングでは何のロックも取得されず、その後に続く SELECTINSERT などが実行された際に、初めて読み取り用の Shared ロックや書き込み用の Reserved ロックが取得されます。
なお、 Shared ロックと異なり、同時に複数のセッションが Reserved ロックを取得することはできません。
そのため、例えば下記のようなコードをほぼ同時に二つ実行すると、後から実行した方は INSERT 実行のタイミングで Reserved ロックの取得に失敗することになります。

これは、 SELECT 実行の時点で Shared ロックが取得され、 INSERT 実行の時点で Reserved ロックの取得が試みられることによります。
Reserved ロックは同時に一つのセッションしか取得できないので、後から実行した方のコードでは、 Reserved ロックの取得ができません。
このタイミングで、後から実行した方のコードは、トランザクションの実行に失敗します。
勘違いしやすいですが、自動のリトライは走らず Busy Timeout の設定も意味を持ちません。
これは、後から実行した方のコードでも、既に SELECT を発行しているために、このトランザクションの Serializable の分離レベルを保証できなくなるからです。
よって、 INSERT 実行のタイミングで、デッドロックにより失敗します。

一方で、後から実行した方のコードで、 SELECT などの読み取りのみを行うのであれば、トランザクションは最後まで問題なく実行されます。
おそらくですが、 Deferred のトランザクションが最も効果的に動作するのはこのような読み取りの場合で、この場合は Phantom Read などの影響を受けずに読み取れることが保証できます。

なお、 Immediate を指定したトランザクションの場合、 BEGIN IMMEDIATE TRANSACTION を実行したタイミングで即座に Reserved ロックが取得されるので、同時に二つのコードを実行してもエラーは発生しません。
ただし、 SELECT も待たされてしまうので、並列性は下がることになります。
同時に読み込みや書き込みがあってもエラーとなる可能性は下がるので、こちらが適しているケースも多々あるように思います。

Ruby on Rails での挙動

Ruby on Rails では、標準では SQLite3 がデータベースとして使われるように設定されています。

一方で、 SQLite3 独自のトランザクションのモードである Deferred や Immediate は、 Rails の世界からは容易には指定することはできません。
原則として常に Deferred になります。
なお、分離レベルとしては、 Read Uncommitted は指定することができるようになったようです。

いずれにしても、 SQLite3 は基本的には分離レベルは Serializable であり、その上で Deferred, Immediate などのトランザクションの種類を指定することになるので、他のデータベースも考慮して、汎用的な Rails のコードを書こうと思うと、トランザクションのモードを指定するのは難しくなります。

sqlite3 ライブラリでの吸収

こちらについて少し考えた結果、 SQLite3 へアクセスするためのライブラリ sqlite3 で吸収するのがよいのではないかと考えました。

そこで、こちらの Pull Request で、「標準で使うトランザクションのモード」をコンストラクタで指定できるようにしてもらいました。

これにより、トランザクション実行時に、どのようなモードにするかの既定値を指定することができるようになります。
Rails では、何も指定せずにトランザクションが開始されるので、そういった状況で使われるモードを指定できることになります。

Ruby on Rails での設定

以上を踏まえ、 sqlite3 のバージョン 1.6.9以降を使った上で、以下の設定値を config/database.yml に書くと、トランザクションが Immediate モードで実行されるようになります。

2023年4月3日月曜日

PyTorch の TorchScript のデバッグメッセージ (PYTORCH_JIT_LOG_LEVEL によるログ出力)

PyTorch の挙動を調べる必要があった際に、ログ出力の設定を調べたことを書いておこうと思います。

PyTorch の TorchScript のログ出力

PyTorch のモデルを保存する際などに PyTorch 内部で利用される TorchScript には、標準でデバッグ用のログ出力の機能が備わっています。

なお、私は ONNX へのエクスポートの際の挙動のデバッグの際にこのログ出力の機能を用いました。

PYTORCH_JIT_LOG_LEVEL 環境変数の設定

基本的に、 jit_log.h の冒頭のコメント部分に書かれていることに従って環境変数を設定します。

この環境変数には、以下のようにログ出力したいファイルのファイル名を指定します。

  • PyTorch のソースコードを見て、ログを出力したいファイルを探します。
    例えば、 torch/csrc/jit/passes/onnx/shape_type_inference.cpp のログを出力したい場合、 shape_type_inference.cpp が設定すべきファイル名になります。
  • 出力したいログのレベルは現時点では 3段階あり、その値に応じて > をいくつか付与します。
  • 複数のファイルのログを出力したい場合は、 : で区切って連結します。
  • 結果として、 shape_type_inference.cpp:>subgraph_matcher.cpp のような文字列を PYTORCH_JIT_LOG_LEVEL に設定します。

結果として、 Bash であれば export 'PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp' のように設定します。
Windows 上のコマンドプロンプトを使う場合は、 set "PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp" のように設定します。

> を含む場合、 '" で囲む必要があるのに注意してください。

ログ出力の例

例として、以下のようなスクリプトを実行する際のログを出力することを考えます。

このスクリプトを実行したときの shape_type_inference.cpp のログをすべて出力する場合、 >>shape_type_inference.cppPYTORCH_JIT_LOG_LEVEL 環境変数に設定します。
この状態で上記のスクリプトを実行すると、以下のようなログが出力されます。

Windows 上でログ出力する際の注意事項

実際には、先の例の通りにやっても、 Windows ではログが出力されません。
これは、 v2.0.0 の時点での (おそらく) バグで、 PYTORCH_JIT_LOG_LEVEL に設定されたファイル名と __FILE__ を比較する際の不具合によるものです。

StripBasename 関数の中でファイルのフルパスからファイル名部分を抽出していますが、この部分はディレクトリの区切り文字として / しか見ていません。
そのため、 Windows で __FILE__C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp のようなファイル名が設定されている際には、ファイル名部分として shape_type_inference.cpp がうまく抽出されず、 PYTORCH_JIT_LOG_LEVELshape_type_inference.cpp が設定されていてもうまくログが出力されませんでした。

この場合、 StripBasename を修正してビルドし直すか、 site-packages\torch\lib\torch_python.dll をバイナリエディタで直接修正し、 DLL に含まれる C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp の部分を C:/actions-runner/_work/pytorch/pytorch/builder/windows/pytorch/torch/csrc/jit/passes/onnx/shape_type_inference.cpp に変更することで、ログを出力できるようになります。

なお、このプルリクエストで修正されました!

このログを使ってデバッグした話の詳細については、次回以降に書くかもしれません。