A4 Slurm クラスタで Qwen2 をトレーニングする

このチュートリアルでは、 Google Cloud上のマルチノード、マルチ GPU Slurm クラスタで大規模言語モデル(LLM)をトレーニングする方法について説明します。このチュートリアルで使用するモデルは、15 億のパラメータを持つ Qwen2 モデルに基づいています。Slurm クラスタは 2 つの a4-highgpu-8g 仮想マシン(VM)を使用し、各 VM には 8 個の NVIDIA B200 GPU が搭載されています。

このチュートリアルで説明する主なプロセスは次の 2 つです。

  1. Google Cloud Cluster Toolkit を使用して、本番環境グレードの高性能 Slurm クラスタをデプロイします。このデプロイの一環として、必要なソフトウェアがプリインストールされたカスタム VM イメージを作成します。また、共有 Filestore インスタンスを設定し、高速 RDMA ネットワーキングを構成します。
  2. クラスタをデプロイしたら、このチュートリアルに付属のスクリプト セットを使用して、分散事前トレーニング ジョブを実行します。このジョブは、Hugging Face Accelerate ライブラリを活用します。

このチュートリアルは、 Google Cloud に高性能 Slurm クラスタをデプロイして LLM をトレーニングすることに関心のある ML エンジニア、研究者、プラットフォーム管理者、オペレーター、データおよび AI スペシャリストを対象としています。

目標

  • Hugging Face を使用して Qwen2 モデルにアクセスする。
  • 環境を準備します。
  • 本番環境グレードの A4 Slurm クラスタを作成してデプロイします。
  • Accelerate ライブラリを使用して Qwen2 モデルをトレーニングします。
  • ジョブをモニタリングします。
  • クリーンアップする。

費用

このドキュメントでは、課金対象である次の Google Cloudコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。

新規の Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. Google Cloud アカウントにログインします。 Google Cloudを初めて使用する場合は、 アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud CLI をインストールします。

  3. 外部 ID プロバイダ(IdP)を使用している場合は、まず連携 ID を使用して gcloud CLI にログインする必要があります。

  4. gcloud CLI を初期化するには、次のコマンドを実行します。

    gcloud init
  5. Google Cloud プロジェクトを作成または選択します

    プロジェクトの選択または作成に必要なロール

    • プロジェクトを選択する: プロジェクトの選択に特定の IAM ロールは必要ありません。ロールが付与されているプロジェクトであれば、どのプロジェクトでも選択できます。
    • プロジェクトを作成する: プロジェクトを作成するには、resourcemanager.projects.create 権限を含むプロジェクト作成者ロール(roles/resourcemanager.projectCreator)が必要です。詳しくは、ロールを付与する方法をご覧ください。
    • Google Cloud プロジェクトを作成します。

      gcloud projects create PROJECT_ID

      PROJECT_ID は、作成する Google Cloud プロジェクトの名前に置き換えます。

    • 作成した Google Cloud プロジェクトを選択します。

      gcloud config set project PROJECT_ID

      PROJECT_ID は、 Google Cloud プロジェクトの名前に置き換えます。

  6. Google Cloud プロジェクトに対して課金が有効になっていることを確認します

  7. 必要な API を有効にします。

    API を有効にするために必要なロール

    API を有効にするには、serviceusage.services.enable 権限が必要です。プロジェクトを作成した場合は、オーナーロール(roles/owner)を介してこの権限がすでに付与されている可能性があります。それ以外の場合は、Service Usage 管理者ロール(roles/serviceusage.serviceUsageAdmin)を介してこの権限を取得できます。ロールを付与する方法をご覧ください。

    gcloud services enable compute.googleapis.com file.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com
  8. Google Cloud CLI をインストールします。

  9. 外部 ID プロバイダ(IdP)を使用している場合は、まず連携 ID を使用して gcloud CLI にログインする必要があります。

  10. gcloud CLI を初期化するには、次のコマンドを実行します。

    gcloud init
  11. Google Cloud プロジェクトを作成または選択します

    プロジェクトの選択または作成に必要なロール

    • プロジェクトを選択する: プロジェクトの選択に特定の IAM ロールは必要ありません。ロールが付与されているプロジェクトであれば、どのプロジェクトでも選択できます。
    • プロジェクトを作成する: プロジェクトを作成するには、resourcemanager.projects.create 権限を含むプロジェクト作成者ロール(roles/resourcemanager.projectCreator)が必要です。詳しくは、ロールを付与する方法をご覧ください。
    • Google Cloud プロジェクトを作成します。

      gcloud projects create PROJECT_ID

      PROJECT_ID は、作成する Google Cloud プロジェクトの名前に置き換えます。

    • 作成した Google Cloud プロジェクトを選択します。

      gcloud config set project PROJECT_ID

      PROJECT_ID は、 Google Cloud プロジェクトの名前に置き換えます。

  12. Google Cloud プロジェクトに対して課金が有効になっていることを確認します

  13. 必要な API を有効にします。

    API を有効にするために必要なロール

    API を有効にするには、serviceusage.services.enable 権限が必要です。プロジェクトを作成した場合は、オーナーロール(roles/owner)を介してこの権限がすでに付与されている可能性があります。それ以外の場合は、Service Usage 管理者ロール(roles/serviceusage.serviceUsageAdmin)を介してこの権限を取得できます。ロールを付与する方法をご覧ください。

    gcloud services enable compute.googleapis.com file.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com
  14. ユーザー アカウントにロールを付与します。次の IAM ロールごとに次のコマンドを 1 回実行します。 roles/compute.admin, roles/iam.serviceAccountUser, roles/file.editor, roles/storage.admin, roles/serviceusage.serviceUsageAdmin, roles/compute.osAdminLogin, roles/iap.tunnelResourceAccessor

    gcloud projects add-iam-policy-binding PROJECT_ID --member="user:USER_IDENTIFIER" --role=ROLE

    次のように置き換えます。

    • PROJECT_ID: プロジェクト ID。
    • USER_IDENTIFIER: ユーザー アカウントの識別子。例: myemail@example.com
    • ROLE: ユーザー アカウントに付与する IAM ロール。
  15. Google Cloud プロジェクトのデフォルトのサービス アカウントを有効にします。
    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --project=PROJECT_ID

    PROJECT_NUMBER は、使用するプロジェクト番号に置き換えます。プロジェクト番号を確認するには、 既存のプロジェクトを取得するをご覧ください。

  16. デフォルトのサービス アカウントに編集者ロール(roles/editor)を付与します。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
        --role=roles/editor
  17. ユーザー アカウントのローカル認証情報を作成します。
    gcloud auth application-default login
  18. プロジェクトで OS Login を有効にします。
    gcloud compute project-info add-metadata --metadata=enable-oslogin=TRUE
  19. Hugging Face アカウントにログインするか、アカウントを作成します

