HuggingFace Accelerate 0.12 : Getting Started : クイックツアー

HuggingFace Accelerate 0.12 : Getting Started : クイックツアー (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 10/02/2022 (v0.12.0)

* 本ページは、HuggingFace Accelerate の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

  • 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
  • sales-info@classcat.com  ;  Web: www.classcat.com  ;   ClassCatJP

 

 

HuggingFace Accelerate 0.12 : Getting Started : クイックツアー

🤗 Accelerate の主要な機能と回避すべき落とし穴を見てみましょう。

 

主要な使用方法

貴方自身のスクリプトで 🤗 Accelerate を使用するためには、4 つのことを変更する必要があります :

  1. Accelerator 主要クラスをインポートして accelerator オブジェクトでインスタンス化します :
    from accelerate import Accelerator
    
    accelerator = Accelerator()
    

    これは訓練スクリプトでできる限り早期に起きるべきです、それは分散訓練に必要なものすべてを初期化するからです。貴方がいる環境の種類 (GPU を持つ単に 1 つのマシン, 複数の GPU を持つ 1 つのマシン、複数の GPU or TPU を持つ複数のマシン等) を示す必要はありません、ライブラリがこれを自動的に検出します。

  2. モデルと入力データに対する呼び出し .to(device) or .cuda() を削除します。accelerator オブジェクトがこれを処理し、それらすべてのオブジェクトを正しいデバイスに配置します。貴方が何をしているかわかっていれば、それらの .to(device) 呼び出しをそのままにできますが、accelerator オブジェクトにより提供されるデバイス : accelerator.device を使用する必要があります。
     
    自動デバイス配置を完全に無効にするには、Accelerator を初期化するとき device_placement=False を渡します。

    オブジェクトを手動で正しいデバイスに配置する場合、accelerator.device にモデルを置いた後に optimizer を作成することに注意してください、そうでないと訓練は TPU 上で失敗します。

  3. 訓練に関連するすべてのオブジェクト (optimizer, モデル, 訓練 dataloader, 学習率スケジューラ) を prepare() メソッドに渡します。これはすべてが訓練の準備が整ったことを確実にします。
    model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
        model, optimizer, train_dataloader, lr_scheduler
    )
    

    特に、訓練データローダは利用可能な GPU/TPU コアに渡りシャードされますので、各々は訓練データセットの異なる部分を見ることになります。また、データが同じようにシャッフルされることを確実にするため、すべてのプロセスのランダムな状態はデータローダを通して各イテレーションの最初に同期されます (shuffle=True か何らかのランダムサンプラーを使用することを決めた場合)。

    訓練の実際のバッチサイズは使用されるデバイス数にスクリプトで設定したバッチサイズを掛けた数になります : 例えば、訓練データローダを作成するときバッチサイズ 16 に設定された 4 GPU 上の訓練は 64 の実際のバッチサイズで訓練されます。

    別の方法として、Accelerator を作成して初期化するときにオプション split_batches=True を使用することもできます、その場合にはスクリプトを 1, 2, 4 or 64 GPU のいずれで実行しても、バッチサイズは常に同じままです。

    この命令は、訓練のためのすべてのオブジェクトが作成されたらすぐに、実際の訓練ループを開始する前に、実行する必要があります。

    スケジューラが各 optimizer ステップでステップされる必要があるとき、prepare() には学習率スケジューラだけを渡す必要があります。

    訓練データローダはこのメソッドを通り抜けるとき長さを変化させるかもしれません : X GPU で実行する場合、それは split_batches=True を設定しない限り X で除算された長さを持ちます (実際のバッチサイズは X で乗算されているため)。

    訓練データローダの長さを使用する命令 (例えば合計訓練ステップ数をロギングしたい場合) は parepare() への呼び出し後に行なう必要があります。

    データローダだけを prepare() に送ることは完全にできますが、モデルと optimizer を一緒に prepare() に送るのがベストです。

    検証データローダを prepare() に送ることを望むかどうかは、分散評価を実行したいかどうかに依存します (後述)。

  4. 行 loss.backward() を accelerator.backward(loss) で置き換えます。

And you’re all set! これらすべての変更によって、スクリプトはローカルマシンそしてマルチ GPU or TPU で実行されます!分散訓練を起動するためにお気に入りのツールを使用するか、🤗 Accelerate launcher を使用することができます。

 

分散評価

(訳注: 原文 参照)

 

分散スクリプトの起動

