Usar o Ray para ajustar o Gemma 3 para tarefas de visão no GKE

Neste tutorial, mostramos como ajustar um modelo Gemma 3 usando o framework Ray em um cluster do GKE de vários nós. O cluster usa duas instâncias de máquina virtual (VM) A4, cada uma com oito GPUs NVIDIA B200 anexadas.

O conteúdo deste tutorial está dividido em duas partes:

  1. Como preparar o cluster do Ray em um cluster do GKE Autopilot.
  2. Execução de um job de treinamento distribuído usando duas instâncias A4, cada uma com oito GPUs B200.

Este tutorial é destinado a engenheiros de machine learning (ML), pesquisadores, administradores e operadores de plataforma, além de especialistas em dados e IA interessados em distribuir uma carga de trabalho de IA em vários nós e GPUs.

Objetivos

  • Acesse um modelo do Gemma 3 usando o Hugging Face.

  • Prepare seu ambiente.

  • Crie um cluster do GKE Autopilot com o operador do Ray instalado.

  • Configure o cluster do Ray no cluster do GKE para aceitar jobs do Ray.

  • Configure e execute um job do Ray que ajuste o modelo Gemma 3 com base na entrada visual.

  • Monitore sua carga de trabalho.

  • Fazer a limpeza.

Custos

Neste documento, você vai usar os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custo baseada na projeção de uso deste tutorial, use a calculadora de preços.

Novos usuários do Google Cloud podem estar qualificados para um teste sem custo financeiro.

Antes de começar

  1. Faça login na sua conta do Google Cloud . Se você começou a usar o Google Cloud, crie uma conta para avaliar o desempenho de nossos produtos em situações reais. Clientes novos também recebem US$ 300 em créditos para executar, testar e implantar cargas de trabalho.
  2. Instale a CLI do Google Cloud.

  3. Ao usar um provedor de identidade (IdP) externo, primeiro faça login na gcloud CLI com sua identidade federada.

  4. Para inicializar a gcloud CLI, execute o seguinte comando:

    gcloud init
  5. Crie ou selecione um Google Cloud projeto.

    Funções necessárias para selecionar ou criar um projeto

    • Selecionar um projeto: não é necessário um papel específico do IAM para selecionar um projeto. Você pode escolher qualquer projeto em que tenha recebido um papel.
    • Criar um projeto: para criar um projeto, é necessário ter o papel de Criador de projetos (roles/resourcemanager.projectCreator), que contém a permissão resourcemanager.projects.create. Saiba como conceder papéis.
    • Crie um projeto do Google Cloud :

      gcloud projects create PROJECT_ID

      Substitua PROJECT_ID por um nome para o projeto Google Cloud que você está criando.

    • Selecione o projeto Google Cloud que você criou:

      gcloud config set project PROJECT_ID

      Substitua PROJECT_ID pelo nome do projeto do Google Cloud .

  6. Verifique se o faturamento está ativado para o projeto do Google Cloud .

  7. Ative a API necessária:

    Funções necessárias para ativar APIs

    Para ativar as APIs, é necessário ter o papel do IAM de administrador do Service Usage (roles/serviceusage.serviceUsageAdmin), que contém a permissão serviceusage.services.enable. Saiba como conceder papéis.

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  8. Instale a CLI do Google Cloud.

  9. Ao usar um provedor de identidade (IdP) externo, primeiro faça login na gcloud CLI com sua identidade federada.

  10. Para inicializar a gcloud CLI, execute o seguinte comando:

    gcloud init
  11. Crie ou selecione um Google Cloud projeto.

    Funções necessárias para selecionar ou criar um projeto

    • Selecionar um projeto: não é necessário um papel específico do IAM para selecionar um projeto. Você pode escolher qualquer projeto em que tenha recebido um papel.
    • Criar um projeto: para criar um projeto, é necessário ter o papel de Criador de projetos (roles/resourcemanager.projectCreator), que contém a permissão resourcemanager.projects.create. Saiba como conceder papéis.
    • Crie um projeto do Google Cloud :

      gcloud projects create PROJECT_ID

      Substitua PROJECT_ID por um nome para o projeto Google Cloud que você está criando.

    • Selecione o projeto Google Cloud que você criou:

      gcloud config set project PROJECT_ID

      Substitua PROJECT_ID pelo nome do projeto do Google Cloud .

  12. Verifique se o faturamento está ativado para o projeto do Google Cloud .

  13. Ative a API necessária:

    Funções necessárias para ativar APIs

    Para ativar as APIs, é necessário ter o papel do IAM de administrador do Service Usage (roles/serviceusage.serviceUsageAdmin), que contém a permissão serviceusage.services.enable. Saiba como conceder papéis.

    gcloud services enable gcloud services enable compute.googleapis.com logging.googleapis.com cloudresourcemanager.googleapis.com servicenetworking.googleapis.com container.googleapis.com
  14. Atribua papéis à sua conta de usuário. Execute o seguinte comando uma vez para cada um dos seguintes papéis do IAM: 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

    Substitua:

    • PROJECT_ID: o ID do projeto.
    • USER_IDENTIFIER: o identificador da sua conta de usuário . Por exemplo, myemail@example.com.
    • ROLE: o papel do IAM concedido à sua conta de usuário.
  15. Ative a conta de serviço padrão para seu projeto do Google Cloud :
    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --project=PROJECT_ID

    Substitua PROJECT_NUMBER pelo número do projeto. Para revisar o número do projeto, consulte Receber um projeto atual.

  16. Conceda o papel de editor (roles/editor) à conta de serviço padrão:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
        --role=roles/editor
  17. Crie as credenciais de autenticação local para sua conta de usuário:
    gcloud auth application-default login
  18. Faça login ou crie uma conta do Hugging Face.

