Menggunakan Ray untuk menyempurnakan Gemma 3 untuk tugas visi di GKE

Tutorial ini menunjukkan cara menyetel model Gemma 3 dengan menggunakan framework Ray di cluster GKE multi-node. Cluster ini menggunakan dua instance mesin virtual (VM) A4, yang masing-masing memiliki delapan GPU NVIDIA B200 yang terpasang.

Isi tutorial ini dibagi menjadi dua bagian:

  1. Menyiapkan Cluster Ray di atas cluster Autopilot GKE.
  2. Menjalankan tugas pelatihan terdistribusi, menggunakan 2 instance A4, dengan masing-masing 8 GPU B200.

Tutorial ini ditujukan untuk engineer, peneliti, administrator, dan operator platform machine learning (ML), serta spesialis data dan AI yang tertarik untuk mendistribusikan beban kerja AI di beberapa node dan GPU.

Tujuan

  • Akses model Gemma 3 menggunakan Hugging Face.

  • Siapkan lingkungan Anda.

  • Buat cluster GKE Autopilot dengan Ray Operator yang diinstal di dalamnya.

  • Konfigurasi Cluster Ray di cluster GKE untuk menerima Ray Job.

  • Mengonfigurasi dan menjalankan Tugas Ray yang menyetel model Gemma 3 berdasarkan input visual.

  • Pantau workload Anda.

  • Jalankan pembersihan.

Biaya

Dalam dokumen ini, Anda akan menggunakan komponen Google Cloudyang dapat ditagih berikut:

Untuk membuat perkiraan biaya berdasarkan proyeksi penggunaan Anda, gunakan kalkulator harga.

Pengguna Google Cloud baru mungkin memenuhi syarat untuk mendapatkan uji coba gratis.

Sebelum memulai

  1. Login ke akun Google Cloud Anda. Jika Anda baru menggunakan Google Cloud, buat akun untuk mengevaluasi performa produk kami dalam skenario dunia nyata. Pelanggan baru juga mendapatkan kredit gratis senilai $300 untuk menjalankan, menguji, dan men-deploy workload.
  2. Instal Google Cloud CLI.

  3. Jika Anda menggunakan penyedia identitas (IdP) eksternal, Anda harus login ke gcloud CLI dengan identitas gabungan Anda terlebih dahulu.

  4. Untuk melakukan inisialisasi gcloud CLI, jalankan perintah berikut:

    gcloud init
  5. Buat atau pilih Google Cloud project.

    Peran yang diperlukan untuk memilih atau membuat project

    • Pilih project: Memilih project tidak memerlukan peran IAM tertentu—Anda dapat memilih project mana pun yang telah diberi peran.
    • Membuat project: Untuk membuat project, Anda memerlukan peran Pembuat Project (roles/resourcemanager.projectCreator), yang berisi izin resourcemanager.projects.create. Pelajari cara memberikan peran.
    • Buat Google Cloud project:

      gcloud projects create PROJECT_ID

      Ganti PROJECT_ID dengan nama untuk Google Cloud project yang Anda buat.

    • Pilih project Google Cloud yang Anda buat:

      gcloud config set project PROJECT_ID

      Ganti PROJECT_ID dengan nama project Google Cloud Anda.

  6. Verifikasi bahwa penagihan diaktifkan untuk project Google Cloud Anda.

  7. Aktifkan API yang diperlukan:

    Peran yang diperlukan untuk mengaktifkan API

    Untuk mengaktifkan API, Anda memerlukan peran IAM Service Usage Admin (roles/serviceusage.serviceUsageAdmin), yang berisi izin serviceusage.services.enable. Pelajari cara memberikan peran.

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  8. Instal Google Cloud CLI.

  9. Jika Anda menggunakan penyedia identitas (IdP) eksternal, Anda harus login ke gcloud CLI dengan identitas gabungan Anda terlebih dahulu.

  10. Untuk melakukan inisialisasi gcloud CLI, jalankan perintah berikut:

    gcloud init
  11. Buat atau pilih Google Cloud project.

    Peran yang diperlukan untuk memilih atau membuat project

    • Pilih project: Memilih project tidak memerlukan peran IAM tertentu—Anda dapat memilih project mana pun yang telah diberi peran.
    • Membuat project: Untuk membuat project, Anda memerlukan peran Pembuat Project (roles/resourcemanager.projectCreator), yang berisi izin resourcemanager.projects.create. Pelajari cara memberikan peran.
    • Buat Google Cloud project:

      gcloud projects create PROJECT_ID

      Ganti PROJECT_ID dengan nama untuk Google Cloud project yang Anda buat.

    • Pilih project Google Cloud yang Anda buat:

      gcloud config set project PROJECT_ID

      Ganti PROJECT_ID dengan nama project Google Cloud Anda.

  12. Verifikasi bahwa penagihan diaktifkan untuk project Google Cloud Anda.

  13. Aktifkan API yang diperlukan:

    Peran yang diperlukan untuk mengaktifkan API

    Untuk mengaktifkan API, Anda memerlukan peran IAM Service Usage Admin (roles/serviceusage.serviceUsageAdmin), yang berisi izin serviceusage.services.enable. Pelajari cara memberikan peran.

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  14. Memberikan peran ke akun pengguna Anda. Jalankan perintah berikut satu kali untuk setiap peran IAM berikut: 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

    Ganti kode berikut:

    • PROJECT_ID: Project ID Anda.
    • USER_IDENTIFIER: ID untuk akun pengguna Anda. Misalnya, myemail@example.com.
    • ROLE: Peran IAM yang Anda berikan ke akun pengguna Anda.
  15. Aktifkan akun layanan default untuk project Google Cloud Anda:
    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --project=PROJECT_ID

    Ganti PROJECT_NUMBER dengan nomor project Anda. Untuk meninjau nomor project Anda, lihat Mendapatkan project yang sudah ada.

  16. Berikan peran Editor (roles/editor) ke akun layanan default:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
        --role=roles/editor
  17. Buat kredensial autentikasi lokal untuk akun pengguna Anda:
    gcloud auth application-default login
  18. Login atau buat akun Hugging Face.

