🖥️

Tokyo 30の舞台裏?AWSで作る!フルマネージドな大規模GPUクラスターの構築/運用のリアル

に公開

はじめに

チューリングのMLOpsチームに所属する大戸(おおど)と言います。
2025年10月に入社し、クラウド上のGPUクラスターの構築・運用や、大量のデータセット管理システムの設計・開発など、MLOps領域の業務を幅広く担当しています。

今回は、実際に都内を30分程度走行させることに成功したモデルの開発を支えた GPUクラスター基盤 の話を書こうと思います。

https://zenn.dev/turing_motors/articles/bc6436727234ad

大規模なクラウドGPUクラスターの話を、構築から運用からクロージング(削除)まで、一貫してやったという記事は少ないと思ったので、書きました。

この記事で書いていること

本記事の概要を、エンジニア向けに3行でまとめると、下記になります。

  • Amazon SageMaker HyperPod+Slurm+Lustre を用いたクラウドGPUクラスターの 構築・リージョン移行・運用改善の実践例 を紹介
  • IaCの不完全さによる事故、コスト増大、NCCL timeout などの 運用トラブルとその対処 を具体的に振り返る
  • フルマネージド環境下での 観測性の限界と、改善に向けた設計判断(EFS廃止・GPU刷新検討) をまとめた

所属チームと筆者

チューリングのMLOpsチーム

なぜ、MLOpsチームがGPUクラスターを構築運用しているのか?と思う人もいると思います。
これはスタートアップという特性(ex. 人手不足のインフラチームを助ける!)に加え、もともとMLエンジニアとMLOps寄りのソフトウェアエンジニアが混在する「E2Eチーム」から独立した歴史があり、一般的に言われる「MLOpsの領域」よりも、幅広い領域を対応しています。

https://zenn.dev/turing_motors/articles/af10c5e32ea013


この筆者はどんな人?

SRE、AIスタートアップでの推論基盤開発、クラウドベンダーでのソリューションアーキテクト、オンプレ環境構築などを経験してきました。こうした背景から本プロジェクトを任されましたが、GPUクラスターの構築・運用は未経験で、ゼロからのスタートでした。


チューリングのGPUクラスター

チューリングでは、HPC(High Performance Computing:高性能計算)向けのジョブスケジューラである Slurm を運用しており 、ストレージは、HPC向けの分散ストレージとして広く使われている Lustre を利用しています。

私が入社した2025年10月時点では、下記の3つのSlurmクラスター環境がありました。

※現在、3 の環境は2026年1月上旬をもって停止/削除しています。

  1. Gaggle Cluster(Gaggle):自社オンプレミス環境に構築したもの
  2. GMO GPUクラウド(以下、GMO環境):GMOのクラウドサービス
  3. Amazon SageMaker HyperPod(以下、HyperPod):AWSフルマネージドなSlurmクラスターサービス

1、2 は CTO室のインフラチーム が構築・運用していますが、3 の環境はMLOpsチームが構築・運用していました。
これは、クラウド関係のリソース(ex. AWSやDatabricks)は、MLOpsが管理しているためです。


クラスターの移行

私がジョインして最初の仕事は、稼働中のHyperPodを停止・削除し、別リージョンで再構築する リージョン移行 でした。
具体的には、バージニア北部(us-east-1)からオレゴン(us-west-2)への移行です。

なぜ移行が必要か?

当初は、オレゴン(us-west-2)で構築始めたかったが、GPUリソースがないため、バージニア北部(us-east-1)に構築しました。
しかし、データセットがあるS3 bucketはオレゴンにあるため、データ転送料がかかるという問題がありました。
今回このタイミングで、オレゴンでのリソースが確保できたため、移行することになりました。

システム構成

ここでは、移行対象のクラスターに関して、説明します。

構成図

AWS公式記事とほぼ同様の構成です(図も記事のものを使用しています)。詳細は割愛しますが、構成上のポイントは次の3点です。

  1. 通信性能を最大化するため、HyperPodのWorkerノードとLustreを AZ-id単位で揃えて配置
  2. インスタンスの設定・起動処理は Lifecycle scriptとしてS3に配置
  3. インスタンスはAWS管理アカウントで起動されるが、SSM経由でログイン・操作が可能

