Addestra Qwen2 su un cluster Slurm A4

Questo tutorial mostra come addestrare un modello linguistico di grandi dimensioni (LLM) su un cluster Slurm multi-nodo e multi-GPU su Google Cloud. Il modello che utilizzi in questo tutorial si basa su un modello Qwen2 con 1,5 miliardi di parametri. Il cluster Slurm utilizza due macchine virtuali (VM) a4-highgpu-8g, ognuna delle quali dispone di 8 GPU NVIDIA B200.

Le due procedure principali descritte in questo tutorial sono le seguenti:

  1. Esegui il deployment di un cluster Slurm ad alte prestazioni di livello di produzione utilizzando Google Cloud Cluster Toolkit. Nell'ambito di questo deployment, crei un'immagine VM personalizzata con il software necessario preinstallato. Configura anche un'istanza Filestore condivisa e configura il networking RDMA ad alta velocità.
  2. Dopo il deployment del cluster, esegui un job di pre-addestramento distribuito utilizzando il set di script che accompagnano questo tutorial. Il job utilizza la libreria Hugging Face Accelerate.

Questo tutorial è rivolto a ingegneri, ricercatori, amministratori e operatori di piattaforme di machine learning (ML) e a specialisti di dati e AI interessati al deployment di cluster Slurm ad alte prestazioni su Google Cloud per addestrare LLM.

Obiettivi

  • Accedi al modello Qwen2 utilizzando Hugging Face.
  • Prepara l'ambiente.
  • Crea ed esegui il deployment di un cluster Slurm A4 di livello di produzione.
  • Addestra il modello Qwen2 utilizzando la libreria Accelerate .
  • Monitorare il job.
  • Eseguire la pulizia.

Costi

In questo documento vengono utilizzati i seguenti componenti fatturabili di Google Cloud:

Per generare una stima dei costi in base all'utilizzo previsto, utilizza il calcolatore prezzi.

I nuovi utenti di Google Cloud potrebbero avere diritto a una prova senza costi.

Prima di iniziare

  1. Installa Google Cloud CLI.

  2. Configura gcloud CLI per utilizzare la tua identità federata.

    Per ulteriori informazioni, vedi Accedi a gcloud CLI con la tua identità federata.

  3. Per inizializzare gcloud CLI, esegui questo comando:

    gcloud init
  4. Crea o seleziona un Google Cloud progetto.

    Ruoli richiesti per selezionare o creare un progetto

    • Seleziona un progetto: la selezione di un progetto non richiede un ruolo IAM specifico. Puoi selezionare qualsiasi progetto per cui ti è stato concesso un ruolo.
    • Crea un progetto: per creare un progetto, devi disporre del ruolo Autore progetto (roles/resourcemanager.projectCreator), che contiene l'autorizzazione resourcemanager.projects.create. Scopri come concedere i ruoli.
    • Creare un progetto Google Cloud :

      gcloud projects create PROJECT_ID

      Sostituisci PROJECT_ID con un nome per il progetto Google Cloud che stai creando.

    • Seleziona il progetto Google Cloud che hai creato:

      gcloud config set project PROJECT_ID

      Sostituisci PROJECT_ID con il nome del progetto Google Cloud .

  5. Verifica che la fatturazione sia attivata per il tuo progetto Google Cloud .

  6. Abilita l'API richiesta:

    Ruoli richiesti per abilitare le API

    Per abilitare le API, devi disporre dell'autorizzazione serviceusage.services.enable. Se hai creato il progetto, probabilmente disponi già di questa autorizzazione tramite il ruolo Proprietario (roles/owner). In caso contrario, puoi ottenere questa autorizzazione tramite il ruolo Amministratore utilizzo dei servizi (roles/serviceusage.serviceUsageAdmin). Scopri come concedere i ruoli.

    gcloud services enable compute.googleapis.com file.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com
  7. Concedi ruoli al tuo account utente. Esegui il seguente comando una volta per ciascuno dei seguenti ruoli IAM: 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

    Sostituisci quanto segue:

  8. Abilita il account di servizio predefinito per il tuo progetto Google Cloud :
    gcloud iam service-accounts enable PROJECT_NUMBER-compute@ \
        --project=PROJECT_ID

    Sostituisci PROJECT_NUMBER con il numero del progetto. Per rivedere il numero del progetto, consulta Ottenere un progetto esistente.

  9. Concedi il ruolo Editor (roles/editor) al service account predefinito:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="serviceAccount:PROJECT_NUMBER-compute@" \
        --role=roles/editor
  10. Crea le credenziali di autenticazione locale per il tuo account utente:
    gcloud auth application-default login
  11. Attiva OS Login per il tuo progetto:
    gcloud compute project-info add-metadata --metadata=enable-oslogin=TRUE
  12. Accedi o crea un account Hugging Face.

