Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

Optimun NeuronでHuggingFace TrainerをAWS Neuron上で簡単に動かす

Optimun Neuron を使うと、HuggingFace TrainerをAWS Neuron上で気軽に扱えるようになります。具体的には以下のように、TrainerとTrainingArgumentsを置き換えるだけです。

from optimum.neuron import NeuronTrainer as Trainer
from optimum.neuron import NeuronTrainingArguments as TrainingArguments

こうするとTrn1上で、今までと同じコードで学習が可能です。AWS Neuronを使う場合はモデルのコンパイルが必要なのですが、これも自動で行われます。学習の間に動的にコンパイルを実行し、学習をしてくれます。この動的なコンパイルは長くて30分ほどかかりますが、学習自体が速いため総合的な時間は短縮されるでしょう。

作成されるモデルはコンパイル済みのものではありません。AWS Neuronというとそれ専用になるイメージがありますが、大丈夫です。

スクリプトの実行時は torchrun というコマンドを使います。

torchrun --nproc_per_node=2 train_model.py

公式のサンプルノートブックがおすすめ

こちらに公式で公開されているノートブックが用意されています。やりたいことに合わせて参考にするのがおすすめです!

github.com

ただNotebookであれこれ試すとなるとNeuronのコンパイルが頻繁に走って面倒な場合があります。個人的には手元のGPUなどでしっかりチューニングしてから、AWS Neuronで学習させるのが良いと思います。データセットの準備なども手元で済ませるほうが良いです(これについては別途記事に)。

SageMakerでは勝手にtorchrunをしてくれる

SageMakerでTrn1を使う場合は、自動で train_model.pytorchrun で実行してくれます。ですのでやることといえばインポートを切り替えるくらいなのでとても便利です。

モデルのコンパイル時間にどれくらいかかるか?などもモデルによりけりなので、ぜひ一度試してみると良いでしょう。インスタンスタイプを変えるだけで、勝手にSageMakerがよしなにやってくれます。

他にも知っておくと良いかも、ということ

AWS Neuronでの学習について詳しく知りたいとき

Optimun Neuronを使うと良い感じにラップされすぎていて、逆に何をしているのか分かりにくいときがあります。
pytorh-xlaを使ってNeuron上でTrainerを使った学習のサンプルがAWSの公式であるのでこちらもおすすめです。

aws.amazon.com

この記事、以前のインタビューでもご一緒した常世さんが書いてらっしゃいます。

trn1.32xlargeでマルチノードを使う場合は設定が必要

Trn1でマルチノードで学習させる際は、EFAの設定が必要になります。SecurityGroupで所属するマシン同士ですべてのInbound、Outboundを許可したり、専用のネットワークインタフェースを作成する必要があります。EC2の場合、手動で準備する必要がありかなり手間は大きいです。以下のドキュメントでコマンドによる作成方法などが書かれていますのでこちらを参考にしてください。

awsdocs-neuron.readthedocs-hosted.com

設定がうまくいっているかチェックするコマンドがとても役立ちます。ネットワークのPing Pongも確認してくれるので、SecurityGroupの設定が正しいかなども確認できます(実際にこのコマンドのおかげで間違いに気づいたこともありました)。

チェックポイントから再開させるとバグ

Optimun Neuronのバージョンによってはチェックポイントから再開する際にバグが発生する可能性があります。細かいことは次回の記事で記載いたします。

おわりに

AWS NeuronはTrnもInfも情報が少ないですが、公式できちんと整備されたドキュメントやサンプルはたくさんあります。ぜひお手元のモデルでの学習に活用してみてください。ShodoではAWS Neuronも最大限活用し、AIを使ったサービス運営を行っています。

現在、Shodoではアドベントカレンダー応援クーポンを配布しております。80%オフでShodoを最長3ヶ月間使えるクーポンです。以下のクーポンコードをご購入時に入力して、このアドベントカレンダーの季節にShodoのAI校正をブログの執筆にお役立てください。

XMAS2024

shodo.ink

