Ray zum Feinabstimmen von Gemma 3 für Vision-Aufgaben in GKE verwenden

In dieser Anleitung wird gezeigt, wie Sie ein Gemma 3-Modell mit dem Ray-Framework in einem GKE-Cluster mit mehreren Knoten abstimmen. Der Cluster verwendet zwei A4-VM-Instanzen, an die jeweils acht NVIDIA B200-GPUs angehängt sind.

Der Inhalt dieser Anleitung ist in zwei Teile unterteilt:

  1. Ray-Cluster auf einem GKE Autopilot-Cluster vorbereiten.
  2. Ausführen eines verteilten Trainingsjobs mit 2 A4-Instanzen mit jeweils 8 B200-GPUs.

Diese Anleitung richtet sich an Entwickler von maschinellem Lernen (ML), Forscher, Plattformadministratoren und ‑betreiber sowie an Daten- und KI-Spezialisten, die daran interessiert sind, eine KI-Arbeitslast auf mehrere Knoten und GPUs zu verteilen.

Ziele

  • Über Hugging Face auf ein Gemma 3-Modell zugreifen

  • Bereiten Sie Ihre Umgebung vor.

  • Erstellen Sie einen GKE Autopilot-Cluster mit installiertem Ray-Operator.

  • Konfigurieren Sie den Ray-Cluster im GKE-Cluster so, dass er Ray-Jobs akzeptiert.

  • Konfigurieren und führen Sie einen Ray-Job aus, mit dem das Gemma 3-Modell auf Grundlage visueller Eingaben optimiert wird.

  • Arbeitslast überwachen

  • bereinigen.

Kosten

In diesem Dokument verwenden Sie die folgenden kostenpflichtigen Komponenten von Google Cloud:

Mit dem Preisrechner können Sie eine Kostenschätzung für Ihre voraussichtliche Nutzung vornehmen.

Neuen Nutzern von Google Cloud steht möglicherweise eine kostenlose Testversion zur Verfügung.

