エイエイレトリック

なぐりがき

# Spacy で実行のたびにメモリ消費量が増える問題

簡単に調査したのでまとめます。

tl;dr

  • Spacy は語彙をハッシュ値に変換し管理するVocab クラスを持つ
  • 未知語があるたび Vocab のデータは増えていく
  • 現時点 (v3.7.2) で Vocab をリセットする方法はない
  • メモリを圧迫しないように、APIに載せる場合は定期的にモデルをリロードする必要がある

導入

Spacy model を動かす APIDjango で実装しクラウドにデプロイしたところ、 定期的に uWSGI の [deadlock-detector] が発生しました。

メモリが足りずプロセスが再起動しているようでした。

サイズを増やしても解消しなかったので、メモリリークを疑い調査することにしました。

Spacy の Vocab クラス

Vocab は Language クラスの要素です。 初期化の時点ではサイズは0です。

import spacy

nlp = spacy.load("ja_core_news_sm")

print(nlp.vocab, len(nlp.vocab))
>>> <spacy.vocab.Vocab object at 0xXXXX> 0

解析するとサイズが増えます。

Vocab には 語彙をkey にしたLexeme が格納されます。 このハッシュテーブルを利用し、処理を高速化しているようです。

doc = nlp("初期化の時点ではサイズは0です。")

print(len(nlp.vocab))
>>> 10

nlp.vocab["です"]
>>> <spacy.lexeme.Lexeme object at 0xXXXX>

下記ドキュメントの文言通り、 nlp.vocab は未知語が出現する限り増えます。

Note that a Vocab instance is not static. It increases in size as texts with new tokens are processed. Some models may have an empty vocab at initialization.

Memory Leak 問題

Vocab は少量のデータを分析する分には問題ないです。

しかし、Django などAPI上でモデルを動かし続けた場合、vocabは増大しメモリを圧迫します。 最悪の場合、メモリが溢れて API が落ちます。

Spacy の issue でも vocab が原因のメモリの増加に関する問題がいくつか上がっています。

今のところ vocab のテーブルを削除する機能はないです。(Vocab._reset_cacheはあるものの実装されていない)

おそらく一番上の issue の回答が今現在の最適解と思われます。

The recommended solution if the memory usage is a problem is to periodically reload the pipeline with spacy.load.

Spacy のメモリ消費量と実行時間

定期的に spacy.load する、といってもモデルを毎回ロードすると実行速度に影響が出そうに見えます。

特に API として運用したい場合、レスポンスが遅いのは困ります。

実行時間とメモリ消費量について簡単に調査しました。 Google Colab (CPU) 上で Spacy を実行します。

データはnltk_dataでダウンロードしたbrown corpusを使いました。 データ数が多いので500ファイルのうち100ファイルだけ使っています。

# ダウンロードは下記スクリプトを実行
# python -m nltk.downloader brown

from nltk.corpus import brown
len(brown.sents())
>> 57340

len(brown.fileids())
>> 500

def iter_file():
  """file ごと返却
  :return: List[str]
  """
  for fileid in brown.fileids()[:100]:
    sents = brown.sents(fileids=fileid)
    yield [" ".join(words) for words in sents]

実験コード

Spacy の実行条件とコードは以下です。 メモリの消費量の調査については tracemalloc のサンプルをそのまま使っています。

設定1: 同じモデルを使い続ける

%%time

tracemalloc.start()

snapshot1 = tracemalloc.take_snapshot()
snapshot1.dump("/content/same_model_per_file_1")
nlp = spacy.load("en_core_web_sm")
result = []
for sents in iter_file():
  for doc in nlp.pipe(sents):
    result.append(doc)

snapshot2 = tracemalloc.take_snapshot()
snapshot2.dump("/content/same_model_per_file_2")
top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("vocab {}".format(len(nlp.vocab)))
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
    print(stat)

設定2: ファイルごとモデルをリロードする

%%time
tracemalloc.start()

snapshot1 = tracemalloc.take_snapshot()
snapshot1.dump("/content/reload_model_per_file_1")
result = []
for sents in iter_file():
  nlp = spacy.load("en_core_web_sm")
  for doc in nlp.pipe(sents):
    result.append(doc)

snapshot2 = tracemalloc.take_snapshot()
snapshot2.dump("/content/reload_model_per_file_2")
top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("[ Top 10 differences ]")
for stat in top_stats[:10]:
    print(stat)

結果

※ 1回だけの結果なので参考程度に

設定1:同じモデルを使い続ける

Wall time: 1min 17s

と、実行時間が「ファイルごと」より少ないです。

モデルのロードは時間がかかることがわかります。

Vocab のサイズは 22695 まで増えました。

しかし、メモリの消費量で一番多いのは thinc という spacy が内部で使っているパッケージでした。

/usr/local/lib/python3.10/dist-packages/thinc/layers/residual.py:50: size=103 MiB (+103 MiB), count=200 (+200), average=527 KiB
/usr/local/lib/python3.10/dist-packages/spacy/language.py:1113: size=73.5 MiB (+73.5 MiB), count=299717 (+299717), average=257 B
/usr/local/lib/python3.10/dist-packages/spacy/language.py:2131: size=13.6 MiB (+13.6 MiB), count=233845 (+233845), average=61 B
/usr/local/lib/python3.10/dist-packages/thinc/model.py:673: size=12.4 MiB (+12.4 MiB), count=244 (+244), average=51.8 KiB

設定2: ファイルごとモデルをリロードする

Wall time: 10min 47s

と 「設定1」の10倍の実行時間がかかりました。

やはりモデルは毎回読み込むものではないです。

メモリの消費量も、最大のものが 1355 MiB とかなり多く、メモリリークが疑われます。 「設定1」でも 13.6 MiB と消費量が多い spacy/language.py:2131 です。

調べたところ language#from_disk の呼び出し部分のようです。

データ数が100なので、 13.6 MiB * 100 ≒ 1355 MiB とメモリが解放されてないことがわかります。

/usr/local/lib/python3.10/dist-packages/spacy/language.py:2131: size=1355 MiB (+1341 MiB), count=23383114 (+23149269), average=61 B
/usr/local/lib/python3.10/dist-packages/srsly/msgpack/__init__.py:79: size=117 MiB (+109 MiB), count=1973505 (+1843350), average=62 B
/usr/local/lib/python3.10/dist-packages/spacy/language.py:2141: size=75.7 MiB (+74.3 MiB), count=534481 (+517674), average=148 B
/usr/local/lib/python3.10/dist-packages/spacy/language.py:115: size=60.8 MiB (+60.1 MiB), count=521874 (+515909), average=122 B

続く

今回は Google Colab で tracemalloc を使って調べました。

API として使う場合は勝手が変わってくるので、実運用に近い設定でメモリの消費も調べます。

ひとまずの調査としての記録でした。