執筆:@hirokiky
Shodoで執筆されました

SageMakerでのトレーニング時は事前にデータをエンコードしておく

SageMakerでトレーニングする際、強めのマシンを使う場合は事前にデータをエンコードしておくと良いです。データ量が多いと dataset.map(encoder) をするのも案外時間がかかります。そうするとGPUやTrnを有効活用していない時間も課金されてしまうので、事前にエンコードしたものを使いましょう。とくにSpotInstanceを使って繰り返しSageMakerのサーバーを起動する場合はやっておくことをおすすめします。

事前にデータをエンコードする

事前にエンコードするには、以下のように普段通りのエンコードし、 dataset.save_to_disk() を呼び出します。

dataset = load_dataset(...)
encoder = Encoder()
dataset = dataset.map(encoder)
dataset = dataset.remove_columns([...])
dataset.save_to_disk("data/mydataset_enc/")

読み込む際は load_from_disk() を使います。

from datasets import load_from_disk

load_from_disk("data/mydataset_enc/")

このフォルダーをS3にアップロードしておきましょう。

SageMakerでエンコードしたデータセットを使う

SageMakerでの学習を開始する際に、先ほどアップロードしたS3のパスを指定します。

from sagemaker.huggingface import HuggingFace

estimator = HuggingFace(
    entry_point="train_model.py",
)
estimator.fit({
    "train": "s3://my-dataset/mydataset_enc/",
})

このように指定するとSageMakerが実行前に自動でこのフォルダーをダウンロードしてくれます。
SageMakerの内部で実行する train_model.py では以下のようにファイルを読み込みましょう。

dataset = load_from_disk(os.environ["SM_CHANNEL_TRAIN"])

以上です!

これでSageMakerを起動するたびに大規模なデータセットエンコードを待つ必要がありません。

おわりに

Shodoでは独自のAIモデルを使ってAI校正のサービスを提供しています。

現在、Shodoではアドベントカレンダー応援クーポンを配布しております。80%オフでShodoを最長3ヶ月間使えるクーポンです。以下のクーポンコードをご購入時に入力して、このアドベントカレンダーの季節にShodoのAI校正をブログの執筆にお役立てください。

XMAS2024

shodo.ink

執筆:@hirokiky
Shodoで執筆されました

HuggingFace Trainerで学習とハイパーパラメーターチューニングを両立するプログラムを書く

HuggingFace Trainer を使っていて、トレーニング用のプログラムとハイパーパラメーターチューニング用のプログラムを両立させる方法を考えてみました。

HuggingFace Trainerでは trainer.hyperparameter_search(...) メソッドを使ってハイパーパラメーターのチューニングが可能です。たとえばOptunaを使う場合も backend="optuna" と指定するだけで済み、Pruneの設定なども簡単に行えます。

レーニングもハイパーパラメーターチューニングも Trainer(...) を使いますので、ある程度処理を共通化したいところです。ですがいくつか違う点があるのでその差分をうまく吸収するコードを書いていきましょう。