Acessar o Gemma 3 usando o Hugging Face

Para usar o Hugging Face e acessar o Gemma 3, faça o seguinte:

  1. Assine o contrato de consentimento para usar o Gemma 3.

  2. Crie um token read access do Hugging Face.

  3. Copie e salve o valor do token read access. Você vai usá-lo mais tarde neste tutorial.

Preparar o ambiente

Prepare seu ambiente configurando as opções necessárias e definindo as variáveis de ambiente.

Execute o comando a seguir:

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

Substitua:

  • RESERVATION_URL: o URL da reserva que você quer usar para criar o cluster. Com base no projeto em que a reserva existe, especifique um dos seguintes valores:
    • A reserva existe no seu projeto: RESERVATION_NAME
    • A reserva existe em um projeto diferente, e seu projeto pode usar a reserva: projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME. URLs completos e parciais são aceitos. Por exemplo, é possível usar projects/RESERVATION_PROJECT_ID/reservations/RESERVATION_NAME.
  • REGION: a região em que você quer criar o cluster do GKE. Só é possível criar o cluster na região em que a reserva está.
  • CLUSTER_NAME: o nome do cluster do GKE a ser criado.
  • HF_TOKEN: o token do Hugging Face que você criou em uma etapa anterior.
  • GCS_BUCKET: o nome do bucket em que você armazena os resultados do checkpoint de treinamento.

Criar um cluster do GKE no modo Autopilot

Para criar um cluster do GKE no modo Autopilot, execute o seguinte comando:

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

A criação do cluster do GKE pode levar algum tempo. Para verificar se o Google Cloud terminou de criar o cluster, acesse Clusters do Kubernetes no Google Cloud console.

Criar um secret do Kubernetes para as credenciais do Hugging Face

No Cloud Shell, faça o seguinte para criar um secret do Kubernetes para as credenciais do Hugging Face:

  1. Configure kubectl para se conectar ao cluster:

    gcloud container clusters get-credentials $CLUSTER_NAME \
        --region=$REGION
    
  2. Crie um secret do Kubernetes para armazenar seu token do Hugging Face:

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