Mengakses Gemma 3 menggunakan Hugging Face

Untuk menggunakan Hugging Face guna mengakses Gemma 3, lakukan langkah-langkah berikut:

  1. Tandatangani perjanjian izin untuk menggunakan Gemma 3.

  2. Buat token Hugging Face read access.

  3. Salin dan simpan nilai token read access. Anda akan menggunakannya nanti dalam tutorial ini.

Menyiapkan lingkungan Anda

Siapkan lingkungan Anda dengan mengonfigurasi setelan yang diperlukan dan menetapkan variabel lingkungan.

Jalankan perintah berikut:

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

Ganti kode berikut:

  • RESERVATION_URL: URL pemesanan yang ingin Anda gunakan untuk membuat cluster. Berdasarkan project tempat pemesanan berada, tentukan salah satu nilai berikut:
    • Reservasi ada di project Anda: RESERVATION_NAME
    • Pemesanan ada di project lain, dan project Anda dapat menggunakan pemesanan: projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME. URL lengkap dan sebagian diterima. Misalnya, Anda dapat menggunakan projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME.
  • REGION: region tempat Anda ingin membuat cluster GKE. Anda hanya dapat membuat cluster di region tempat reservasi Anda berada.
  • CLUSTER_NAME: nama cluster GKE yang akan dibuat.
  • HF_TOKEN: token Hugging Face yang Anda buat di langkah sebelumnya.
  • GCS_BUCKET: nama bucket tempat Anda menyimpan hasil dari titik pemeriksaan pelatihan.

Membuat cluster GKE dalam mode Autopilot

Untuk membuat cluster GKE dalam mode Autopilot, jalankan perintah berikut:

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

Mungkin perlu waktu beberapa saat untuk menyelesaikan pembuatan cluster GKE. Untuk memverifikasi apakah Google Cloud telah selesai membuat cluster Anda, buka Cluster Kubernetes di konsol Google Cloud .

Buat secret Kubernetes untuk kredensial Hugging Face

Di Cloud Shell, untuk membuat secret Kubernetes bagi kredensial Hugging Face, lakukan hal berikut:

  1. Konfigurasi kubectl untuk terhubung ke cluster Anda:

    gcloud container clusters get-credentials $CLUSTER_NAME \
        --region=$REGION
    
  2. Buat secret Kubernetes untuk menyimpan token Hugging Face Anda:

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

Buat bucket Google Cloud Storage

Jika Anda ingin menggunakan bucket baru untuk menyimpan artefak pelatihan, jalankan perintah berikut:

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