https://aws.amazon.com/jp/blogs/machine-learning/introducing-amazon-sagemaker-hyperpod-to-train-foundation-models-at-scale/

構築管理に関して

当時は、MLOpsメンバーが作った AWS CDK(以下、CDK) と、インフラチームメンバーが作ったScript群(以下、Lifecycle script)があり、一部は自動化(例:VPC作成など)、一部は手動で構築されました。

  • CDK:AWS上のリソースをコード管理する
  • Lifecycle script:S3上に配置し、HyperPodのインスタンス起動時に読み込まれて、ストレージのマウントや必要な設定を行うScript群(shell / Python)

https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-lifecycle-best-practices-slurm.html

これらは、AWSが公開している以下のトレーニング資材を、チューリングの環境・用途に合わせてカスタマイズしたものです。

  • awsome-distributed-training(HyperPodのサンプル)

https://github.com/aws-samples/awsome-distributed-training/tree/main/1.architectures/5.sagemaker-hyperpod

なお、CDKは TypeScript で記述されています。チューリングではTerraformとCDKを併用しており、AWS側は主にCDKで管理されています。

移行の概要

システム構成は、概ね以下の通りで、削除/作成するリソースは次の通りです。

  • Amazon SageMaker HyperPod(コンピュート/ジョブスケジューラ)
  • Amazon FSx for Lustre(ストレージ)
  • Amazon S3(学習用データセット置き場)

以前との差分は、下記になります。

  • dataset bucketとのリンクを停止(DRA: Data Repository Association)
    • Lustreにデータがない場合にS3から取りにいく設定を使っていたが、1つのデータセットが大量の画像ファイル+メタデータで構成され、初回ダウンロードに時間がかかりすぎた
    • データセット作成後に自動ダウンロードする方式に移ったため、このリンクは停止した

https://docs.aws.amazon.com/ja_jp/fsx/latest/LustreGuide/create-dra-linked-data-repo.html

  • Amazon EFS(以下、EFS) を導入
    • 従来は /home/data が同一Lustre上にあり、Lustreの容量が逼迫するとSSHログインできなくなることがあった
    • そのため /home はEFSを利用する構成に変更した

https://aws.amazon.com/jp/efs/


事前準備

最初に取り掛かったのは、Lifecycle scriptにEFSマウント機能を追加することでした。
既存スクリプトを参考にしつつ、EFSに対応したスクリプトを新規に用意し、設定ファイルのオプションで「Lustreのみ」または「Lustre + EFS」の両方をマウントできるようにしました。

  • mount_efs.sh(抜粋)
# EFS Endpoints and versions
EFS_DNS_NAME="$1"
EFS_MOUNT_POINT="$2"
NFS_VERSION=4.1

mount_fs() {
    # Create mount point directory if it doesn't exist
    if [ ! -d "$EFS_MOUNT_POINT" ]; then
        mkdir -p "$EFS_MOUNT_POINT"
    fi

    retry_with_backoff $MAX_ATTEMPTS $INITIAL_BACKOFF \
      "ansible localhost -b -m ansible.posix.mount -a 'path=${EFS_MOUNT_POINT} src=${EFS_DNS_NAME}:/ fstype=nfs opts=nfsvers=${NFS_VERSION},_netdev,noresvport dump=0 passno=0 state=mounted'"
}
  • lifecycle_script.py(抜粋)
def main(args):
    params = ProvisioningParameters(args.provisioning_parameters)
    resource_config = ResourceConfig(args.resource_config)
    fsx_dns_name, fsx_mountname = params.fsx_settings
    efs_dns_name, efs_mountname = params.efs_settings

    if fsx_dns_name and fsx_mountname and efs_dns_name and efs_mountname:
        # NOTE: use Lustre /data and EFS /home
        print(f"Mount fsx: {fsx_dns_name}. Mount point: {fsx_mountname}")
        ExecuteBashScript("./mount_fsx.sh").run(fsx_dns_name, fsx_mountname, "/data")
        print(f"Mount efs: {efs_dns_name}. Mount point: {efs_mountname}")
        ExecuteBashScript("./mount_efs.sh").run(efs_dns_name, efs_mountname) ## ここで呼び出している

    elif fsx_dns_name and fsx_mountname:
        # NOTE: use only Lustre
        print(f"Mount fsx: {fsx_dns_name}. Mount point: {fsx_mountname}")
        ExecuteBashScript("./mount_fsx.sh").run(fsx_dns_name, f"{fsx_mountname}/data", "/data")
        print(f"Mount fsx: {fsx_dns_name}. Mount point: {fsx_mountname}")
        ExecuteBashScript("./mount_fsx.sh").run(fsx_dns_name, f"{fsx_mountname}/home", "/home")