Criar o bucket do Google Cloud Storage

Se você quiser usar um novo bucket para armazenar os artefatos de treinamento, execute o seguinte:

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

Se você quiser usar um bucket atual, pule esta etapa. No entanto, verifique se o bucket está na mesma região que o cluster.

Salvar o código de treinamento como um ConfigMap

Para evitar a necessidade de incorporar o script de treinamento em uma imagem de contêiner, armazene-o como um ConfigMap no cluster. Esse ConfigMap é montado nos sistemas de arquivos do pod, o que permite atualizar o script de treinamento sem precisar recriar todo o cluster do Ray.

  1. Navegue até a pasta code e crie um arquivo.

    Copie o seguinte código code/vision_train.py no novo arquivo:

    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. Salve o arquivo.

  3. Crie um objeto ConfigMap no cluster:

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

    Para atualizar o script de treinamento, execute novamente o comando anterior. Pode levar um minuto para que as mudanças sejam propagadas para todos os pods.

Configurar cluster do Ray

  1. Para criar um cluster do Ray no cluster do GKE, salve o seguinte YAML como arquivo 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. Aplique essa definição YAML ao cluster usando o seguinte comando:

    envsubst < ray_cluster.yaml | kubectl apply -f -
    

    A flag $RESERVATION é substituída automaticamente pelo nome que você configurou como variável de ambiente.

    O operador do Ray cria os pods raylet, o que aciona o escalonamento automático do cluster para fornecer a esses pods os nós adequados. Três pods são criados no cluster: um nó principal e dois nós de trabalho. Os nós de trabalho estão equipados com as GPUs B200.

  3. Para verificar se todos os três pods estão prontos, execute o seguinte:

    kubectl get pods
    

    A lista de pods de um cluster do Ray pronto é semelhante a esta:

    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
    

Programar um job de treinamento

  1. Salve o seguinte como um arquivo 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. Envie a definição do RayJob para o RayCluster:

    envsubst < ray_job.yaml | kubectl apply -f -
    
  3. Verifique se um novo pod está no cluster:

    kubectl get pods
    

    Anote o nome completo do pod test-ray-job- que aparece na saída. Esse nome é exclusivo do seu job.

  4. Inspecione o progresso do treinamento. Substitua gemma-training-ray-job-UNIQUE_ID pelo nome exclusivo do pod que você anotou na etapa anterior.

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

    A saída será semelhante a esta:

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

    Monitore sua carga de trabalho

Use o painel no Ray para monitorar as cargas de trabalho programadas no cluster.

Para acessar esse painel, configure o encaminhamento de porta para o cluster executando o seguinte comando em uma nova janela de terminal:

kubectl port-forward service/gemma3-tuning-head-svc 8265:8265 > fwd.log 2>&1 &
  1. Abra o seguinte link no navegador: [http://localhost:8265](http://localhost:8265).

  2. Se você estiver usando o Cloud Shell, depois de executar o comando na etapa anterior, clique no botão Visualização da Web, conforme mostrado na imagem a seguir:

    Botão &quot;Visualização na Web&quot;.

    Selecione a opção Alterar porta, insira 8265 e clique em Alterar e visualizar. O painel do Ray é aberto em uma nova guia.

Limpar

Para evitar cobranças na sua conta do Google Cloud pelos recursos usados no tutorial, exclua o projeto que os contém ou mantenha o projeto e exclua os recursos individuais.

Excluir o projeto

Excluir um projeto do Google Cloud :

gcloud projects delete PROJECT_ID

Excluir os recursos

  1. Para excluir o cluster do Ray e liberar o nó com tecnologia de GPU, execute o seguinte:

    kubectl delete -f ray_cluster.yaml
    

    O GKE reduz automaticamente o cluster e libera as máquinas A4 usadas pelo Ray.

  2. Para excluir todo o cluster do GKE, execute o seguinte:

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

A seguir