Jika ingin menggunakan bucket yang sudah ada, Anda dapat melewati langkah ini. Namun, Anda harus memastikan bahwa bucket Anda berada di region yang sama dengan cluster Anda.

Menyimpan kode pelatihan sebagai ConfigMap

Untuk menghindari kebutuhan menyematkan skrip pelatihan ke dalam image container, Anda menyimpannya sebagai ConfigMap di cluster. ConfigMap ini di-mount ke sistem file Pod, yang memungkinkan Anda memperbarui skrip pelatihan tanpa harus membuat ulang seluruh cluster Ray.

  1. Buka folder code dan buat file baru.

    Salin kode code/vision_train.py berikut ke dalam file baru ini:

    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. Simpan file.

  3. Buat objek ConfigMap di cluster Anda:

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

    Untuk memperbarui skrip pelatihan, Anda menjalankan kembali perintah sebelumnya. Mungkin perlu waktu satu menit sebelum perubahan diterapkan ke semua pod.

Mengonfigurasi Cluster Ray

  1. Untuk membuat Cluster Ray di cluster GKE Anda, simpan YAML berikut sebagai file 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. Terapkan definisi YAML ini ke cluster Anda menggunakan perintah berikut:

    envsubst < ray_cluster.yaml | kubectl apply -f -
    

    Flag $RESERVATION otomatis diganti dengan nama yang Anda konfigurasi sebagai variabel lingkungan.

    Ray Operator membuat pod raylet, yang pada gilirannya memicu penskalaan otomatis cluster untuk menyediakan node yang sesuai bagi pod tersebut. Tiga pod dibuat di cluster Anda: satu node head, dan dua worker node. Node pekerja dilengkapi dengan GPU B200.

  3. Untuk memverifikasi bahwa ketiga pod sudah siap, jalankan perintah berikut:

    kubectl get pods
    

    Daftar pod Ray Cluster yang siap mirip dengan berikut ini:

    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
    

Menjadwalkan tugas pelatihan

  1. Simpan kode berikut sebagai file ray_job.yaml:

    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. Kirimkan definisi RayJob ke RayCluster Anda:

    envsubst < ray_job.yaml | kubectl apply -f -
    
  3. Periksa apakah Pod baru ada di cluster Anda:

    kubectl get pods
    

    Catat nama lengkap Pod test-ray-job- yang Anda lihat di output. Nama ini unik untuk tugas Anda.

  4. Periksa progres pelatihan Anda. Ganti gemma-training-ray-job-UNIQUE_ID dengan nama Pod unik yang Anda catat di langkah sebelumnya.

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

    Output yang Anda lihat mirip dengan berikut ini:

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

    Memantau workload Anda

Anda dapat menggunakan dasbor di Ray untuk memantau beban kerja yang dijadwalkan di cluster Anda.

Untuk mengakses dasbor ini, Anda perlu menyiapkan penerusan port ke cluster dengan menjalankan perintah berikut di jendela terminal baru:

kubectl port-forward service/gemma3-tuning-head-svc 8265:8265 > fwd.log 2>&1 &
  1. Buka link berikut di browser Anda: [http://localhost:8265](http://localhost:8265).

  2. Atau, jika Anda menggunakan Cloud Shell, setelah menjalankan perintah pada langkah sebelumnya, Anda dapat mengklik tombol Web Preview, seperti yang ditunjukkan pada gambar berikut:

    Tombol Pratinjau Web.

    Pilih opsi Ubah port, masukkan 8265, lalu klik Ubah dan Pratinjau. Dasbor Ray akan terbuka di tab baru.

Pembersihan

Agar tidak perlu membayar biaya pada akun Google Cloud Anda untuk resource yang digunakan dalam tutorial ini, hapus project yang berisi resource tersebut, atau simpan project dan hapus setiap resource.

Menghapus project Anda

Menghapus Google Cloud project:

gcloud projects delete PROJECT_ID

Menghapus resource

  1. Untuk menghapus Cluster Ray dan melepaskan node yang didukung GPU, jalankan perintah berikut:

    kubectl delete -f ray_cluster.yaml
    

    GKE akan otomatis menurunkan skala cluster Anda dan melepaskan mesin A4 yang digunakan oleh Ray.

  2. Untuk menghapus seluruh cluster GKE, jalankan perintah berikut:

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

Langkah berikutnya