Hinweis

  1. Melden Sie sich in Ihrem Google Cloud -Konto an. Wenn Sie mit Google Cloudnoch nicht vertraut sind, erstellen Sie ein Konto, um die Leistungsfähigkeit unserer Produkte in der Praxis sehen und bewerten zu können. Neukunden erhalten außerdem ein Guthaben von 300 $, um Arbeitslasten auszuführen, zu testen und bereitzustellen.
  2. Installieren Sie die Google Cloud CLI.

  3. Wenn Sie einen externen Identitätsanbieter (IdP) verwenden, müssen Sie sich zuerst mit Ihrer föderierten Identität in der gcloud CLI anmelden.

  4. Führen Sie den folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  5. Erstellen Sie ein Google Cloud Projekt oder wählen Sie eines aus.

    Rollen, die zum Auswählen oder Erstellen eines Projekts erforderlich sind

    • Projekt auswählen: Für die Auswahl eines Projekts ist keine bestimmte IAM-Rolle erforderlich. Sie können jedes Projekt auswählen, für das Ihnen eine Rolle zugewiesen wurde.
    • Projekt erstellen: Zum Erstellen eines Projekts benötigen Sie die Rolle „Projektersteller“ (roles/resourcemanager.projectCreator), die die Berechtigung resourcemanager.projects.create enthält. Weitere Informationen zum Zuweisen von Rollen
    • So erstellen Sie ein Google Cloud -Projekt:

      gcloud projects create PROJECT_ID

      Ersetzen Sie PROJECT_ID durch einen Namen für das Google Cloud -Projekt, das Sie erstellen.

    • Wählen Sie das von Ihnen erstellte Google Cloud Projekt aus:

      gcloud config set project PROJECT_ID

      Ersetzen Sie PROJECT_ID durch den Namen Ihres Projekts in Google Cloud .

  6. Prüfen Sie, ob für Ihr Google Cloud Projekt die Abrechnung aktiviert ist.

  7. Aktivieren Sie die erforderliche API:

    Rollen, die zum Aktivieren von APIs erforderlich sind

    Zum Aktivieren von APIs benötigen Sie die IAM-Rolle „Service Usage-Administrator“ (roles/serviceusage.serviceUsageAdmin), die die Berechtigung serviceusage.services.enable enthält. Weitere Informationen zum Zuweisen von Rollen

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  8. Installieren Sie die Google Cloud CLI.

  9. Wenn Sie einen externen Identitätsanbieter (IdP) verwenden, müssen Sie sich zuerst mit Ihrer föderierten Identität in der gcloud CLI anmelden.

  10. Führen Sie den folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  11. Erstellen Sie ein Google Cloud Projekt oder wählen Sie eines aus.

    Rollen, die zum Auswählen oder Erstellen eines Projekts erforderlich sind

    • Projekt auswählen: Für die Auswahl eines Projekts ist keine bestimmte IAM-Rolle erforderlich. Sie können jedes Projekt auswählen, für das Ihnen eine Rolle zugewiesen wurde.
    • Projekt erstellen: Zum Erstellen eines Projekts benötigen Sie die Rolle „Projektersteller“ (roles/resourcemanager.projectCreator), die die Berechtigung resourcemanager.projects.create enthält. Weitere Informationen zum Zuweisen von Rollen
    • So erstellen Sie ein Google Cloud -Projekt:

      gcloud projects create PROJECT_ID

      Ersetzen Sie PROJECT_ID durch einen Namen für das Google Cloud -Projekt, das Sie erstellen.

    • Wählen Sie das von Ihnen erstellte Google Cloud Projekt aus:

      gcloud config set project PROJECT_ID

      Ersetzen Sie PROJECT_ID durch den Namen Ihres Projekts in Google Cloud .

  12. Prüfen Sie, ob für Ihr Google Cloud Projekt die Abrechnung aktiviert ist.

  13. Aktivieren Sie die erforderliche API:

    Rollen, die zum Aktivieren von APIs erforderlich sind

    Zum Aktivieren von APIs benötigen Sie die IAM-Rolle „Service Usage-Administrator“ (roles/serviceusage.serviceUsageAdmin), die die Berechtigung serviceusage.services.enable enthält. Weitere Informationen zum Zuweisen von Rollen

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  14. Weisen Sie Ihrem Nutzerkonto Rollen zu. Führen Sie den folgenden Befehl für jede der folgenden IAM-Rollen einmal aus: roles/compute.admin, roles/iam.serviceAccountUser, roles/file.editor, roles/storage.admin, roles/container.clusterAdmin, roles/serviceusage.serviceUsageAdmin

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

    Ersetzen Sie Folgendes:

    • PROJECT_ID: Ihre Projekt-ID.
    • USER_IDENTIFIER: Die Kennung für Ihr Nutzerkonto . Beispiel: myemail@example.com
    • ROLE: Die IAM-Rolle, die Sie Ihrem Nutzerkonto zuweisen.
  15. Aktivieren Sie das Standarddienstkonto für Ihr Google Cloud Projekt:
    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --project=PROJECT_ID

    Ersetzen Sie PROJECT_NUMBER durch die Projekt-ID. Informationen zum Abrufen Ihrer Projektnummer finden Sie unter Vorhandenes Projekt abrufen.

  16. Weisen Sie dem Standarddienstkonto die Rolle „Bearbeiter“ (roles/editor) zu:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
        --role=roles/editor
  17. Erstellen Sie lokale Anmeldedaten zur Authentifizierung für Ihr Nutzerkonto:
    gcloud auth application-default login
  18. Melden Sie sich in einem Hugging Face-Konto an oder erstellen Sie ein Konto.

Über Hugging Face auf Gemma 3 zugreifen

So greifen Sie mit Hugging Face auf Gemma 3 zu:

  1. Einwilligungsvereinbarung unterzeichnen, um Gemma 3 zu verwenden

  2. Erstellen Sie ein Hugging Face-read access-Token.

  3. Kopieren und speichern Sie den read access-Tokenwert. Sie benötigen sie später in dieser Anleitung.

Umgebung vorbereiten

Bereiten Sie Ihre Umgebung vor, indem Sie die erforderlichen Einstellungen konfigurieren und die Umgebungsvariablen festlegen.

Führen Sie den folgenden Befehl aus:

gcloud config set billing/quota_project $PROJECT_ID
export RESERVATION=RESERVATION_URL
export REGION=REGION
export CLUSTER_NAME=CLUSTER_NAME
export HF_TOKEN=HF_TOKEN
export NETWORK=default
export GCS_BUCKET=GCS_BUCKET