Hugging Face を使用して Qwen2 にアクセスする

Hugging Face を使用して Qwen2 にアクセスする手順は次のとおりです。

  1. Qwen 2 1.5B を使用するための同意契約に署名します

  2. read アクセス トークンを作成します

Cluster Toolkit をインストールする

Cluster Toolkit は、 Google Cloudへのハイ パフォーマンス コンピューティング(HPC)、AI、ML のワークロードのデプロイを簡素化するオープンソース ツールです。gcluster の使用とクラスタの管理の詳細については、Cluster Toolkit の概要をご覧ください。

  1. Cluster Toolkit のバージョンを準備します。

    export CLUSTER_TOOLKIT_TAG=v1.97.0
    
    # Detect OS (linux or mac)
    case "$(uname -s)" in
      Linux*)     OS="linux" ;;
      Darwin*)    OS="mac" ;;
      *)          echo "Error: Unsupported operating system: $(uname -s)" >&2; exit 1 ;;
    esac
    
    # Detect Architecture (amd64 or arm64)
    case "$(uname -m)" in
      x86_64)     ARCH="amd64" ;;
      aarch64|arm64) ARCH="arm64" ;;
      *)          echo "Error: Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
    esac
  2. リリースをダウンロードします。

    # Download and extract the platform-specific bundle
    curl -LO "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${CLUSTER_TOOLKIT_TAG}/gcluster_bundle_${OS}_${ARCH}.zip"
    unzip "gcluster_bundle_${OS}_${ARCH}.zip" -d cluster-toolkit/
    rm -f "gcluster_bundle_${OS}_${ARCH}.zip"
  3. gcluster パスを定義します。

    export CLUSTER_TOOLKIT_PATH="$(pwd)/cluster-toolkit"
    export PATH="${CLUSTER_TOOLKIT_PATH}:${PATH}"
    gcluster --version

環境を準備する

環境を準備する手順は次のとおりです。

  1. デフォルトの環境変数を設定します。

    export PROJECT_ID="YOUR_PROJECT_ID"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export ZONE="YOUR_ZONE"
    export REGION="YOUR_REGION"
    export RESERVATION_URL="RESERVATION_NAME"
    export GCS_BUCKET="YOUR_BUCKET_NAME"
    export HF_TOKEN="HUGGING_FACE_TOKEN"
    
    gcloud config set project "${PROJECT_ID}"
    gcloud config set billing/quota_project "${PROJECT_ID}"

    次のように置き換えます。

    • YOUR_PROJECT_ID: GKE クラスタを作成する Google Cloud プロジェクトの名前。
    • YOUR_CLUSTER_NAME: 作成する Slurm クラスタの名前。
    • YOUR_ZONE: 予約が存在するゾーン。
    • YOUR_REGION,: 予約が存在するリージョン。
    • RESERVATION_NAME: Slurm クラスタの作成に使用する予約の URL または名前。
    • YOUR_BUCKET_NAME: トレーニング チェックポイントの結果を保存するバケットの名前。作成する前に、バケット名の要件をよく理解しておいてください。
    • HUGGING_FACE_TOKEN: 前の手順で作成した Hugging Face トークン。
  2. Cloud Storage バケットを作成します。

    gcloud storage buckets create "gs://${GCS_BUCKET}" \
      --project="${PROJECT_ID}"

A4 Slurm クラスタを作成する