分散訓練を起動するために通常のコマンドを使用できます (torch.distributed.launch for PyTorch のような)、それらは 🤗 Accelerate と完全に互換です。ここでの唯一の注意点は、🤗 Accelerate はすべての有用な情報を決定するために環境を利用しますので、torch.distributed.launch はフラグ –use_env で使用する必要があることです。

🤗 Accelerate はまたすべてのランチャーを統一する CLI ツールも提供していますので、一つのコマンドを覚える必要があるだけです。それを使用するには、貴方のマシンで単に次を実行して :

accelerate config

問われる質問に答えるだけです。これは 🤗 Accelerate のために default_config.yaml ファイルをキャッシュフォルダにセーブします。そのキャシュフォルダは (優先度の高い順に) :

  • accelerate でサフィックスされる、環境変数 HF_HOME の項目。
  • それが存在しない場合、huggingface/accelerate でサフィックスされる環境変数 XDG_CACHE_HOME の項目。
  • これも存在しない場合、フォルダ ~/.cache/huggingface/accelerate

セーブしたいファイルの位置をフラグ –config_file で指定することもできます。

これが成されれば、次を実行することで貴方のセットアップのすべてが上手くいっているかテストできます :

accelerate test

これは分散環境をテストする短いスクリプトを起動します。それが上手く動作すれば、次のステップへの準備ができました!

前のステップで config ファイルの位置を指定した場合、それをここでも渡す必要があることに注意してください :

accelerate test --config_file path_to_config.yaml

これが終われば、スクリプトを次のコマンドで実行できます :

accelerate launch path_to_script.py --args_for_the_script

config ファイルをデフォルトでない場所にストアした場合、このようにしてそれをランチャーに示すことができます :

accelerate launch --config_file path_to_config.yaml path_to_script.py --args_for_the_script

config ファイルにより決定される任意の引数をオーバーライドすることもできます。渡すことができるパラメータの完全なリストを見るには、accelerate launch -h を実行してください。

スクリプトの起動についての詳細は Launch チュートリアル を確認してください。

 

ノートブックからの訓練の起動

Accelerate 0.3.0 では、ノートブックから訓練関数を起動する手助けをするために新しい notebook_launcher() が導入されました。このランチャーは Colab or Kaggle 上の TPU による訓練の起動、そして複数の GPU 上の訓練をサポートしています (ノートブックを実行しているマシンがそれらを持つ場合)。

ノートブックのセルで訓練 and/or 評価全体を担う関数を単に定義してから、以下のコードでセルを実行します :

from accelerate import notebook_launcher

notebook_launcher(training_function)

Accelerator オブジェクトは訓練関数の內部でだけ定義される必要があります。これは初期化がランチャー內だけで行われる必要があるからです。

詳細については Notebook Launcher チュートリアル を確認してください。

 

TPU 上の訓練

スクリプトを TPU で起動したい場合、知っておくべき幾つかの注意点があります。内部的には、TPU は訓練ステップ (forward パス, backward パスと optimizer ステップ) 內で発生するすべての演算のグラフを作成します。最適化のためのこのグラフの構築とコンパイルにある程度時間がかかるのが、訓練の最初のステップが常に非常に長い理由です。

良い知らせは、このコンパイルはキャッシュされますので 2 番目のステップとそれに続くすべては遥かに高速であることです。悪い知らせは、それは貴方のすべてのステップが正確に同じ演算である場合にだけそれが適用されることです、これは以下も意味しています :

  • すべてのバッチ內ですべてのテンソルが同じ長さを持つ
  • 静的なコードを持つ (i.e., ステップ毎に変更できる長さの for ループはない)

2 つのステップの間で上記のどれかが変化すれば新しいコンパイルのトリガーになります、これは再度長い時間がかかります。実際にはこれは以下を意味します、すべてのテンソルが同じ shape の入力であるように特別な注意を払う必要があり (従って NLP 問題に取り組んでいる場合例えば動的パディングはなし)、(LSTM のように) 入力に依存して異なる長さを持つ for ループを持つ層は使用するべきではありません、そうでなければ訓練は耐え難いほど遅いものになります。

TPU 用のスクリプトで特別な動作を導入するには、accelerator の distributed_type を確認できます :

from accelerate import DistributedType

if accelerator.distributed_type == DistributedType.TPU:
    # do something of static shape
else:
    # go crazy and be dynamic

NLP サンプル は動的パディングを持つ状況の例を紹介しています。