たとえばこんな点があります:

  • .hyperparameter_search() 時はモデルを model= で渡すのではなく、 model_init= に関数を渡す
  • ハイパーパラメーターチューニング時に保存やevalの実行頻度を変える
    • save_strategy="no" を指定してモデルを保存しない(学習時は save_strategy="epoch"
    • eval_strategy="epoch" を指定(学習時は eval_strategy="steps"

レーニング用とチューニング用で再利用する

以下のように、 Trainer を作る関数を用意します。この関数の結果に trainer.train_model() するか trainer.hyperparameter_search() を呼ぶことになります。

def model_init():
    return AutoModel.from_pretrained(...)


def trainer_builder(
    epochs,
    output_dir,
    learning_rate,
    model_init=False,
    **trainer_arguments,
):
    # データセットの用意
    dataset = load_dataset(...)

    train_data = ...
    val_data = ...

    if model_init:
        model_kwargs = {"model_init": model_init, "model": False}
    else:
        model_kwargs = {"model": model_init()}

    args = TrainerArguments(
        output_dir=output_dir,
        num_train_epochs=epochs,
        learning_rate=learning_rate,
        per_device_train_batch_size=batch_size,
        per_device_eval_batch_size=batch_size,
        **trainer_kwargs,
    )

    return Trainer(
        **model_kwargs,
        args=args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
    )

レーニング用のプログラムでは以下のように trainer_builder() を呼びだします。
コマンドの引数をうまく取ったり、そのまま受け渡したりしてハイパーパラメーターチューニング時との差分を埋めています。

@click.command()
@click.option("--epoch", type=int, default=32)
@click.option("--output_dir", default="output")
@click.option("--learning_rate", type=float, default=1e-05)
@click.option("--resume_ckpt", default=False, is_flag=True)
def main(resume_ckpt, **cmd_kwargs):
    trainer = trainer_builder(
        save_strategy="epoch",
        eval_strategy="steps",
        eval_steps=1000,
        **cmd_kwargs,
    )
    trainer.train(resume_from_checkpoint=resume_ckpt)

他にもコマンドの引数として受け取りたいパラメーターがあれば随時追加すればOKです。

ハイパーパラメーターチューニングは以下のようにします。

def hp(trial):
    return {
        "learning_rate": trial.suggest_float("learning_rate", 1e-05, 1e-04),
    }


def main():
    trainer = trainer_builder(
        epochs=10,
        output_dir="output",
        learning_rate=1e-05,  # どのみち上書きされる
        batch_size=32,
        model_init=True,
        save_strategy="no",
        eval_strategy="epoch",
        report_to=[],
    )
    result = trainer.hyperparameter_search(
        hp,
        direction="minimize",
        n_traials=10,
        backend="optuna",
        pruner=optuna.pruners.MedianPruner(),
    )
    pprint(result)

これでトレーニング用とチューニング用でコードをある程度共通化できます。

注釈

この書き方が万全という自信はなく、まだ実験中なので他に良い書き方があれば教えてください。たとえばTrainerを共通化するのではなく、データセットの準備を共通化すれば済むなどの考え方は他にもありそうです。また、トレーニング用のコードでは model_init の代わりに model 引数を使っていますが、 前者で済むのであれば切り替えは不要です(.hyperparameter_search() の場合は model_init が必須です)。

おわりに

Shodoではモデルのファインチューニングを行っており、自社でAIを運用しております。モデルのハイパーパラメーターチューニングや、継続的な開発をしやすいプログラムが重要になりますので、こういった工夫が重要になります。ぜひほかにも「こうしたほうが良いよ!」等のアドバイスがあればぜひ教えていただけると嬉しいです。

また現在、Shodoではアドベントカレンダー応援クーポンを配布しております。80%オフでShodoを最長3ヶ月間使えるクーポンです。以下のクーポンコードをご購入時に入力して、このアドベントカレンダーの季節にShodoのAI校正をブログの執筆にお役立てください。

XMAS2024

shodo.ink

執筆:@hirokiky
Shodoで執筆されました

SageMakerでのトレーニング実行前に FileNotFoundError: [Errno 2] No such file or directory: 'train'

SageMakerでトレーニング中に No such file or directory: 'train' というエラーが発生しました。

Traceback (most recent call last):
  File "/usr/local/bin/dockerd-entrypoint.py", line 28, in <module>
    subprocess.check_call(shlex.split(" ".join(sys.argv[1:])))
  File "/opt/conda/lib/python3.10/subprocess.py", line 364, in check_call
    retcode = call(*popenargs, **kwargs)
  File "/opt/conda/lib/python3.10/subprocess.py", line 345, in call
    with Popen(*popenargs, **kwargs) as p:
  File "/opt/conda/lib/python3.10/subprocess.py", line 971, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/opt/conda/lib/python3.10/subprocess.py", line 1863, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)

2024-11-01T14:50:11.898Z
FileNotFoundError: [Errno 2] No such file or directory: 'train'

見たところ私自身のプログラムが原因ではないようでした。
エラーが発生したときの実行ログはこちらです。

Starting
Preparing the instances for training
Downloading
Downloading the training image
Training
Training image download completed. Training in progress.
Uploading
Uploading generated training model
Failed
Training job failed

問題の原因と解決方法

SageMakerでトレーニング時に指定するDockerイメージに誤りがありました。
training 用のコンテナーを指定すべきところ、inference 用にしてしまっていました。
このトレーニングではDockerイメージを直接指定しています。

763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference-neuronx:2.1.2-neuronx-py310-sdk2.20.1-ubuntu20.04

763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training-neuronx:2.1.2-neuronx-py310-sdk2.20.1-ubuntu20.04

単なるうっかり間違いでしたが、エラーの特定には時間がかかりました。

問題発見の糸口

エラーの表示から自身のプログラムが起動されていないことは分かったので、インポート時のエラーかDockerの起動周りで問題があることは想像できました。そこでDockerのイメージから内容を探りました。

SageMakerで使われるDockerイメージは deep-learning-containersというリポジトリーで管理されています。検索するとエラーの根本になっていた dockerd-entrypoint.pyが見つかったので、さらにそれの呼び出し元を探したところ以下にありました。

Dockerfileのエントリーポイントとして設定されています。

ENTRYPOINT ["python", "/usr/local/bin/dockerd-entrypoint.py"]

https://github.com/aws/deep-learning-containers/blob/f97413f4b6833cea3e68ad3c7b9b67adcf72bc4a/huggingface/pytorch/inference/docker/1.13/py3/sdk2.14.1/Dockerfile.neuronx#L187

結果、CMDに渡される引数を確認する必要があると分かり、 sagemaker-training-toolkit というSageMaker側で動いているプログラムを検索しました。

docker run で検索すると以下のテストがあり、sagemaker-training-toolkit からDockerがこう呼ばれ、エラーが発生したと予想できました。

        command = (
            "docker run --name sagemaker-training-toolkit-test "
            "sagemaker-training-toolkit-test:dummy train"
        )

https://github.com/aws/sagemaker-training-toolkit/blob/54721512597e77530a68e14c35a857be2f5c5687/test/integration/local/test_dummy.py#L27

そこでDocker Imageで同じ動作(docker run ... image-name train)を再現しました。

docker pull 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference-neuronx:2.1.2-neuronx-py310-sdk2.20.1-ubuntu20.04
docker run --rm 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference-neuronx:2.1.2-neuronx-py310-sdk2.20.1-ubuntu20.04 train

すると同じエラーが発生しました。

Traceback (most recent call last):
  File "/usr/local/bin/dockerd-entrypoint.py", line 28, in <module>
    subprocess.check_call(shlex.split(" ".join(sys.argv[1:])))
  File "/opt/conda/lib/python3.10/subprocess.py", line 364, in check_call
    retcode = call(*popenargs, **kwargs)
  File "/opt/conda/lib/python3.10/subprocess.py", line 345, in call
    with Popen(*popenargs, **kwargs) as p:
  File "/opt/conda/lib/python3.10/subprocess.py", line 971, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/opt/conda/lib/python3.10/subprocess.py", line 1863, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'train'

これでDockerイメージの相性に間違いがあると分かったので、イメージを再確認したところ training 用ではなかったと分かりました。

おわりに

今回は単にうっかりミスでしたが、深いところまで調べる必要がありました。さすがのChatGPTもこの問題については解決できないようでしたし、少し勝った気持ちになり嬉しかったです。

SageMakerをフル活用しているとAWS提供のDeepLearningコンテナーやツールキットも知っていく必要があります。
かなり色々なところをケアしてくれる良いサービスですし拡張性もありますが、拡張やカスタムコンテナーなどの領域に進んでいく際は各リポジトリーについても知っておくことをおすすめします。

SageMakerとNeuron(inf1/inf2/trn1)については語り尽くせないのですが、ぜひ少しでも参考になれば幸いです。 昨今色々と騒がれていますが、これが本当のAIだよと伝えたいです。

執筆:@hirokiky
Shodoで執筆されました

PyCon JP 2024はディープなトークが多くて良かった #pyconjp2024

2024年9月27日、28日はPyCon JP 2024のカンファレンスデーでした。

https://2024.pycon.jp/

今回はトークの満足度が個人的にとても高かった。とくにコア開発、処理系、PEP Author(仕様策定に携わる人)のトークも充実しており、トークだけを見てもイベントとして贅沢なものだったと思います。

参加したトークはこのあたりでした:

  • キーノート(1日目)
  • Django Ninjaで高速なAPI開発を実現する: 実践ガイドとベストプラクティス
  • Crimes with the Python syntax
  • Extracting Structured Data from LLMs with LangChain and Pydantic
  • An overview of the optimisation pipeline in CPython 3.13 and onwards
  • Unlocking the Parallel Universe: Subinterpreters and Free-Threading in Python 3.13
  • 【招待講演】PythonUTF-8
  • Unlocking Python's Core Magic
  • CloudFlare Workers in Pythonでサーバーレスアプリケーションを作ろう
  • Sleuthing in Cython: Wrapping and Debugging Legacy C Libraries for Python
  • Playing games in the browser with WASM
  • The Wheelhouse of Horrors
  • Why Knowing Cython Helps in Understanding Python: A Deep Dive into Cython & PVM
  • プロダクションでのPython非同期ユースケース - Trio/Trio-Utilを中心に
  • キーノート(2日目)

すべては紹介しきれないので、印象に残った点だけここで挙げさせてください。

ディープなトークの数々

Python 3.13で入るCPythonの最適化の話、Wheelパッケージの構造と動作の話(どのようにしてsoファイルを同梱するのか)、言語としてUTF-8を標準にしていく過程と開発者内での議論の話が印象的でした。コア開発に関わる人たちによる直接の話であり、Pythonそのものについて「どうより良くしていくか」、「どうなっているのか」という内部的な話を聞けたのが貴重でした。

Python 3.13ではスタックマシンの最適化のため、ある動作が繰り返された場合にTier2というマシンでTier1内の処理を反復しないような制御について説明されており興味深かったです。

PythonエンコーディングUTF-8にしていく過程の話も、いかにコア開発者の中でコンセンサスを取り、後方互換性を担保しながら一つひとつ前に進めていくリアルな話が大変面白かったです。完璧な正解のない世界ですし、僕たちが知らないような動作を保証する必要があったりと難しいものだと感じました。それを進めてくれるmethaneさんに改めて感謝したいものです。ありがとうございます。

初日のキーノート:James Powell

また初日のキーノートスピーチにはとても感銘を受けました。結論としてはツールやライブラリー、言語の思想や構造を理解したうえでそれに沿った使い方をすべき、というものでした。pandas で金融のデータを扱うケースを参考にしつつ、「こういう使い方は間違いだよね」と紹介していくものでした。一番簡単な例を挙げると日時、銘柄、価格のカラムがあるのであれば、日時と銘柄をインデックスにして価格のみをカラムとすべきというものです。

ともかくとして単にツールを使うのではなく、そのツールのもつ思想や目的、内部的な構造を深く理解しようという啓蒙でした。データベースを使うにしても、DuckDBを使うにしても、何にしても「それがどういう内部構造でどういう意図のものか」を理解して適切に使おうというものです。ノウハウ、小手先のテクニックという話ではなく、プリンシパルに帰ろうという技術的な天啓を感じさせるものでした。

トークをたくさん聞けた

今回は私が登壇せず、スタッフでもスポンサーでもなかったので一人の参加者として改めて楽しめました。純粋に参加者として参加したのはPyCon JP 2011以来かもしれません(2012年にはJointイベントでDjango・Pyramidのスタッフでした)。

その中で本当に楽しめるトークが盛りだくさんで良かったです(もちろん上記した以外のトークも素晴らしいものばかり)。もうPythonは15年近くやっていますし、近年はPythonの用途も幅広くなっています。ですがその中でも新しい領域の話や、コア開発・ライブラリー開発に関わるディープなトークもたくさんあり堪能できました。

また、トークの後には質問ができたり、登壇者と直接話す機会があり現地に参加して良かったと思っています。子どもを連れての参加でしたので、イベントの前後の子守もあり正直疲れ果ててしまいましたが、全力で参加できて良かったです。

最後に、今の時代こそ

PyCon JP 2024はトークの内容としてもイベントとしても素晴らしいものでありました。スタッフの皆さまには感謝しております。

今の時代はイベント運営というのが簡単ではなくなったように思います。たくさんの人が関わっており、いろいろな意見、憶測、守るべきことなどが存在する世界です。それも大切なことですし、内省し改善すべきこともあるでしょう。ですがそういったリスクにさらされやすい運営の人たちが「じゃぁもう辞めようか」となってしまうのは悲しいものです。建設的な議論と、前向きな行動が、改めて周囲の僕たちを含めて必要なのかなと思います。

あとポジティブなフィードバックとかね。

今年のPyCon JP 2024はトークの内容もイベントも堪能でき、かつ技術的なディープさもちゃんとありとても良かったです。まぁ、僕のこの「良かったよ」というメッセージは大して多くの人にも見られず、炎上しやすいマイナスの意見が人の心に残りやすいものです。でもだからこそ、今回はきちんと感謝を伝えたいなと思いブログに残すことにしました。

ありがとうございました。

執筆:@hirokiky
Shodoで執筆されました

「ストーナー」を読み終えて

前回書いたように株主総会や決算が終わり、やっとまともな呼吸ができるようになって、ストーナーという小説を読んでいました。途中までほんの少しずつ読んでいたのを、精神的な落ち着きが得られた今になって、一気に読み終えることができました。

本の感想はあまり言いたくないというか、自分の中でもう少し大事にしたい何かがある状態です。なので人におすすめなどはしたくないものの、どんな感想でも見つけ出して自分のものにしてしまえる時代だからこそ、何かを書いておきたい気持ちになっています。おそらくは、読み終えた人の大半はこんな感情になるんじゃないでしょうか。

太鼓判を押したいものでもないですが、自分の中で大切にしたい何かがある本でした。小説(とくに純文学)を読むのも久しぶりといえばそうで、最近はエンジニアや経営者らしく技術書やビジネス書、会計の本などばかり読んでしまっていました。だからこそ、この本を読めて良かったと思いますし、たまたま妻と娘が家にいない貴重な自分の時間をこの本の最後に割けて良かったです。妻子をもつと一人の時間ってものすごく貴重なので、それを読書に使うのはほんとうに贅沢なことですからね。それでも良かったと思っています。

まぁ、これを感想とさせてください。

むしろ結婚し、子どもを育て、そして仕事を頑張るなかでこそ味わえた読了感なのかもしれません。読み終わったときは悲しみというか、何かを失った気持ちになりましたが、今はそれも含めて受容する精神的なプロセスのようなものを感じています。そういう本なんです。

こんな気持ちになった本はサン=テグジュペリの「人間の土地」以来かもしれません。

思えばここ数年は、あまりにも世俗的というか、目的をもって本を読むことや消化することに汚染されすぎていたなと思います(以前もそんな話をしましたが)。この本の冒頭から分かるように、劇的でなくヒーローも登場しなければ、快活でも知見を授けてくれる本でもありません。でもだからこそ、読む価値があったなと思える時間でした。そういえば、こんなにも純粋に活字そのものに心を惹かれたことって最近あったっけ?と思えるほどです。嘘みたいな話ですが、さっき出ていった妻がすぐ帰宅して「忘れ物でもあったかな」と思っていると、もう1時間近く経っていたということもありました。

今、僕の横には「肩をすくめるアトラス」があるのですが、また何か目的主義的というか何らかの悪癖に身を投じる予感がしています。今だけはもう少し、ありのままの世界に浸らせてもらいましょう。

執筆:@hirokiky
Shodoで執筆されました