Ersetzen Sie Folgendes:

  • RESERVATION_URL: die URL der Reservierung, die Sie zum Erstellen des Clusters verwenden möchten. Geben Sie basierend auf dem Projekt, in dem die Reservierung vorhanden ist, einen der folgenden Werte an:
    • Die Reservierung ist in Ihrem Projekt vorhanden: RESERVATION_NAME
    • Die Reservierung ist in einem anderen Projekt vorhanden und Ihr Projekt kann sie verwenden: projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME. Es werden sowohl vollständige als auch teilweise URLs akzeptiert. Sie können beispielsweise projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME verwenden.
  • REGION: die Region, in der Sie Ihren GKE-Cluster erstellen möchten. Sie können den Cluster nur in der Region erstellen, in der Ihre Reservierung vorhanden ist.
  • CLUSTER_NAME: Der Name des zu erstellenden GKE-Cluster.
  • HF_TOKEN: das Hugging Face-Token, das Sie in einem früheren Schritt erstellt haben.
  • GCS_BUCKET: Der Name des Buckets, in dem Sie die Ergebnisse des Trainings-Checkpoints speichern.

GKE-Cluster im Autopilot-Modus erstellen

Führen Sie Folgendes aus, um einen GKE-Cluster im Autopilot-Modus zu erstellen:

gcloud container clusters create-auto $CLUSTER_NAME \
    --enable-ray-operator \
    --enable-ray-cluster-monitoring \
    --enable-ray-cluster-logging \
    --location=$REGION

Es kann einige Zeit dauern, bis der GKE-Cluster erstellt ist. Rufen Sie in der Google Cloud Console die Seite Kubernetes-Cluster auf, um zu prüfen, ob Google Cloud den Cluster erstellt hat.

Kubernetes-Secret für Hugging Face-Anmeldedaten erstellen

So erstellen Sie in Cloud Shell ein Kubernetes-Secret für Hugging Face-Anmeldedaten:

  1. Konfigurieren Sie kubectl, um eine Verbindung zu Ihrem Cluster herzustellen:

    gcloud container clusters get-credentials $CLUSTER_NAME \
        --region=$REGION
    
  2. Erstellen Sie ein Kubernetes-Secret zum Speichern Ihres Hugging Face-Tokens:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    

Google Cloud Storage-Bucket erstellen

Wenn Sie einen neuen Bucket zum Speichern Ihrer Trainingsartefakte verwenden möchten, führen Sie Folgendes aus:

gcloud storage buckets create gs://$GCS_BUCKET --location=$REGION

Wenn Sie einen vorhandenen Bucket verwenden möchten, können Sie diesen Schritt überspringen. Sie müssen jedoch darauf achten, dass sich Ihr Bucket in derselben Region wie Ihr Cluster befindet.

Trainingscode als ConfigMap speichern