細心の注意を払うべき最後のことは : モデルが tied 重みを持つ場合 (埋め込み行列の重みをデコーダの重みと結び付けている言語モデルのような)、このモデルを TPU に移すと (貴方自身でかモデルを prepare() に渡した後で) この結び付きをブレイクします。後で重みを結び直す必要があります。Transformers レポジトリの run_clm_no_trainer スクリプトでこの例を見つけることができます。

TPU 上の訓練についての詳細は TPU チュートリアル を確認してください。

 

他の注意点

スクリプト変換で発生するかもしれない小さい問題のすべてとその解決方法をここでリストアップします。

 

1 つのプロセスだけでステートメントを実行する

命令の幾つかは与えられたサーバ上の 1 つのプロセスでのみ実行する必要があります : 例えばデータダウンロードや log ステートメントです。このためには、このようにステートメントを test 內にラップします :

if accelerator.is_local_main_process:
    # Is executed once per server

別の例はプログレスバーです : 出力で複数のプログレスバーを持つことを避けるため、ローカルのメインプロセス上でだけ一つを表示するべきです :

from tqdm.auto import tqdm

progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process)

local はマシン毎という意味です : 訓練を幾つかの GPU を装備した 2 つのサーバ上で実行している場合、命令はそれらのサーバの各々で一度実行されます。例えば、最終的なモデルを 🤗 モデルハブにアップロードするような、すべてのプロセスに対して 1 度だけ (そしてマシン毎ではない) 何かを実行する必要がある場合は、それをこのように test にラップします :

if accelerator.is_main_process:
    # Is executed once only

プリンティング・ステートメントについてはマシン毎に 1 度だけ実行されることを望むでしょう、print 関数を単に accelerator.print で置き換えることができます。

 

遅延実行 (= Defer execution)

通常のスクリプトを実行するとき、命令は順番に実行されます。スクリプトを幾つかの GPU 上に同時に配備するために 🤗 Accelerate を使用すると複雑さを導入します : 各プロセスがすべての命令を順番に実行する一方で、幾つかは他よりも速い場合があります。

ある命令を実行する前にすべてのプロセスが特定のポイントに到達するのを待つ必要があるかもしれません。例えば、すべてのプロセスが訓練を完了したことを確かめる前にモデルをセーブするべきではありません。このためには、コードで次の行を書くだけです :

accelerator.wait_for_everyone()

この命令は、他のすべてのプロセスがそのポイントに到達するまで、最初に到達したすべてのプロセスをブロックします (スクリプトを 1 つだけの GPU や CPU で実行する場合、これは何もしません)。

 

モデルのセーブ/ロード

訓練したモデルのセーブは少し調整が必要かもしれません : 最初にすべてのプロセスが上で示されたようなスクリプトのポイントに到達することを待つ必要があり、それかモデルをセーブする前にモデルを unwrap する必要があります。これは prepare() メソッドを通り抜けるとき、モデルは (分散訓練を扱う) より大きなモデル內に配置されているかもしれないためです。これは、予防措置なしにモデル状態辞書のセーブすることはその潜在的な追加の層を考慮に入れ、ベースモデルにロードし直すことができない重みという結果になることを意味します。

これが最初にモデルを unwrap することを勧める理由です。ここに例があります :

accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
accelerator.save(unwrapped_model.state_dict(), filename)

スクリプトがチェックポイントをロードするロジックを含む場合、unwrapped モデルで重みをロードすることも推奨します (これは、モデルを prepare() を通り抜けさせた後で load 関数を使用する場合にだけ有用です)。ここに例があります :

unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.load_state_dict(torch.load(filename))

すべてのモデルパラメータはテンソルへの参照ですので、これは重みをモデル內にロードすることに注意してください。

 

状態全体のセーブ/ロード

モデルを訓練するとき、同じスクリプトでリストアされる、モデル, optimizer, ランダム・ジェネレーター, そして潜在的に LR スケジューラの現在の状態をセーブすることを望むかもしれません。セーブ場所を単純に渡すだけで、それを行なうためにそれぞれ save_state()load_state() を使用できます。register_for_checkpointing() を通してストアされた他のステートフルな項目を登録した場合、それらもまたセーブされてロードされます。

register_for_checkpointing() に渡されたすべてのオブジェクトはストアされる load_state_dict と state_dict 関数を持たなければなりません。

 

勾配クリッピング

スクリプトで勾配クリッピングを使用している場合、torch.nn.utils.clip_grad_norm_ や torch.nn.utils.clip_grad_value_ への呼び出しをそれぞれ clipgrad_norm()clipgrad_value() で置き換える必要があります。

 

