Vertex Pipelines で lightfm が「時々」status 132 で exit して困った


(Vertex Pipelines とタイトルに入れているものの VertexPipelines に固有の問題ではないです)

状況

Vertex Pipelines 上で実行される機械学習パイプラインを作成していた。
機械学習パイプラインでは lightfm を使用して機械学習モデルの訓練を行っていた。
Vertex Pipelines 上で機械学習モデルの訓練を行う際(=LightFM#fit_partial をコールした際)に時々以下のようなエラーを吐いて落ちてしまっていた。

The replica workerpool0-0 exited with a non-zero status of 132.

Vertex Pipelines 用のコンテナイメージは Github Action で作成していた。
コンテナイメージを作成する際に使用した Dockerfile は以下のようになっていた。

# Dockerfile
FROM python:3.7
WORKDIR /app
COPY . /app
RUN pip install lightfm==1.16

(この場合の)解決策

ほぼ同じ状況で困っている人が Issue を立ててくれていて、しかも解決策について議論されていた。

https://github.com/lyst/lightfm/issues/604
こちらの Issue によると lightfmpip install でインストールする前に環境変数に LIGHTFM_NO_CFLAGS=1 を設定しておけば良いとのこと。
上記の Dockerfile の場合、以下のように書き換えれば良いです。
# Dockerfile (修正版)
FROM python:3.7
WORKDIR /app
COPY . /app
RUN LIGHTFM_NO_CFLAGS=1\
       pip install -r requirements.txt

これでめでたく lightfm が想定通り動いてくれるようになりました。
よかった😊。

解決したけど...🤔

とりあえず解決はしたものの、何故コレが起きたのか、何故上記の方法で解決できたのかがいまいち分かりませんでした。
今まであまり遭遇したことがないタイプのエラーだったので、勉強になりそうだな〜と思いこの事象について少しだけ詳しく調べてみることにしました。
以降では今時点で自分が理解した内容をまとめてあります (あまりこのあたりの内容に明るくないので間違っていたら教えていただけると嬉しいです🙇‍♂️)。

exit status 132 とはなんぞや

exit status は以下のようにマッピングされるらしい。
exit status number: 128+nFatal error signal: n
ref:https://tldp.org/LDP/abs/html/exitcodes.html
今回の場合、132128 + 4 なので、エラーシグナル 4 ということだと思われる。
またエラーシグナル 4 は Wikipedia 曰く SIGILL を意味するらしい。ref:https://ja.wikipedia.org/wiki/シグナル_(Unix)
じゃあ SIGILL は何かというとこちらも Wikipedia 曰く、

通常、命令でないメモリ領域にジャンプしたときに発生(コールスタックのリターンアドレスが破壊されたときなど)。他に特権レベルが高くないと実行できない命令を実行しようとしたときなどにも発生する。

とのこと。
ひとまず、コンピュータが処理できないおかしな命令を出しちゃった時に 132 が吐かれる、くらいの理解で先に進んでみる。

Issue のコメント

次は Issue のコメントを見ていこうと思います。

Hi,
if you clone and install via pip install . -vv you can see the flags in the end of a line that usually starts with "gcc". For example, mine looks like this

gcc -pthread -B /home/simon/miniconda3/envs/test2/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/simon/miniconda3/envs/test2/include/python3.7m -c lightfm/_lightfm_fast_openmp.c -o build/temp.linux-x86_64-3.7/lightfm/_lightfm_fast_openmp.o -ffast-math -march=native -fopenmp

There, you see that -ffast-math -march=native -fopenmp were used. In my experience ffast-math is often problematic. You can avoid it by setting an environment variable, i.e.

export LIGHTFM_NO_CFLAGS=1 && python -m pip install . -vv

This is the relevant line in setup.py
Alternatively, you can install the pre-compiled package from conda-forge where we also compile without ffast-math.

gcc-ffast-mathetc. 耳馴染みのない単語たちだ...
一つずつ見ていきます。

gcc

ソースコードをコンパイルする際に使えるやつ、くらいの理解で一旦先に進んでみます。

-ffast-math -march=native -fopenmp

どれもコンパイル時の最適化オプションらしい。

https://linuc.org/study/knowledge/374/

LIGHTFM_NO_CFLAGS

Issue のコメントにある通り setup.py 内で環境変数 LIGHTFM_NO_CFLAGS を参照している。LIGHTFM_NO_CFLAGS が設定されている場合のみ -ffast-math-march=native (Anaconda でない場合)をコンパイルオプションに設定しているみたい。

https://github.com/lyst/lightfm/blob/d05289982928f81b957c1f0ff63dcaee0e915f3b/setup.py#L17-L18

pip install についている -vv というオプション

(今回の問題とはあまり関係ないです。)
このオプション実は初めて見ました。
インストール中のログをどの程度詳しく表示するか指定するためのオプションなんですね。

https://pip.pypa.io/en/stable/cli/pip/#cmdoption-v
ちなみに additive なオプションとのことで 3 つまで増やせる (-vvv) らしい。
additive なオプションというのも実は初めて見ました。そういうオプションもあるんだね。
デバッグの際に便利だ。

ということは多分...

以下のような流れで今回の問題が発生したのかもしれない。
GithubActions で pip install する際に lightfm が GithubActions のマシンの CPU アーキテクチャに対して最適化される。
GithubActions で使用したマシンと実際にプログラムが実行されるマシンの CPU アーキテクチャが異なっている場合がある。
このような場合、 GithubActions のマシンに対して最適化された lightfm は実際にプログラムが実行されるマシンでうまく動かない場合がある。

感想

最初にこの現象に遭遇した時、132exit したよ、という見慣れないエラーメッセージだけしかログに残っておらず、これじゃあ何もわからんよ、と思った。
けど落ち着いてググってみると(いつも遭遇するその他の問題と同じように)ちゃんと理解できる(気になれる)ことが分かって良かった。

参考資料