Damit Sie Ihr Trainingsskript nicht in ein Container-Image einbetten müssen, speichern Sie es als ConfigMap in Ihrem Cluster. Diese ConfigMap wird in die Pod-Dateisysteme eingebunden. So können Sie das Trainingsskript aktualisieren, ohne den gesamten Ray-Cluster neu erstellen zu müssen.

  1. Rufen Sie den Ordner code auf und erstellen Sie eine neue Datei.

    Kopieren Sie den folgenden code/vision_train.py-Code in diese neue Datei:

    import argparse
    import datetime
    import ray
    import ray.train.huggingface.transformers
    import torch
    from PIL import Image
    from datasets import load_dataset
    from peft import LoraConfig
    from ray.train import ScalingConfig, RunConfig
    from ray.train.torch import TorchTrainer
    from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig
    from trl import SFTConfig
    from trl import SFTTrainer
    
    # System message for the assistant
    system_message = "You are an expert product description writer for Amazon."
    
    # User prompt that combines the user query and the schema
    user_prompt = """Create a Short Product description based on the provided <PRODUCT> and <CATEGORY> and image.
    Only return description. The description should be SEO optimized and for a better mobile search experience.
    
    <PRODUCT>
    {product}
    </PRODUCT>
    
    <CATEGORY>
    {category}
    </CATEGORY>
    """
    
    def get_args():
        parser = argparse.ArgumentParser()
        parser.add_argument("--model_id", type=str, default="google/gemma-3-4b-it", help="Hugging Face model ID")
        # parser.add_argument("--hf_token", type=str, default=None, help="Hugging Face token for private models")
        parser.add_argument("--dataset_name", type=str, default="philschmid/amazon-product-descriptions-vlm", help="Hugging Face dataset name")
        parser.add_argument("--output_dir", type=str, default="gemma-3-4b-seo-optimized", help="Directory to save model checkpoints")
        parser.add_argument("--gcs_bucket", type=str, required=True, help="storage bucket name used to synchronize tasks and save checkpoints")
        parser.add_argument("--push_to_hub", help="Push model to Hugging Face hub", action="store_true")
    
        # LoRA arguments
        parser.add_argument("--lora_r", type=int, default=16, help="LoRA attention dimension")
        parser.add_argument("--lora_alpha", type=int, default=16, help="LoRA alpha scaling factor")
        parser.add_argument("--lora_dropout", type=float, default=0.05, help="LoRA dropout probability")
    
        # SFTConfig arguments
        parser.add_argument("--max_seq_length", type=int, default=512, help="Maximum sequence length")
        parser.add_argument("--num_train_epochs", type=int, default=3, help="Number of training epochs")
        parser.add_argument("--per_device_train_batch_size", type=int, default=1, 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=2e-4, 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="epoch", help="Checkpoint save strategy")
        parser.add_argument("--save_steps", type=int, default=100, help="Save checkpoint every X steps")
    
        return parser.parse_args()
    
    # Convert dataset to OAI messages
    def format_data(sample):
        return {
            "messages": [
                {
                    "role": "system",
                    "content": [{"type": "text", "text": system_message}],
                },
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": user_prompt.format(
                                product=sample["Product Name"],
                                category=sample["Category"],
                            ),
                        },
                        {
                            "type": "image",
                            "image": sample["image"],
                        },
                    ],
                },
                {
                    "role": "assistant",
                    "content": [{"type": "text", "text": sample["description"]}],
                },
            ],
        }
    
    def process_vision_info(messages: list[dict]) -> list[Image.Image]:
        image_inputs = []
        # Iterate through each conversation
        for msg in messages:
            # Get content (ensure it's a list)
            content = msg.get("content", [])
            if not isinstance(content, list):
                content = [content]
    
            # Check each content element for images
            for element in content:
                if isinstance(element, dict) and ("image" in element or element.get("type") == "image"):
                    # Get the image and convert to RGB
                    if "image" in element:
                        image = element["image"]
                    else:
                        image = element
                    image_inputs.append(image.convert("RGB"))
        return image_inputs
    
    def train(args):
        # Load dataset from the hub
        dataset = load_dataset(args.dataset_name, split="train", streaming=True)
    
        # Convert dataset to OAI messages
        # need to use list comprehension to keep Pil.Image type, .mape convert image to bytes
        dataset = [format_data(sample) for sample in dataset]
    
        # Hugging Face model id
        model_id = args.model_id
    
        # Check if GPU benefits from bfloat16
        if torch.cuda.get_device_capability()[0] < 8:
            raise ValueError("GPU does not support bfloat16, please use a GPU that supports bfloat16.")
    
        # Define model init arguments
        model_kwargs = dict(
            attn_implementation="eager",  # Use "flash_attention_2" when running on Ampere or newer GPU
            torch_dtype=torch.bfloat16,  # What torch dtype to use, defaults to auto
            # device_map="auto",  # Let torch decide how to load the model
        )
    
        # BitsAndBytesConfig int-4 config
        model_kwargs["quantization_config"] = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_use_double_quant=True,
            bnb_4bit_quant_type="nf4",
            bnb_4bit_compute_dtype=model_kwargs["torch_dtype"],
            bnb_4bit_quant_storage=model_kwargs["torch_dtype"],
        )
    
        # Load model and tokenizer
        model = AutoModelForImageTextToText.from_pretrained(model_id, **model_kwargs)
        processor = AutoProcessor.from_pretrained(model_id, use_fast=True)
    
        peft_config = LoraConfig(
            lora_alpha=args.lora_alpha,
            lora_dropout=args.lora_dropout,
            r=args.lora_r,
            bias="none",
            target_modules="all-linear",
            task_type="CAUSAL_LM",
            modules_to_save=[
                "lm_head",
                "embed_tokens",
            ],
        )
    
        args = SFTConfig(
            output_dir=args.output_dir,  # directory to save and repository id
            num_train_epochs=args.num_train_epochs,  # number of training epochs
            per_device_train_batch_size=args.per_device_train_batch_size,  # batch size per device during training
            gradient_accumulation_steps=args.gradient_accumulation_steps,  # number of steps before performing a backward/update pass
            gradient_checkpointing=True,  # use gradient checkpointing to save memory
            optim="adamw_torch_fused",  # use fused adamw optimizer
            logging_steps=args.logging_steps,  # log every N steps
            save_strategy=args.save_strategy,  # save checkpoint every epoch
            learning_rate=args.learning_rate,  # learning rate, based on QLoRA paper
            bf16=True,  # use bfloat16 precision
            max_grad_norm=0.3,  # max gradient norm based on QLoRA paper
            warmup_ratio=0.03,  # warmup ratio based on QLoRA paper
            lr_scheduler_type="constant",  # use constant learning rate scheduler
            push_to_hub=args.push_to_hub,  # push model to hub
            report_to="tensorboard",  # report metrics to tensorboard
            gradient_checkpointing_kwargs={
                "use_reentrant": False
            },  # use reentrant checkpointing
            dataset_text_field="",  # need a dummy field for collator
            dataset_kwargs={"skip_prepare_dataset": True},  # important for collator
        )
        args.remove_unused_columns = False  # important for collator
    
        # Create a data collator to encode text and image pairs
        def collate_fn(examples):
            texts = []
            images = []
            for example in examples:
                image_inputs = process_vision_info(example["messages"])
                text = processor.apply_chat_template(
                    example["messages"], add_generation_prompt=False, tokenize=False
                )
                texts.append(text.strip())
                images.append(image_inputs)
    
            # Tokenize the texts and process the images
            batch = processor(text=texts, images=images, return_tensors="pt", padding=True)
    
            # The labels are the input_ids, and we mask the padding tokens and image tokens in the loss computation
            labels = batch["input_ids"].clone()
    
            # Mask image tokens
            image_token_id = [
                processor.tokenizer.convert_tokens_to_ids(
                    processor.tokenizer.special_tokens_map["boi_token"]
                )
            ]
            # Mask tokens for not being used in the loss computation
            labels[labels == processor.tokenizer.pad_token_id] = -100
            labels[labels == image_token_id] = -100
            labels[labels == 262144] = -100
    
            batch["labels"] = labels
            return batch
    
        trainer = SFTTrainer(
            model=model,
            args=args,
            train_dataset=dataset,
            peft_config=peft_config,
            processing_class=processor,
            data_collator=collate_fn,
        )
    
        callback = ray.train.huggingface.transformers.RayTrainReportCallback()
        trainer.add_callback(callback)
        trainer = ray.train.huggingface.transformers.prepare_trainer(trainer)
    
        # Start training, the model will be automatically saved to the Hub and the output directory
        trainer.train()
    
        # Save the final model again to the Hugging Face Hub
        trainer.save_model()
    
    if __name__ == "__main__":
        args = get_args()
        print("Starting training task!")
        training_name = f"gemma_vision_train_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
    
        gcs_bucket = args.gcs_bucket
        if not gcs_bucket.startswith("gs://"):
            gcs_bucket = "gs://" + gcs_bucket
    
        run_config = RunConfig(
            storage_path=gcs_bucket,
            name=training_name,
        )
        scaling_config = ScalingConfig(num_workers=16, use_gpu=True, accelerator_type="B200")
        ray_trainer = TorchTrainer(train, train_loop_config=args, scaling_config=scaling_config, run_config=run_config)
        print("Commencing training!")
        result = ray_trainer.fit()
    
  2. Speichern Sie die Datei.

  3. Erstellen Sie ein ConfigMap-Objekt in Ihrem Cluster:

    kubectl create cm ray-job-cm --from-file=code -o yaml --dry-run=client | kubectl apply -f -
    

    Wenn Sie das Trainingsskript aktualisieren möchten, führen Sie den vorherigen Befehl noch einmal aus. Es kann eine Minute dauern, bis Änderungen auf alle Pods übertragen werden.