混合精度訓練

訓練を 🤗 Accelerate で混合精度で実行している場合、損失はモデル內で計算されてベストな結果を得ます (例えば Transformer モデルのように)。モデル外部のすべての計算は全精度で実行されます (これは損失計算に対して一般に貴方が望むものです、特に softmax を伴う場合)。けれども損失計算を accelerator.autocast コンテキストマネージャ內に配置したい場合があるかもしれません :

with accelerator.autocast():
    loss = complex_loss_function(outputs, target):

混合精度訓練のもう一つの注意点は、勾配は訓練の最初と時に訓練の間に幾つかの更新をスキップすることです : 動的損失スケーリング・ストラテジーにより、訓練中に勾配がオーバフローするポイントがあり、次のステップで再度この発生を回避するために損失スケーリング因子が減少させられます。

これは、更新がないとき学習率スケジューラを更新する場合があることを意味します、これは一般には問題ありませんが、非常に少ない訓練データをもつときやスケジューラの最初の学習率値が非常に重要な場合に影響があるかもしれません。この場合、このように optimizer ステップが行なわれないときに学習率スケジューラの更新をスキップできます :

if not accelerator.optimizer_step_was_skipped:
    lr_scheduler.step()

 

勾配の累積

勾配累積を実行するには accumulate() を使用して gradient_accumulation_steps を指定します。これはマルチデバイス訓練のとき、勾配が同期あるいは非同期されることを自動的に確実にし、ステップが実際に実行されるべきかを確認し、そして損失を自動スケールします :

accelerator = Accelerator(gradient_accumulation_steps=2)
model, optimizer, training_dataloader = accelerator.prepare(model, optimizer, training_dataloader)

for input, label in training_dataloader:
    with accelerator.accumulate(model):
        predictions = model(input)
        loss = loss_function(predictions, label)
        accelerator.backward(loss)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()

 

DeepSpeed

DeepSpeed サポートは実験的ですので、基礎 API は近い将来進化し、幾つかの僅かなブレイキングチェンジを行なうかもしれません。特に、🤗 Accelerate は貴方自身で書いた DeepSpeed config をまだサポートしていません、これは次のバージョンで追加されます。

notebook_launcher() は DeepSpeed 統合をまだサポートしていません。

 

內部メカニズム

內部的には、ライブラリは最初に環境を分析することで動作します、そこではスクリプトが起動されて、どの種類の分散セットアップが使用されているか、幾つの異なるプロセスが存在するか、そして現在のスクリプトはどのプロセスに在るかを決定します。それらすべての情報は ~AcceleratorState 內にストアされます。

このクラスは、最初に ~Accelerator をインスタンス化したときに初期化され、分散セットアップが必要とする特定の初期化を実行します。その状態は AcceleratorState のすべてのインスタンスに渡り一意に共有されます。

それから、prepare() を呼び出すとき、ライブラリは :

  • 分散セットアップのために適応されたコンテナにモデルをラップし、
  • optimizer を AcceleratedOptimizer にラップし、
  • DataLoaderShard でデータローダの新しいバージョンを作成します。

モデルと optimizer は単純なラッパーに単に配置される一方で、データローダは再作成されます。これは、PyTorch がひとたび作成されたデータローダの batch_sampler をユーザに変更させずに、ライブラリは、他の num_processes バッチを生成するのに batch_sampler を変更することにより、プロセス間のデータのシャーディングを処理するのが主な理由です。

DataLoaderShard は以下の機能を追加するために DataLoader をサブクラス化します :

  • それは各新しいイテレーションですべてのプロセスの適切なランダム数ジェネレータを同期し、(シャッフルのような) ランダム化がプロセスに渡り正確に同じ方法で行なわれることを確実にします。

  • それは (device_placement=True を選択していない限りは) バッチを生成する前に妥当なデバイスに配置します。

ランダム数ジェネレータの同期はデフォルトでは以下を同期します :

  • PyTorch >= 1.6 に対しては (PyTorch RandomSampler のような) 与えられたサンプラーの generator 属性
  • PyTorch <=1.5.1 のメインのランダム数ジェネレータ

メインの Accelerator の rng_types 引数で同期させるランダム数ジェネレータを選択できます。PyTorch >= 1.6 では、すべてのプロセスでメインのランダム数ジェネレータで同じシードを設定することを回避するためにローカルジェネレータに依存することを勧めます。

內部についての詳細は、Internals ページ を見てください。

 

以上