Accedere a Qwen2 utilizzando Hugging Face

Per utilizzare Hugging Face per accedere a Qwen2:

  1. Firma il contratto di consenso per utilizzare Qwen 2 1.5B.

  2. Crea un token di accesso read.

Installa Cluster Toolkit

Cluster Toolkit è uno strumento open source che semplifica il deployment di carichi di lavoro di computing ad alte prestazioni (HPC), intelligenza artificiale (AI) e machine learning (ML) su Google Cloud. Per saperne di più sull'utilizzo di gcluster e sulla gestione dei cluster, consulta la panoramica di Cluster Toolkit.

  1. Prepara la versione di 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. Scarica la release:

    # 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. Definisci il percorso gcluster:

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

prepara l'ambiente

Per preparare l'ambiente:

  1. Imposta le variabili di ambiente predefinite:

    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_GCS_BUCKET"
    export HF_TOKEN="YOUR_HF_TOKEN"
    
    gcloud config set project "${PROJECT_ID}"
    gcloud config set billing/quota_project "${PROJECT_ID}"

    Sostituisci quanto segue:

    • YOUR_PROJECT_ID: il nome del Google Cloud progetto in cui vuoi creare il cluster GKE.
    • YOUR_CLUSTER_NAME: il nome del cluster Slurm che vuoi creare.
    • YOUR_ZONE: la zona in cui esiste la prenotazione.
    • YOUR_REGION,: la regione in cui esiste la prenotazione.
    • RESERVATION_NAME: l'URL o il nome della prenotazione che vuoi utilizzare per creare il cluster Slurm.
    • YOUR_GCS_BUCKET: il nome del bucket in cui memorizzi i risultati del checkpoint di addestramento. Prima di crearlo, acquisisci familiarità con i requisiti per la denominazione dei bucket.
    • YOUR_HF_TOKEN: il token Hugging Face creato in un passaggio precedente.
  2. Crea un bucket Cloud Storage:

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

Crea un cluster Slurm A4

Per creare un cluster Slurm A4:

  1. Crea il file 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. Crea i manifest Terraform:

    gcluster create \
      -d "${MANIFEST_PATH}/a4high-slurm-deployment.yaml" \
      "${MANIFEST_PATH}/a4high-slurm-blueprint.yaml" \
      -o "${CLUSTER_NAME}"
  3. Applica patch ai manifest:

    sed -i '/deletion_protection = {/,/}/ { s/enabled = true/enabled = false/; /reason  = "Avoid data loss"/d; }' "${CLUSTER_NAME}/${CLUSTER_NAME}/cluster-env/main.tf"
  4. Esegui il deployment del cluster:

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

    Il comando gcluster deploy è un processo in due fasi, che è il seguente:

    • La prima fase crea un'immagine personalizzata con tutto il software preinstallato, il cui completamento può richiedere fino a 50 minuti.

    • La seconda fase esegue il deployment del cluster utilizzando l'immagine personalizzata. Il completamento di questo processo richiede in genere meno tempo rispetto alla prima fase.

    Se la prima fase va a buon fine, ma la seconda no, puoi provare a eseguire nuovamente il deployment del cluster Slurm saltando la prima fase:

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

Prepara il workload

Per preparare il workload:

  1. Crea script di workload.

  2. Carica gli script nel cluster Slurm.

  3. Connettiti al cluster Slurm.

  4. Installa framework e strumenti.

Crea script del workload

Per creare gli script che verranno utilizzati dal carico di lavoro di addestramento:

  1. Per configurare l'ambiente virtuale Python, crea il file install_environment.sh con i seguenti contenuti:

    #!/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. Per specificare le configurazioni per il job di perfezionamento, crea il file accelerate_config.yaml con i seguenti contenuti:

    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. Per specificare le attività da eseguire nei job sul cluster Slurm, crea il file submit.slurm con il seguente contenuto:

    #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. Per specificare le dipendenze per il job di perfezionamento, crea un file requirements.txt con i seguenti contenuti:

    # 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
    Nota: questo tutorial non utilizza le versioni più recenti delle dipendenze NVIDIA e Pytorch. Se hai bisogno di dipendenze più recenti, consulta la documentazione NVIDIA e la documentazione di Pytorch.

  5. Per scaricare, tokenizzare ed elaborare il set di dati in un formato pronto per l'addestramento, crea un file preprocess_data.py con i seguenti contenuti:

    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. Per specificare le istruzioni per il tuo job, crea un file train.py con il seguente contenuto:

    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()