Ray-Cluster konfigurieren

  1. Um einen Ray-Cluster in Ihrem GKE-Cluster zu erstellen, speichern Sie die folgende YAML-Datei als ray_cluster.yaml.

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: gemma3-tuning
    spec:
      rayVersion: '2.48.0'
      headGroupSpec:
        rayStartParams:
          dashboard-host: '0.0.0.0'
        template:
          metadata:
          spec:
            containers:
            - name: ray-head
              image: rayproject/ray:2.48.0
              ports:
              - containerPort: 6379
                name: gcs
              - containerPort: 8265
                name: dashboard
              - containerPort: 10001
                name: client
              resources:
                limits:
                  cpu: "24"
                  ephemeral-storage: "9Gi"
                  memory: "64Gi"
                requests:
                  cpu: "24"
                  ephemeral-storage: "9Gi"
                  memory: "64Gi"
              env:
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
              volumeMounts:
                - name: job-code
                  mountPath: /code/
                - mountPath: /mnt/local-ssd/
                  name: local-storage
            volumes:
              - name: job-code
                configMap:
                  name: ray-job-cm
              - name: local-storage
                emptyDir: { }
      workerGroupSpecs:
      - replicas: 2
        minReplicas: 1
        maxReplicas: 5
        groupName: gpu-group
        rayStartParams: {}
        template:
          spec:
            containers:
            - name: ray-worker
              image: rayproject/ray:2.48.0-gpu
              resources:
                limits:
                  nvidia.com/gpu: "8"
                requests:
                  nvidia.com/gpu: "8"
              env:
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
              volumeMounts:
                - name: job-code
                  mountPath: /code/
                - mountPath: /mnt/local-ssd/
                  name: local-storage
            volumes:
              - name: job-code
                configMap:
                  name: ray-job-cm
              - name: local-storage
                emptyDir: { }
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
              cloud.google.com/reservation-name: $RESERVATION
              cloud.google.com/reservation-affinity: "specific"
              cloud.google.com/gke-gpu-driver-version: latest
    
  2. Wenden Sie diese YAML-Definition mit dem folgenden Befehl auf Ihren Cluster an:

    envsubst < ray_cluster.yaml | kubectl apply -f -
    

    Das Flag $RESERVATION wird automatisch durch den Namen ersetzt, den Sie als Umgebungsvariable konfiguriert haben.

    Der Ray-Operator erstellt die Raylet-Pods, was wiederum das Autoscaling des Clusters auslöst, um diese Pods mit den entsprechenden Knoten zu versehen. In Ihrem Cluster werden drei Pods erstellt: ein Head-Knoten und zwei Worker-Knoten. Die Worker-Knoten sind mit den B200-GPUs ausgestattet.

  3. Führen Sie den folgenden Befehl aus, um zu prüfen, ob alle drei Pods bereit sind:

    kubectl get pods
    

    Die Pod-Liste eines einsatzbereiten Ray-Clusters sieht in etwa so aus:

    NAME                                   READY   STATUS    RESTARTS   AGE
    gemma3-tuning-gpu-group-worker-s4h8f   2/2     Running   0          16m
    gemma3-tuning-gpu-group-worker-stg5f   2/2     Running   0          5m34s
    gemma3-tuning-head-zbdvp               2/2     Running   0          16m
    