構築/環境削除

構築時はインフラチームのメンバー、MLOpsメンバーと一緒に作業し、半日程度確保して新環境を構築しました。無事に起動し、問題なく動作しました。全社オールハンズでも入社紹介と成果報告を同時にできて、個人的にも嬉しいタイミングでした。

ストレージの誤削除

その後、旧環境を削除する際に、us-east-1のLustreを削除するつもりが、誤って移行後のus-west-2環境を削除してしまいました。

当時はCDK管理でしたが、リージョンをハードコードしていたり、EFS対応の差分が出ていたこともあり、AWSコンソールからCloudFormationを削除したところ、リージョンの見間違いで削除してしまいました。

またHyperPodクラスターも、Lustreを再作成するとマウントし直しが必要で、全ノード停止 → 再作成が必要になりました。そのためクラスターも作り直しました。

これまで大きなオペレーションミスがなかった分、この時は血の気が引きました。
ただ、Lustre上のデータが消えても元データはS3にあるため、データ消失には至りませんでした。一方で、時間をかけてダウンロードしたデータの再取得などで、MLエンジニアの皆さんの時間を奪ってしまいました。

結果として「自分で壊して自分で直す」形で復旧しましたが、次回以降こうした事故が起きないよう、より強固にIaC化する必要性を痛感し、作り込みを始めました。

より強固なIaC化へ

VPCやIAM Role、Lustreなど基盤部分はCDKで管理できていた一方で、以下がコード管理できていませんでした。

  • HyperPodクラスター
  • 認証用のLDAPサーバ
  • それに付随するリソース

そこで、CDK側を整備しつつ、環境ごとの設定差分はconfigファイルに閉じ込めることで、TerraformのmoduleのようにCDKを扱えるように修正し、複数環境をコードで管理できるようにしました。

ただしLifecycle script内で、作成したLustreの接続情報などを記載する必要があり、そこまでの完全自動化は当時の時間制約もあり実現できませんでした。
大まかなデプロイの流れは以下です
(1,2はCDKでコマンド一発、3は手動、4はGitHub Actionsで自動化、5はCDK)

  1. AWS上の必要リソース(例:VPC、Security Group、IAM Role)+LDAPサーバを構築
  2. Lustreを構築
  3. 2の情報をLifecycle scriptに反映
  4. Lifecycle scriptをS3へアップロード(mainマージ後にGitHub Actionsで自動化)
  5. HyperPodを構築

これにより、数時間程度(数コマンドと、いくつかのconfig追加で)で環境作成・削除ができるようになりました。

運用

運用していく中で、特にインパクト大きかったという事例を紹介します。

コストが想定の2倍に

移行後、GPUノード数は同じにもかかわらず、想定の約2倍のコストが発生していることを日次のコストモニタリングで検知しました。早期に異常を発見できた点は救いでした。

AWSに確認した結果

  • 割引対象のインスタンスなしで、クラスターを作成し、その後インスタンスを追加していた
    • Life Cycle Scriptで失敗すると、インスタンスが起動せずScriptの修正になるので、これを手早く行いたかったので、最小の構成でまず起動するようにしていた
  • 当時のHyperPod仕様では、クラスター作成時に割引対象のインスタンスが1台でも起動していないと割引が無効化され、オンデマンド扱いになること
  • そのため、割引対象のインスタンスの起動分(Resavationされており、止めて費用が発生しないようにするには事前にAWSへの申請が必要)に、オンデマンドでの利用料金が上乗せされ、想定の二倍程度かかっているように見えた
    • 該当のtypeのinstanceはどのように起動しても、割引が受けられるとの認識だったが、今回のケースは違った