A4 Slurm クラスタを作成する手順は次のとおりです。

  1. a4high-slurm-deployment.yaml ファイルを作成します。

    MANIFEST_PATH="${CLUSTER_TOOLKIT_PATH}/examples/machine-learning/a4-highgpu-8g"
    cat <<EOF > "${MANIFEST_PATH}/a4high-slurm-deployment.yaml"
    terraform_backend_defaults:
      type: gcs
      configuration:
        bucket: ${GCS_BUCKET}
    
    vars:
      deployment_name: ${CLUSTER_NAME}
      project_id: ${PROJECT_ID}
      region: ${REGION}
      zone: ${ZONE}
      a4h_cluster_size: 2
      a4h_reservation_name: ${RESERVATION_URL}
    EOF
  2. Terraform マニフェストを作成します。

    gcluster create \
      -d "${MANIFEST_PATH}/a4high-slurm-deployment.yaml" \
      "${MANIFEST_PATH}/a4high-slurm-blueprint.yaml" \
      -o "${CLUSTER_NAME}"
  3. マニフェストにパッチを適用します。

    sed -i '/deletion_protection = {/,/}/ { s/enabled = true/enabled = false/; /reason  = "Avoid data loss"/d; }' "${CLUSTER_NAME}/${CLUSTER_NAME}/cluster-env/main.tf"
  4. クラスタをデプロイします。

    gcluster deploy "${CLUSTER_NAME}/${CLUSTER_NAME}" --auto-approve

    gcluster deploy コマンドは 2 フェーズのプロセスです。

    • 最初のフェーズでは、すべてのソフトウェアがプリインストールされたカスタム イメージがビルドされます。この処理には最大 50 分かかることがあります。

    • 第 2 フェーズでは、そのカスタム イメージを使用してクラスタをデプロイします。通常、このプロセスは最初のフェーズよりも短時間で完了します。

    第 1 フェーズは成功したが第 2 フェーズが失敗した場合は、第 1 フェーズをスキップして Slurm クラスタのデプロイを再試行できます。

    gcluster deploy "${CLUSTER_NAME}" --auto-approve --skip "image" -w

ワークロードを準備する

ワークロードを準備する手順は次のとおりです。

  1. ワークロード スクリプトを作成する

  2. スクリプトを Slurm クラスタにアップロードします

  3. Slurm クラスタに接続します

  4. フレームワークとツールをインストールします

ワークロード スクリプトを作成する