Trainingsjob planen

  1. Speichern Sie Folgendes als ray_job.yaml-Datei:

    apiVersion: ray.io/v1
    kind: RayJob
    metadata:
      name: test-ray-job
    spec:
      entrypoint: python /code/vision_train.py --gcs_bucket $GCS_BUCKET
      runtimeEnvYAML: |
        pip:
          - torch==2.8.0
          - torchvision==0.23.0
          - ray==2.48.0
          - transformers==4.55.2
          - datasets==4.0.0
          - evaluate==0.4.5
          - accelerate==1.10.0
          - pillow==11.3.0
          - bitsandbytes==0.47.0
          - trl==0.21.0
          - peft==0.17.0
      clusterSelector:
        ray.io/cluster: gemma3-tuning
    
  2. Senden Sie die RayJob-Definition an Ihren RayCluster:

    envsubst < ray_job.yaml | kubectl apply -f -
    
  3. Prüfen Sie, ob sich ein neuer Pod in Ihrem Cluster befindet:

    kubectl get pods
    

    Notieren Sie sich den vollständigen Namen des test-ray-job--Pods, der in der Ausgabe angezeigt wird. Dieser Name ist für Ihren Job eindeutig.

  4. Trainingsfortschritt prüfen Ersetzen Sie gemma-training-ray-job-UNIQUE_ID durch den eindeutigen Pod-Namen, den Sie im vorherigen Schritt notiert haben.

    kubectl logs -f <gemma-training-ray-job-UNIQUE_ID>
    

    Die Ausgabe sieht in etwa so aus:

    2025-08-20 08:29:34,966 INFO cli.py:41 -- Job submission server address: http://gemma3-tuning-head-svc.default.svc.cluster.local:8265
    2025-08-20 08:29:34,991 SUCC cli.py:65 -- -----------------------------------------------
    2025-08-20 08:29:34,991 SUCC cli.py:66 -- Job 'test-ray-job-82mm7' submitted successfully
    2025-08-20 08:29:34,991 SUCC cli.py:67 -- -----------------------------------------------
    2025-08-20 08:29:34,992 INFO cli.py:291 -- Next steps
    2025-08-20 08:29:34,992 INFO cli.py:292 -- Query the logs of the job:
    2025-08-20 08:29:34,992 INFO cli.py:294 -- ray job logs test-ray-job-82mm7
    2025-08-20 08:29:34,992 INFO cli.py:296 -- Query the status of the job:
    2025-08-20 08:29:34,992 INFO cli.py:298 -- ray job status test-ray-job-82mm7
    2025-08-20 08:29:34,992 INFO cli.py:300 -- Request the job to be stopped:
    2025-08-20 08:29:34,992 INFO cli.py:302 -- ray job stop test-ray-job-82mm7
    2025-08-20 08:29:35,003 INFO cli.py:312 -- Tailing logs until the job exits (disable with --no-wait):
    2025-08-20 08:29:34,982 INFO job_manager.py:531 -- Runtime env is setting up.
    Starting training task!
    Commencing training!
    2025-08-20 08:30:08,498 INFO worker.py:1606 -- Using address 10.76.0.17:6379 set in the environment variable RAY_ADDRESS
    2025-08-20 08:30:08,506 INFO worker.py:1747 -- Connecting to existing Ray cluster at address: 10.76.0.17:6379...
    2025-08-20 08:30:08,527 INFO worker.py:1918 -- Connected to Ray cluster. View the dashboard at 10.76.0.17:8265
    2025-08-20 08:30:08,701 INFO tune.py:253 -- Initializing Ray automatically. For cluster usage or custom Ray initialization, call `ray.init(...)` before `<FrameworkTrainer>(...)`.
    2025-08-20 08:30:08,951 WARNING tune_controller.py:2132 -- The maximum number of pending trials has been automatically set to the number of available cluster CPUs, which is high (519 CPUs/pending trials). If you're running an experiment with a large number of trials, this could lead to scheduling overhead. In this case, consider setting the `TUNE_MAX_PENDING_TRIALS_PG` environment variable to the desired maximum number of concurrent pending trials.
    2025-08-20 08:30:08,953 WARNING tune_controller.py:2132 -- The maximum number of pending trials has been automatically set to the number of available cluster CPUs, which is high (519 CPUs/pending trials). If you're running an experiment with a large number of trials, this could lead to scheduling overhead. In this case, consider setting the `TUNE_MAX_PENDING_TRIALS_PG` environment variable to the desired maximum number of concurrent pending trials.
    
    View detailed results here: YOUR_GCS_BUCKET/gemma_vision_train_2025_08_20_08_30_07
    To visualize your results with TensorBoard, run: `tensorboard --logdir /tmp/ray/session_2025-08-20_04-43-14_215096_1/artifacts/2025-08-20_08-30-08/gemma_vision_train_2025_08_20_08_30_07/driver_artifacts`
    
    Training started with configuration:
    ╭──────────────────────────────────────────────────────────────────────╮
    │ Training config                                                      │
    ├──────────────────────────────────────────────────────────────────────┤
    │ train_loop_config/dataset_name                  ...-descriptions-vlm │
    │ train_loop_config/gcs_bucket                    ...-bucket-yooo-west │
    │ train_loop_config/gradient_accumulation_steps                      4 │
    │ train_loop_config/learning_rate                               0.0002 │
    │ train_loop_config/logging_steps                                   10 │
    │ train_loop_config/lora_alpha                                      16 │
    │ train_loop_config/lora_dropout                                  0.05 │
    │ train_loop_config/lora_r                                          16 │
    │ train_loop_config/max_seq_length                                 512 │
    │ train_loop_config/model_id                      google/gemma-3-4b-it │
    │ train_loop_config/num_train_epochs                                 3 │
    │ train_loop_config/output_dir                    ...-4b-seo-optimized │
    │ train_loop_config/per_device_train_batch_size                      1 │
    │ train_loop_config/push_to_hub                                  False │
    │ train_loop_config/save_steps                                     100 │
    │ train_loop_config/save_strategy                                epoch │
    ╰──────────────────────────────────────────────────────────────────────╯
    (RayTrainWorker pid=45455, ip=10.76.0.71) Setting up process group for: env:// [rank=0, world_size=16]
    (TorchTrainer pid=45197, ip=10.76.0.71) Started distributed worker processes:
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45455) world_rank=0, local_rank=0, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45450) world_rank=1, local_rank=1, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45454) world_rank=2, local_rank=2, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45448) world_rank=3, local_rank=3, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45453) world_rank=4, local_rank=4, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45452) world_rank=5, local_rank=5, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45451) world_rank=6, local_rank=6, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=4c934ab2f646a578b03cc335586f30b943e811b645526a74c50bfca1, ip=10.76.0.71, pid=45449) world_rank=7, local_rank=7, node_rank=0
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45729) world_rank=8, local_rank=0, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45726) world_rank=9, local_rank=1, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45728) world_rank=10, local_rank=2, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45727) world_rank=11, local_rank=3, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45725) world_rank=12, local_rank=4, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45724) world_rank=13, local_rank=5, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45723) world_rank=14, local_rank=6, node_rank=1
    (TorchTrainer pid=45197, ip=10.76.0.71) - (node_id=c0db52b44f891f3d6a1cedcbea4c6beb2c8434c66ef414dc15e65743, ip=10.76.0.135, pid=45722) world_rank=15, local_rank=7, node_rank=1
    
    ...
    
    Training finished iteration 3 at 2025-08-20 08:40:43. Total running time: 10min 34s
    ╭─────────────────────────────────────────╮
    │ Training result                         │
    ├─────────────────────────────────────────┤
    │ checkpoint_dir_name   checkpoint_000002 │
    │ time_this_iter_s               152.6374 │
    │ time_total_s                  525.88585 │
    │ training_iteration                    3 │
    │ epoch                           2.75294 │
    │ grad_norm                      47.27161 │
    │ learning_rate                    0.0002 │
    │ loss                            22.5275 │
    │ mean_token_accuracy             0.90325 │
    │ num_tokens                     1583017. │
    │ step                                 60 │
    ╰─────────────────────────────────────────╯
    
    ...
    
    Training completed after 3 iterations at 2025-08-20 08:40:52. Total running time: 10min 43s
    2025-08-20 08:40:53,113 INFO tune.py:1009 -- Wrote the latest version of all result files and experiment state to 'YOUR_GCS_BUCKET/gemma_vision_train_2025_08_20_08_30_07' in 0.1663s.
    
    2025-08-20 08:40:58,304 SUCC cli.py:65 -- ----------------------------------
    2025-08-20 08:40:58,305 SUCC cli.py:66 -- Job 'test-ray-job-82mm7' succeeded
    2025-08-20 08:40:58,305 SUCC cli.py:67 -- ----------------------------------
    

    Arbeitslast überwachen