Carica gli script nel cluster Slurm

Per caricare gli script creati nella sezione precedente nel cluster Slurm, segui questi passaggi:

  1. Imposta la variabile LOGIN_NODE recuperando il nome del nodo di accesso per il cluster:

    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)"

    La variabile LOGIN_NODE memorizza un valore simile a ${CLUSTER_NAME}-login-001.

  2. Crea una regola firewall:

    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. Carica gli script nella home directory del nodo di accesso:

    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}":~/

Connettiti al cluster Slurm

Connettiti al cluster Slurm connettendoti al nodo di accesso tramite SSH:

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

Installare framework e strumenti

Dopo aver eseguito la connessione al nodo di accesso, installa framework e strumenti nel seguente modo:

  1. Configura un ambiente virtuale Python con tutte le dipendenze richieste:

    chmod +x install_environment.sh
    ./install_environment.sh

Inizia il pre-training del tuo workload

Per iniziare l'addestramento del workload:

  1. Invia il job allo scheduler Slurm:

    sbatch submit.slurm
  2. Sul nodo di accesso del cluster Slurm, puoi monitorare l'avanzamento del job controllando i file di output creati nella directory home:

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

    Se il job viene avviato correttamente, il file .err mostra una barra di avanzamento che si aggiorna man mano che il job procede.

Monitorare il workload

Puoi monitorare l'utilizzo delle GPU nel cluster Slurm per verificare che il job di ottimizzazione venga eseguito in modo efficiente. Per farlo, apri il seguente link nel browser:

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

In alternativa, puoi inserire il comando direttamente nel terminale:

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"

Quando monitori il carico di lavoro, puoi visualizzare quanto segue:

  • Utilizzo delle GPU: per un job di messa a punto ottimale, puoi aspettarti di vedere l'utilizzo di tutte le 16 GPU (8 GPU per ogni VM nel cluster) aumentare e stabilizzarsi a un livello specifico durante l'addestramento. TEST

  • Durata del job: il completamento del job dovrebbe richiedere circa un'ora.

Scaricare il modello

Dopo aver eseguito correttamente il job, il modello addestrato viene salvato nella directory ~/qwen2-from-scratch-on-smollm-fineweb/ sul nodo di accesso. Poiché questa directory condivisa permanente è montata su tutti i nodi del cluster, i checkpoint del modello rimangono disponibili anche dopo il completamento del job o la deallocazione dei nodi di calcolo.

Puoi scaricare il modello salvato dal nodo di accesso alla tua macchina locale utilizzando il comando gcloud compute scp, come mostrato nell'esempio seguente:

# 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

Dopo aver scaricato il modello, puoi:

  • Carica il modello per l'inferenza: utilizza il framework Hugging Face Transformers per caricare la directory qwen2-trained-model/ ed eseguire l'inferenza con il modello Qwen2 appena addestrato.
  • Ottimizzazione aggiuntiva: utilizza il checkpoint salvato come punto di partenza per un'ottimizzazione aggiuntiva su un set di dati più specifico.
  • Trasferisci il modello a Hugging Face Hub: condividi il modello addestrato trasferendolo a Hugging Face Hub.

Esegui la pulizia

Per evitare che al tuo account Google Cloud vengano addebitati costi relativi alle risorse utilizzate in questo tutorial, elimina il progetto che contiene le risorse oppure mantieni il progetto ed elimina le singole risorse.

Eliminare le risorse

  1. Per eliminare il cluster Slurm:

    ./gcluster destroy "${CLUSTER_NAME}" --auto-approve
  2. Per eliminare il bucket Cloud Storage:

    gcloud storage buckets delete "gs://${GCS_BUCKET}" --quiet || true
  3. Per eliminare un'immagine Packer, apri il browser web, vai alla pagina seguente, cerca l'immagine specifica e fai clic su Elimina.

    http://console.cloud.google.com/compute/images
  4. Per eliminare tutte le reti VPC, le regole firewall, i router, gli IP e le subnet associati al progetto:

    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 "========================================================================="

Elimina il progetto

Elimina un progetto Google Cloud :

gcloud projects delete PROJECT_ID

Passaggi successivi