トレーニング ワークロードで使用するスクリプトを作成する手順は次のとおりです。

  1. Python 仮想環境を設定するには、次の内容で install_environment.sh ファイルを作成します。

    #!/bin/bash
    # This script should be run ONCE on the login node to set up the
    # shared Python virtual environment.
    
    set -e
    echo "--- Creating Python virtual environment in /home ---"
    python3 -m venv ~/.venv
    echo "--- Activating virtual environment ---"
    source ~/.venv/bin/activate
    
    echo "--- Installing build dependencies ---"
    pip install --upgrade pip wheel packaging
    
    echo "--- Installing PyTorch for CUDA 12.8 ---"
    pip install torch --index-url https://download.pytorch.org/whl/cu128
    
    echo "--- Installing application requirements ---"
    pip install -r requirements.txt
    
    echo "--- Environment setup complete. You can now submit jobs with sbatch. ---"
    
  2. ファインチューニング ジョブの構成を指定するには、次の内容を含む accelerate_config.yaml ファイルを作成します。

    compute_environment: "LOCAL_MACHINE"
    distributed_type: "FSDP"
    downcast_bf16: "no"
    fsdp_config:
      fsdp_auto_wrap_policy: "TRANSFORMER_BASED_WRAP"
      fsdp_backward_prefetch: "BACKWARD_PRE"
      fsdp_cpu_ram_efficient_loading: true
      fsdp_forward_prefetch: false
      fsdp_offload_params: false
      fsdp_sharding_strategy: "FULL_SHARD"
      fsdp_state_dict_type: "SHARDED_STATE_DICT"
      fsdp_transformer_layer_cls_to_wrap: "Qwen2DecoderLayer"
    machine_rank: 0
    main_training_function: "main"
    mixed_precision: "bf16"
    num_machines: 2
    num_processes: 16
    rdzv_backend: "static"
    same_network: true
    tpu_env: []
    use_cpu: false
  3. Slurm クラスタで実行するジョブのタスクを指定するには、次の内容を含む submit.slurm ファイルを作成します。

    #SBATCH --job-name=qwen2-pretrain-smollm-fineweb
    #SBATCH --nodes=2
    #SBATCH --ntasks-per-node=1  # 1 main srun task on the node that manages accelerate
    #SBATCH --gpus-per-node=8    # access to all 8 GPUs on the node
    #SBATCH --partition=a4high
    #SBATCH --output=logs/slurm-%j.out
    #SBATCH --error=logs/slurm-%j.err
    
    set -euo pipefail
    echo "--- Slurm Job Started ---"
    
    # --- STAGE 1: Setup environment and pre-process data on each node's local SSD ---
    srun --ntasks=$SLURM_NNODES --ntasks-per-node=1 --gpu-bind=none bash -c '
      set -e
      echo "Setting up local environment on $(hostname)..."
      LOCAL_VENV="/mnt/localssd/venv_job_${SLURM_JOB_ID}"
      LOCAL_CACHE="/mnt/localssd/hf_cache_job_${SLURM_JOB_ID}"
      PROCESSED_DATA_DIR="/mnt/localssd/processed_data_${SLURM_JOB_ID}"
      LOCAL_TMP="/mnt/localssd/tmp_job_${SLURM_JOB_ID}"
    
      rsync -a --info=progress2 ~/.venv/ ${LOCAL_VENV}/
      mkdir -p ${LOCAL_CACHE} ${PROCESSED_DATA_DIR} ${LOCAL_TMP}
    
      echo "Pre-processing data on $(hostname)..."
      source ${LOCAL_VENV}/bin/activate
      export TMPDIR="${LOCAL_TMP}"
      export TEMP="${LOCAL_TMP}"
      export TMP="${LOCAL_TMP}"
      export HF_HOME="${LOCAL_CACHE}"
      export HF_DATASETS_CACHE="${LOCAL_CACHE}"
      export HF_MODULES_CACHE="${LOCAL_CACHE}/modules"
      export HF_METRICS_CACHE="${LOCAL_CACHE}/metrics"
    
      python "${HOME}/preprocess_data.py" \
        --dataset_name "HuggingFaceFW/fineweb-edu" \
        --dataset_config "CC-MAIN-2024-10" \
        --tokenizer_id "Qwen/Qwen2-1.5B" \
        --max_seq_length 1024 \
        --output_path ${PROCESSED_DATA_DIR}
    
      echo "Setup on $(hostname) complete."
    '
    
    # --- STAGE 2: Run the Training Job using the Local Environment ---
    echo "--- Starting Training ---"
    
    LOCAL_VENV="/mnt/localssd/venv_job_${SLURM_JOB_ID}"
    PROCESSED_DATA_DIR="/mnt/localssd/processed_data_${SLURM_JOB_ID}"
    LOCAL_OUTPUT_DIR="/mnt/localssd/outputs_${SLURM_JOB_ID}"
    
    export MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)
    export MASTER_PORT=29505
    
    # Network and initialization debugging configurations
    export NCCL_DEBUG=INFO
    export NCCL_DEBUG_SUBSYS=INIT,COLL
    export TORCH_DISTRIBUTED_DEBUG=INFO
    export NCCL_IB_DISABLE=0
    
    # Launching with full GPU access enabled for accelerate
    srun --ntasks=$SLURM_NNODES --ntasks-per-node=1 --gpu-bind=none bash -c "
      mkdir -p ${LOCAL_OUTPUT_DIR}
      source ${LOCAL_VENV}/bin/activate
    
      # Reset the forced Slurm isolation to expose all 8 GPUs to the processes
      unset CUDA_VISIBLE_DEVICES
    
      # Retrieve the default network device.
      DETECTED_IFACE=\$(ip route show | grep default | awk '{print \$5}' | head -n 1)
      echo \"[INFO] Automatically detected cluster network interface: \$DETECTED_IFACE\"
    
      # Dynamic injection of the detected interface into the network configuration
      export NCCL_SOCKET_IFNAME=\"\${DETECTED_IFACE},gpu*\"
      export TP_SOCKET_IFNAME=\"\${DETECTED_IFACE}\"
      export GLOO_SOCKET_IFNAME=\"\${DETECTED_IFACE}\"
    
      accelerate launch \
        --config_file ~/accelerate_config.yaml \
        --num_machines \$SLURM_NNODES \
        --num_processes \$((SLURM_NNODES * 8)) \
        --machine_rank \$SLURM_NODEID \
        --main_process_ip \$MASTER_ADDR \
        --main_process_port \$MASTER_PORT \
        train.py \
          --model_config_id 'Qwen/Qwen2-1.5B' \
          --preprocessed_data_path ${PROCESSED_DATA_DIR} \
          --output_dir ${LOCAL_OUTPUT_DIR} \
          --per_device_train_batch_size 4 \
          --gradient_accumulation_steps 4 \
          --max_steps 10000 \
          --learning_rate 5e-5 \
          --save_strategy steps \
          --save_steps 500 \
          --logging_steps 1
    "
    
    # --- STAGE 3: Copy Final Model from Local SSD to Home Directory ---
    echo "--- Copying final model from local SSD to /home ---"
    mkdir -p ~/qwen2-from-scratch-on-smollm-fineweb/
    
    srun --nodes=1 --ntasks=1 --ntasks-per-node=1 bash -c "
      rsync -a --info=progress2 ${LOCAL_OUTPUT_DIR}/ ~/qwen2-from-scratch-on-smollm-fineweb/
    "
    
    echo "--- Slurm Job Finished ---"
  4. ファインチューニング ジョブの依存関係を指定するには、次の内容で requirements.txt ファイルを作成します。

    # Hugging Face Libraries (Pinned to recent, stable versions for reproducibility)
    transformers==4.53.3
    datasets==4.0.0
    accelerate==1.9.0
    evaluate==0.4.5
    bitsandbytes==0.46.1
    trl==0.19.1
    peft==0.16.0
    
    # Other dependencies
    tensorboard==2.20.0
    protobuf==6.31.1
    sentencepiece==0.2.0
    注: このチュートリアルでは、NVIDIA と Pytorch の依存関係の最新バージョンを使用していません。新しい依存関係が必要な場合は、NVIDIA のドキュメントPytorch のドキュメントをご覧ください。

  5. データセットをダウンロードしてトークン化し、トレーニングの準備ができた形式に前処理するには、次の内容の preprocess_data.py ファイルを作成します。

    import argparse
    from datasets import load_dataset
    from transformers import AutoTokenizer
    import os
    from itertools import chain
    
    def get_args():
       parser = argparse.ArgumentParser(description="Download and preprocess a dataset.")
       parser.add_argument("--dataset_name", type=str, required=True)
       parser.add_argument("--dataset_config", type=str, required=True)
       parser.add_argument("--tokenizer_id", type=str, required=True)
       parser.add_argument("--max_seq_length", type=int, required=True)
       parser.add_argument("--output_path", type=str, required=True, help="Path to save the processed dataset.")
       return parser.parse_args()
    
    def main():
       args = get_args()
    
       if os.path.exists(args.output_path) and os.listdir(args.output_path):
           print(f"Processed dataset already exists at {args.output_path}. Skipping.")
           return
    
       # 1. Load tokenizer
       tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_id)
    
       # 2. Load raw dataset
       print(f"Loading raw dataset {args.dataset_name}...")
       raw_dataset = load_dataset(args.dataset_name, name=args.dataset_config, split="train")
    
       # 3. Tokenize
       def tokenize_function(examples):
           return tokenizer(examples["text"])
    
       num_proc = os.cpu_count()
       print(f"Tokenizing dataset using {num_proc} processes...")
       print("Tokenizing dataset...")
       tokenized_dataset = raw_dataset.map(
           tokenize_function,
           batched=True,
           remove_columns=raw_dataset.column_names,
           desc="Running tokenizer on dataset",
           num_proc=num_proc,
       )
    
       # 4. Group texts
       def group_texts(examples):
           concatenated_examples = {k: list(chain.from_iterable(examples[k])) for k in examples.keys()}
           total_length = len(concatenated_examples[list(examples.keys())[0]])
           if total_length >= args.max_seq_length:
               total_length = (total_length // args.max_seq_length) * args.max_seq_length
           result = {
               k: [t[i : i + args.max_seq_length] for i in range(0, total_length, args.max_seq_length)]
               for k, t in concatenated_examples.items()
           }
           result["labels"] = result["input_ids"].copy()
           return result
    
       print("Grouping texts...")
       lm_dataset = tokenized_dataset.map(
           group_texts,
           batched=True,
           desc=f"Grouping texts in chunks of {args.max_seq_length}",
           num_proc=num_proc,
       )
    
       # 5. Save to disk
       print(f"Saving processed dataset to {args.output_path}...")
       lm_dataset.save_to_disk(args.output_path)
       print("Preprocessing complete.")
    
    if __name__ == "__main__":
       main()
  6. ジョブの手順を指定するには、次の内容を含む train.py ファイルを作成します。

    import torch
    import argparse
    from datasets import load_dataset, load_from_disk
    import os
    from transformers import (
        AutoConfig,
        AutoTokenizer,
        AutoModelForCausalLM,
        Trainer,
        TrainingArguments,
        DataCollatorForLanguageModeling,
    )
    from huggingface_hub import login
    
    def get_args():
        parser = argparse.ArgumentParser()
        parser.add_argument("--model_config_id", type=str, default="Qwen/Qwen2-1.5B", help="Hugging Face model config to use for architecture.")
        # Data arguments - used if preprocessed data is not available
        parser.add_argument("--dataset_name", type=str, default="HuggingFaceFW/fineweb-edu", help="Hugging Face dataset for pre-training.")
        parser.add_argument("--dataset_config", type=str, default="CC-MAIN-2024-10", help="Config for the fineweb-edu dataset, e.g., 'CC-MAIN-2024-10'.")
        parser.add_argument("--preprocessed_data_path", type=str, default=None, help="Path to a preprocessed dataset on disk. If provided, skips download and processing.")
        # General arguments
        parser.add_argument("--hf_token", type=str, default=None, help="Hugging Face token for private models/tokenizers")
        parser.add_argument("--output_dir", type=str, default="qwen2-from-scratch-on-smollm-fineweb", help="Directory to save model checkpoints")
    
        # TrainingArguments
        parser.add_argument("--max_seq_length", type=int, default=1024, help="Maximum sequence length")
        parser.add_argument("--num_train_epochs", type=int, default=1, help="Number of training epochs")
        parser.add_argument("--max_steps", type=int, default=-1, help="If set to a positive number, it overrides num_train_epochs.")
        parser.add_argument("--per_device_train_batch_size", type=int, default=4, help="Batch size per device during training")
        parser.add_argument("--gradient_accumulation_steps", type=int, default=4, help="Gradient accumulation steps")
        parser.add_argument("--learning_rate", type=float, default=5e-5, help="Learning rate")
        parser.add_argument("--logging_steps", type=int, default=10, help="Log every X steps")
        parser.add_argument("--save_strategy", type=str, default="steps", help="Checkpoint save strategy")
        parser.add_argument("--save_steps", type=int, default=500, help="Save checkpoint every X steps")
    
        return parser.parse_args()
    
    def main():
        args = get_args()
    
        # --- 1. Setup and Login ---
        if args.hf_token:
            login(args.hf_token)
    
        # --- 2. Load Tokenizer ---
        # We load the tokenizer from the specified config ID to ensure compatibility
        # with the model architecture (e.g., special tokens).
        tokenizer = AutoTokenizer.from_pretrained(args.model_config_id)
    
        # --- 3. Initialize Model from Scratch ---
        print(f"Initializing a new model from {args.model_config_id} configuration...")
        config = AutoConfig.from_pretrained(args.model_config_id)
        model = AutoModelForCausalLM.from_config(config)
    
        print(f"Model has {model.num_parameters():,} parameters.")
    
        # --- 4. Load or Create and prepare the training dataset ---
        if args.preprocessed_data_path and os.path.exists(args.preprocessed_data_path):
            print(f"Loading preprocessed dataset from {args.preprocessed_data_path}...")
    
            # Synchronization of distributed processes
            local_rank = int(os.environ.get("LOCAL_RANK", -1))
            if local_rank != -1:
                # Introducing a minimal time offset per GPU to avoid I/O collisions.
                import time
                time.sleep(local_rank * 0.2)
    
            lm_dataset = load_from_disk(args.preprocessed_data_path, keep_in_memory=False)
    
        else:
            print("No preprocessed dataset found, starting from raw data...")
            raw_dataset = load_dataset(args.dataset_name, name=args.dataset_config, split="train")
    
            # Tokenization function
            def tokenize_function(examples):
                return tokenizer(examples["text"])
    
            tokenized_dataset = raw_dataset.map(
                tokenize_function,
                batched=True,
                remove_columns=raw_dataset.column_names,
                desc="Running tokenizer on dataset",
            )
    
            # Main data processing function that will concatenate all texts from our dataset
            # and generate chunks of max_seq_length.
            def group_texts(examples):
                # Concatenate all texts.
                concatenated_examples = {k: [item for sublist in examples[k] for item in sublist] for k in examples.keys()}
                total_length = len(concatenated_examples[list(examples.keys())[0]])
                # We drop the small remainder.
                if total_length >= args.max_seq_length:
                    total_length = (total_length // args.max_seq_length) * args.max_seq_length
                # Split by chunks of max_len.
                result = {
                    k: [t[i : i + args.max_seq_length] for i in range(0, total_length, args.max_seq_length)]
                    for k, t in concatenated_examples.items()
                }
                result["labels"] = result["input_ids"].copy()
                return result
    
            lm_dataset = tokenized_dataset.map(
                group_texts,
                batched=True,
                desc=f"Grouping texts in chunks of {args.max_seq_length}",
            )
    
    
        # --- 5. Configure Training Arguments ---
        # Check for bfloat16 support
        use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported()
    
        training_args = TrainingArguments(
            output_dir=args.output_dir,
            num_train_epochs=args.num_train_epochs,
            max_steps=args.max_steps,
            per_device_train_batch_size=args.per_device_train_batch_size,
            gradient_accumulation_steps=args.gradient_accumulation_steps,
            learning_rate=args.learning_rate,
            logging_steps=args.logging_steps,
            save_strategy=args.save_strategy,
            save_steps=args.save_steps,
            save_total_limit=2, # Optional: Limit the number of checkpoints
            bf16=use_bf16,
            fp16=not use_bf16,
            optim="adamw_torch",
            lr_scheduler_type="cosine",
            warmup_ratio=0.03,
            report_to="tensorboard",
            gradient_checkpointing=True,
            # Required for gradient checkpointing with some parallelization strategies
            gradient_checkpointing_kwargs={"use_reentrant": False},
        )
    
        # --- 6. Create Trainer and Start Training ---
        # Data collator will take care of creating batches for causal language modeling
        data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
    
        trainer = Trainer(
            model=model,
            args=training_args,
            train_dataset=lm_dataset,
            # eval_dataset=... # Optional: if you have a validation set
            tokenizer=tokenizer,
            data_collator=data_collator,
        )
    
        print("Starting training from scratch...")
        trainer.train()
        print("Training finished.")
    
        # --- 7. Save the final model ---
        print(f"Saving final model to {args.output_dir}")
        trainer.save_model()
    
    if __name__ == "__main__":
        main()

スクリプトを Slurm クラスタにアップロードする

前のセクションで作成したスクリプトを Slurm クラスタにアップロードする手順は次のとおりです。

  1. クラスタのログインノードの名前を取得して、LOGIN_NODE 変数を設定します。

    LOGIN_NODE="$(gcloud compute instances list \
                    --project="${PROJECT_ID}" \
                    --filter="labels.ghpc_deployment='${CLUSTER_NAME}' AND labels.slurm_instance_role='login'" \
                    --format="value(name)" | head -n 1)"

    LOGIN_NODE 変数には ${CLUSTER_NAME}-login-001 と同様の値が格納されます。

  2. ファイアウォール ルールを作成します。

    gcloud compute firewall-rules create allow-ssh-ingress-from-iap \
      --project="${PROJECT_ID}" \
      --network="${CLUSTER_NETWORK}" \
      --direction=INGRESS \
      --action=allow \
      --rules=tcp:22 \
      --source-ranges=35.235.240.0/20 \
      --description="Allow SSH ingress from Google Cloud Identity-Aware Proxy (IAP)"
  3. スクリプトをログインノードのホーム ディレクトリにアップロードします。

    gcloud compute scp \
      --project="${PROJECT_ID}" \
      --zone="${ZONE}" \
      --tunnel-through-iap \
      ./install_environment.sh \
      ./requirements.txt \
      ./submit.slurm \
      ./accelerate_config.yaml \
      ./train.py \
      ./preprocess_data.py \
      "${LOGIN_NODE}":~/

Slurm クラスタに接続する

SSH を使用してログインノードに接続し、Slurm クラスタに接続します。

gcloud compute ssh "${LOGIN_NODE}" \
    --project="${PROJECT_ID}" \
    --tunnel-through-iap \
    --zone="${ZONE}"
    -- -t "export HF_TOKEN='${HF_TOKEN}'; bash -l"

フレームワークとツールをインストールする

ログインノードに接続したら、次の手順でフレームワークとツールをインストールします。

  1. 必要な依存関係をすべて含む Python 仮想環境を設定します。

    chmod +x install_environment.sh
    ./install_environment.sh

ワークロードの事前トレーニングを開始する

ワークロードのトレーニングを開始するには、次の操作を行います。

  1. Slurm スケジューラにジョブを送信します。

    sbatch submit.slurm
  2. Slurm クラスタのログインノードで、home ディレクトリに作成された出力ファイルを確認することで、ジョブの進行状況をモニタリングできます。

    tail -f ~/logs/slurm-1.err # (or .out, depending on where the script is currently sending logs)

    ジョブが正常に開始されると、.err ファイルにプログレスバーが表示され、ジョブの進行状況に応じて更新されます。

ワークロードをモニタリングする

Slurm クラスタでの GPU の使用状況をモニタリングして、ファインチューニング ジョブが効率的に実行されていることを確認できます。これを行うには、ブラウザで次のリンクを開きます。

https://console.cloud.google.com/monitoring/metrics-explorer?project=PROJECT_ID&pageState=%7B%22xyChart%22%3A%7B%22dataSets%22%3A%5B%7B%22timeSeriesFilter%22%3A%7B%22filter%22%3A%22metric.type%3D%5C%22agent.googleapis.com%2Fgpu%2Futilization%5C%22%20resource.type%3D%5C%22gce_instance%5C%22%22%2C%22perSeriesAligner%22%3A%22ALIGN_MEAN%22%7D%2C%22plotType%22%3A%22LINE%22%7D%5D%7D%7D

または、ターミナルにコマンドを直接入力することもできます。

open "https://console.cloud.google.com/monitoring/metrics-explorer?project=${PROJECT_ID}&pageState=%7B%22xyChart%22%3A%7B%22dataSets%22%3A%5B%7B%22timeSeriesFilter%22%3A%7B%22filter%22%3A%22metric.type%3D%5C%22agent.googleapis.com%2Fgpu%2Futilization%5C%22%20resource.type%3D%5C%22gce_instance%5C%22%22%2C%22perSeriesAligner%22%3A%22ALIGN_MEAN%22%7D%2C%22plotType%22%3A%22LINE%22%7D%5D%7D%7D"

ワークロードをモニタリングすると、次の情報を確認できます。

  • GPU 使用率: 正常なファインチューニング ジョブでは、トレーニング全体を通して、16 個の GPU(クラスタ内の各 VM に 8 個の GPU)の使用率が上昇し、特定のレベルで安定することが予想されます。テスト

  • ジョブの所要時間: ジョブが完了するまでに約 1 時間かかります。

モデルをダウンロードする

ジョブが正常に実行されると、トレーニング済みモデルがログインノードの ~/qwen2-from-scratch-on-smollm-fineweb/ ディレクトリに保存されます。この永続共有ディレクトリはクラスタ内のすべてのノードにマウントされるため、ジョブが完了した後やコンピューティング ノードが割り当て解除された後でも、モデル チェックポイントは使用可能です。

次の例に示すように、gcloud compute scp コマンドを使用して、保存したモデルをログインノードからローカルマシンにダウンロードできます。

# From your local machine
gcloud compute scp --project="${PROJECT_ID}" --zone="${ZONE}" --tunnel-through-iap \
  "${LOGIN_NODE}":~/qwen2-from-scratch-on-smollm-fineweb/ ./qwen2-trained-model/ --recurse

モデルをダウンロードしたら、次のことができます。

  • 推論用のモデルを読み込む: Hugging Face Transformers フレームワークを使用して qwen2-trained-model/ ディレクトリを読み込み、新しくトレーニングした Qwen2 モデルで推論を実行します。
  • 追加のファインチューニング: 保存したチェックポイントを、より具体的なデータセットに対する追加のファインチューニングの出発点として使用します。
  • モデルを Hugging Face Hub に push する: トレーニング済みモデルを Hugging Face Hub に push して共有します。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

リソースを削除する

  1. Slurm クラスタを削除する手順は次のとおりです。

    ./gcluster destroy "${CLUSTER_NAME}" --auto-approve
  2. Cloud Storage バケットを削除する手順は次のとおりです。

    gcloud storage buckets delete "gs://${GCS_BUCKET}" --quiet || true
  3. Packer イメージを削除するには、ウェブブラウザを開いて次のページに移動し、特定のイメージを検索して [削除] をクリックします。

    http://console.cloud.google.com/compute/images
  4. プロジェクトに関連付けられているすべての VPC ネットワーク、ファイアウォール ルール、ルーター、IP、サブネットを削除するには、次の操作を行います。

    echo "========================================================================="
    echo " STARTING AUTOMATED NETWORK CLEANUP FOR CLUSTER: ${CLUSTER_NAME}"
    echo "========================================================================="
    
    echo "Discovering all VPC networks linked to the cluster..."
    NETWORKS=$(gcloud compute networks list --project="${PROJECT_ID}" --format="value(name)" | grep "^${CLUSTER_NAME}" || true)
    
    if [ -z "${NETWORKS}" ]; then
        echo "No VPC networks found starting with ${CLUSTER_NAME}. Everything is already clean!"
        exit 0
    fi
    
    echo "Found the following networks to process:"
    echo "${NETWORKS}"
    echo "-------------------------------------------------------------------------"
    
    echo "=== 1. Wiping Global Firewall Rules ==="
    FIREWALL_RULES=$(gcloud compute firewall-rules list \
        --project="${PROJECT_ID}" \
        --filter="network ~ ^${CLUSTER_NAME} OR name ~ ^${CLUSTER_NAME}" \
        --format="value(name)" || echo "")
    
    if [ -n "${FIREWALL_RULES}" ]; then
        echo "Deleting matching firewall rules:"
        echo "${FIREWALL_RULES}"
        echo "${FIREWALL_RULES}" | xargs -r gcloud compute firewall-rules delete --project="${PROJECT_ID}" --quiet
    else
        echo "No matching firewall rules found."
    fi
    
    echo "=== 2. Tearing Down Network-Specific Infrastructure ==="
    echo "${NETWORKS}" | while read -r net_name; do
        [ -z "${net_name}" ] && continue
        echo "Processing resources for network: ${net_name}"
    
        ROUTERS=$(gcloud compute routers list \
            --project="${PROJECT_ID}" \
            --regions="${REGION}" \
            --filter="network=${net_name}" \
            --format="value(name)" || echo "")
    
        if [ -n "${ROUTERS}" ]; then
            echo "  -> Deleting routers: ${ROUTERS}"
            echo "${ROUTERS}" | xargs -r gcloud compute routers delete --region="${REGION}" --project="${PROJECT_ID}" --quiet
        fi
    
        IPS=$(gcloud compute addresses list \
            --project="${PROJECT_ID}" \
            --regions="${REGION}" \
            --filter="name ~ ^${net_name}" \
            --format="value(name)" || echo "")
    
        if [ -n "${IPS}" ]; then
            echo "  -> Deleting IP reservations: ${IPS}"
            echo "${IPS}" | xargs -r gcloud compute addresses delete --region="${REGION}" --project="${PROJECT_ID}" --quiet
        fi
    
        SUBNETS=$(gcloud compute networks subnets list \
            --project="${PROJECT_ID}" \
            --regions="${REGION}" \
            --filter="network=${net_name}" \
            --format="value(name)" || echo "")
    
        if [ -n "${SUBNETS}" ]; then
            echo "  -> Deleting subnetworks:"
            echo "${SUBNETS}"
            echo "${SUBNETS}" | xargs -r gcloud compute networks subnets delete --region="${REGION}" --project="${PROJECT_ID}" --quiet
        fi
    done
    
    echo "-------------------------------------------------------------------------"
    echo "Waiting 15 seconds for Google Cloud API dependencies to unlock..."
    sleep 15
    
    echo "=== 3. Final VPC Networks Destruction ==="
    echo "${NETWORKS}" | while read -r net_name; do
        [ -z "${net_name}" ] && continue
        echo "Deleting core VPC network: ${net_name}..."
        gcloud compute networks delete "${net_name}" --project="${PROJECT_ID}" --quiet || \
        echo "Warning: Could not delete ${net_name} yet. If a lock occurred, please rerun in 1 minute."
    done
    
    echo "========================================================================="
    echo " SUCCESS: All network resources for cluster ${CLUSTER_NAME} have been wiped!"
    echo "========================================================================="

プロジェクトの削除

Google Cloud プロジェクトを削除する:

gcloud projects delete PROJECT_ID

次のステップ