Mit dem Dashboard in Ray können Sie die Arbeitslasten überwachen, die in Ihrem Cluster geplant sind.

Um auf dieses Dashboard zuzugreifen, müssen Sie die Portweiterleitung zu Ihrem Cluster einrichten. Führen Sie dazu den folgenden Befehl in einem neuen Terminalfenster aus:

kubectl port-forward service/gemma3-tuning-head-svc 8265:8265 > fwd.log 2>&1 &
  1. Öffnen Sie den folgenden Link in Ihrem Browser: [http://localhost:8265](http://localhost:8265).

  2. Wenn Sie Cloud Shell verwenden, können Sie nach dem Ausführen des Befehls im vorherigen Schritt optional auf die Schaltfläche Webvorschau klicken, wie im folgenden Bild dargestellt:

    Schaltfläche „Webvorschau“

    Wählen Sie die Option Port ändern aus, geben Sie 8265 ein und klicken Sie dann auf Ändern und Vorschau. Das Ray-Dashboard wird in einem neuen Tab geöffnet.

Bereinigen

Damit Ihrem Google Cloud-Konto die in dieser Anleitung verwendeten Ressourcen nicht in Rechnung gestellt werden, löschen Sie entweder das Projekt, das die Ressourcen enthält, oder Sie behalten das Projekt und löschen die einzelnen Ressourcen.

Projekt löschen

Google Cloud -Projekt löschen:

gcloud projects delete PROJECT_ID

Ressourcen löschen

  1. Führen Sie den folgenden Befehl aus, um den Ray-Cluster zu löschen und den GPU-basierten Knoten freizugeben:

    kubectl delete -f ray_cluster.yaml
    

    GKE skaliert Ihren Cluster automatisch herunter und gibt die von Ray verwendeten A4-Maschinen frei.

  2. Führen Sie den folgenden Befehl aus, um den gesamten GKE-Cluster zu löschen:

    gcloud container clusters delete $CLUSTER_NAME \
    --region=$REGION
    

Nächste Schritte