以上により、コストが倍増していました。
対策として クラスターを再構築し、初回からGPUノードをまとめて起動する構成に変更した ことで問題は解消しました。
ちなみに、一度に契約した台数を全台起動すると失敗することが多いので、最初は60%くらい起動し、徐々に増やしていくと失敗しにくいです。

AWSの皆さまのおかげで原因究明でき、サービスチームとの調整を経て請求も補正され、結果として想定通りの利用料になりました。

学習が途中で止まってしまう

us-east-1からus-west-2への移行後、学習ジョブのハングやノード障害が頻発しました。自動運転の学習は長時間かつ大量データを扱うため、影響は大きい状況でした

以下は、同時刻に複数jobが落ちた際のエラーログの一部です(長大なため抜粋、識別子等はマスクしています)。

[rankXX]:[EXXXX ... ProcessGroupNCCL.cpp:607] [Rank X] Watchdog caught collective operation timeout: WorkNCCL(... Timeout(ms)=600000) ran for 6000XX milliseconds before timing out.
...

原因としてはネットワークやOOMなど複数の可能性が考えられましたが、当時は決定的な情報が得られず、AWS内部ネットワーク起因のように見える状況でした。
HyperPodはフルマネージドであるため、CloudWatchやメトリクスから詳細を追うことが難しく、根本原因の特定には至りませんでした。
また、非常に短期間で構築したため、さまざまなメトリクスを、AWS ManagedのPrometheus飛ばす機能は有効にしていませんでした。

https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-observability-addon.html

そのため、MLエンジニア側ではパラメータ調整やretry実装などで学習を継続しつつ、基盤側ではHyperPodのノード自動復旧機能を活用し、DOWNノードを検知して置き換える運用で対応しました。

#!/usr/bin/env bash
set -euo pipefail

TAG="mark_replace"
FAIL_STATE="fail"
FAIL_REASON="Action:Replace"
EXCLUDE_PATTERN="dev"

log() { logger -t "$TAG" "$*"; }

command -v sinfo >/dev/null 2>&1 || { log "ERROR: sinfo not found"; exit 127; }
command -v scontrol >/dev/null 2>&1 || { log "ERROR: scontrol not found"; exit 127; }

mapfile -t NODES < <(
  sinfo -N 2>&1 \
    | grep -v "$EXCLUDE_PATTERN" \
    | grep -i -w down \
    | awk '{print $1}' \
    | sed '/^$/d' \
    | sort -u
)

if [[ ${#NODES[@]} -eq 0 ]]; then
  log "INFO: No DOWN nodes (excluding pattern: $EXCLUDE_PATTERN)"
  exit 0
fi

log "INFO: Found DOWN nodes: ${NODES[*]}"

for node in "${NODES[@]}"; do
  out=""
  rc=0
  out="$(scontrol update nodename="$node" state="$FAIL_STATE" reason="$FAIL_REASON" 2>&1)" || rc=$?
  if [[ $rc -eq 0 ]]; then
    log "OK: update node=$node state=$FAIL_STATE reason=\"$FAIL_REASON\" output=\"$out\""
  else
    log "ERROR: update node=$node rc=$rc output=\"$out\""
  fi
done

ただし、自動では交換されないケースもあり、その場合は、手順はAWSのドキュメントを参考にしつつ、手動でノードを入れ替えていました。(IDやIPはマスクしています)

https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html

# instance id / ipのリストを取得(出力はマスク)
$ aws sagemaker list-cluster-nodes --cluster-name ${CLUSTER_NAME} ...
... > instance_ids_masked

# 使えないノードが共有される(例:内部ホスト名)
ip-10-1-XX-XXX, ip-10-1-XX-XXX, ...

# 対応するinstance idを抽出(マスク)
i-************ i-************ ...

# 実際に削除する(マスク)
$ aws sagemaker batch-delete-cluster-nodes --cluster-name ${CLUSTER_NAME} --node-ids i-************, i-************...

手動削除は確実に削除できますが、その分インスタンス数が減るため、後で台数を戻す必要があります。

振り返り

色々ありましたが、L4×64台のクラスターを「E2E スケールアップチーム」が粘り強く使い続けてくれて、2025年11月に「Tokyo 30」を達成できました。

https://www.youtube.com/watch?v=6SIk5REOZ4U

このモデルはHyperPod上で学習されました。他チームがオンプレミスを含む様々な環境で試行錯誤を重ねた成果の上に成り立ったものですが、その一部に入社直後から関われたことは印象的な経験でした。

一方で、Slurmや大規模分散学習の深い部分は未経験だったため、トラブル対応ではMLエンジニアやインフラエンジニアに頼る場面も多くありました。元SREとして十分に貢献しきれなかった点は反省点として残っています。

改善策

以下は、根本原因が特定できたわけではありませんが、「会社の行動理念」にある以下2点に合致するアクションとして紹介します。

  • マシュマロをさしてみよう
  • それでも前に進む

https://note.com/daisukeman/n/nf94de6a73c6e

Amazon EFSの利用停止

https://aws.amazon.com/jp/efs/

学習停止の要因として、移行前には利用していなかっEFSに着目し、「EFSを外せば改善するのではないか」という仮説で検証を行いました。環境はIaCで管理していたため、検証用クラスターの構築は比較的スムーズに行えました。

検証環境では学習停止は再現せず、EFSが直接の原因かどうかは断定できませんでした。ただし、移行前との大きな差分がEFS利用であったことから、今後は /home もLustreに載せ、/data とは別のLustreを使う構成に変更する判断をしました。これにより、過去に発生していた容量逼迫によるログイン不可の問題も回避できます。

また、HyperPodの公式ガイドにEFSの記載がない点も踏まえ、「技術的には動作するものの、想定されていない構成によるリスクがあるのではないか」という懸念もありました。

GPUインスタンスタイプの変更

これまではNVIDIA L4搭載の g6.48xlarge を使っていましたが、NVIDIA H200搭載の p5en.48xlarge を使いたいという話も出ました。

背景として、ログ上はネットワーク起因に見えていたこと、p5enファミリーはEC2 UltraClustersにデプロイされネットワーク面でも期待があること、そしてH200で台数を減らせればノード間通信量も減るはず、という点があります。
H200の確保についてはCTOおよびインフラチームが調整してくれることになりました。

P5/P5e/P5en は EC2 UltraClusters にデプロイされる

https://aws.amazon.com/jp/ec2/instance-types/p5/

EC2 UltraClusters は、特定のAZに共同配置され、ペタビット規模のノンブロッキングネットワークでEFAを使用して相互接続された、数千のアクセラレーテッドEC2インスタンスで構成されます。またFSx for Lustreにもアクセスできます。

https://aws.amazon.com/jp/ec2/ultraclusters/

次のクラスターでは、少なくとも より詳細なメトリクス/ログを取って分析できる状態 を作ること、Slurm/Lustreの知見を深めてより役に立てるようにすること、そして完全なIaCを目指すことを決めました。

最後に

本記事ではここまでとし、改善策を踏まえて進化したクラウドGPU基盤の詳細や、提供終了に至った背景については次回の記事で紹介します。

また、GPUクラスターに関心のある方向けにインフラチーム主催のTechtalkも予定(2026年2月5日)しています。大規模なGPUクラスター構築に携われるのは、非常に貴重な経験になるので、ぜひ聞いてください。
そして、もしよければ私たちの仲間になってくれると嬉しいです。

https://turing.connpass.com/event/383052/

MLOpsで働く仲間を募集

MLOpsチームでは、学習サイクルの改善や品質向上を通じて、自動運転L5の実現を目指す仲間を募集しています。チームにはWebフロント、MLエンジニア、プロダクト開発出身のソフトウェアエンジニアなど、様々なバックグラウンドのメンバーが在籍しています。

ドメインは広いですが、すべてを一人で担う必要はありません。自身の強みを活かしつつ、互いに補い合いながらチームとプロダクトを前に進めていける方をお待ちしています。

https://herp.careers/v1/turing/Qc3t_q0FYFq_

長い文章ですが、最後まで読んでいただきありがとうございました。

Tech Blog - Turing

Discussion