Cómo ajustar un LLM con trabajos de Cloud Run

1. Introducción

Descripción general

En este codelab, usarás trabajos de Cloud Run para ajustar un modelo de Gemma 3 y, luego, entregar el resultado en Cloud Run con vLLM.

Actividades

Entrena un modelo para responder a una frase específica con un resultado específico usando el conjunto de datos KomeijiForce/Text2Emoji, establecido como parte de EmojiLM: Modeling the New Emoji Language.

Después del entrenamiento, el modelo responde a una oración que comienza con "Traduce a emojis: " con una serie de emojis que corresponden a esa oración.

Qué aprenderás

  • Cómo realizar un ajuste con la GPU de Cloud Run Jobs
  • Cómo entregar un modelo con Cloud Run y vLLM
  • Cómo usar la configuración de VPC directa para un trabajo de GPU y acelerar la carga y la entrega del modelo

2. Antes de comenzar

Habilita las APIs

Antes de comenzar a usar este codelab, habilita las siguientes APIs ejecutando el siguiente comando:

gcloud services enable run.googleapis.com \
    compute.googleapis.com \
    run.googleapis.com \
    cloudbuild.googleapis.com \
    secretmanager.googleapis.com \
    artifactregistry.googleapis.com

Cuota de GPU

Revisa la documentación de Cuota de GPU para confirmar cómo solicitar cuota.

Si encuentras errores del tipo "No tienes cuota para usar GPUs", confirma tu cuota en g.co/cloudrun/gpu-quota.

Nota: Si usas un proyecto nuevo, es posible que transcurran unos minutos entre la habilitación de la API y la aparición de las cuotas en la página de cuotas.

Hugging Face

En este codelab, se usa un modelo alojado en Hugging Face. Para obtener este modelo, solicita el token de acceso de usuario de Hugging Face con permiso de "Lectura". Más adelante, harás referencia a este valor como YOUR_HF_TOKEN.

Para usar el modelo gemma-3-1b-it, debes aceptar las condiciones de uso.

3. Configuración y requisitos

Configura los siguientes recursos:

  • Cuenta de servicio de IAM y permisos de IAM asociados
  • Secreto de Secret Manager para almacenar tu token de Hugging Face
  • Bucket de Cloud Storage para almacenar tu modelo ajustado
  • Repositorio de Artifact Registry para almacenar la imagen que compilarás para ajustar tu modelo
  1. Configura las variables de entorno para este codelab. Ya completamos previamente varias variables por ti. Especifica el ID del proyecto, la región y el token de Hugging Face.
    export PROJECT_ID=<YOUR_PROJECT_ID>
    export REGION=<YOUR_REGION>
    export HF_TOKEN=<YOUR_HF_TOKEN>
    
    export NEW_MODEL=gemma-emoji
    export AR_REPO=codelab-finetuning-jobs
    export IMAGE_NAME=finetune-to-gcs
    export JOB_NAME=finetuning-to-gcs-job
    export BUCKET_NAME=$PROJECT_ID-codelab-finetuning-jobs
    export SECRET_ID=HF_TOKEN
    export SERVICE_ACCOUNT="finetune-job-sa"
    export SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
    
  2. Para crear la cuenta de servicio, ejecuta este comando:
    gcloud iam service-accounts create $SERVICE_ACCOUNT \
      --display-name="Service account for fine-tuning codelab"
    
  3. Usa Secret Manager para almacenar el token de acceso de Hugging Face:
    gcloud secrets create $SECRET_ID \
          --replication-policy="automatic"
    
    printf $HF_TOKEN | gcloud secrets versions add $SECRET_ID --data-file=-
    
  4. Otorga a tu cuenta de servicio el rol de Secret Manager Secret Accessor:
    gcloud secrets add-iam-policy-binding $SECRET_ID \
      --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
      --role='roles/secretmanager.secretAccessor'
    
  5. Crea un bucket que alojará tu modelo ajustado:
    gcloud storage buckets create -l $REGION gs://$BUCKET_NAME
    
  6. Otorga acceso a tu cuenta de servicio al bucket:
    gcloud storage buckets add-iam-policy-binding gs://$BUCKET_NAME \
      --member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
      --role=roles/storage.objectAdmin
    
  7. Crea un repositorio de Artifact Registry para almacenar la imagen de contenedor:
    gcloud artifacts repositories create $AR_REPO \
        --repository-format=docker \
        --location=$REGION \
        --description="codelab for finetuning using CR jobs" \
        --project=$PROJECT_ID
    

4. Crea la imagen del trabajo de Cloud Run

En el siguiente paso, crearás el código que hará lo siguiente:

  • Importa el modelo de Gemma desde Hugging Face
  • Realiza un ajuste fino en el modelo con el conjunto de datos de Hugging Face. El trabajo usa una sola GPU L4 para el ajuste.
  • Sube el modelo ajustado llamado new_model a tu bucket de Cloud Storage.
  1. Crea un directorio para el código de tu trabajo de ajuste.
    mkdir codelab-finetuning-job
    cd codelab-finetuning-job
    
  2. Crea un archivo llamado finetune.py.
    # Copyright 2025 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #      http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    import os
    
    import torch
    from datasets import load_dataset
    from peft import LoraConfig, PeftModel
    from transformers import (
        AutoModelForCausalLM,
        AutoTokenizer,
        BitsAndBytesConfig,
        TrainingArguments,
    )
    from trl import SFTTrainer
    
    # Cloud Storage bucket to upload the model
    bucket_name = os.getenv("BUCKET_NAME", "YOUR_BUCKET_NAME")
    
    # The model that you want to train from the Hugging Face hub
    model_name = os.getenv("MODEL_NAME", "google/gemma-3-1b-it")
    
    # The instruction dataset to use
    dataset_name = "KomeijiForce/Text2Emoji"
    
    # Fine-tuned model name
    new_model = os.getenv("NEW_MODEL", "gemma-emoji")
    
    ############################ Setup ############################################
    
    # Load the entire model on the GPU 0
    device_map = {"": torch.cuda.current_device()}
    
    # Limit dataset to a random selection
    dataset = load_dataset(dataset_name, split="train").shuffle(seed=42).select(range(1000))
    
    # Setup input formats: trains the model to respond to "Translate to emoji:" with emoji output.
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    def format_to_chat(example):
        return {
            "conversations": [
                {"role": "user", "content": f"Translate to emoji: {example['text']}"},
                {"role": "assistant", "content": example["emoji"]},
            ]
        }
    
    formatted_dataset = dataset.map(
        format_to_chat,
        batched=False,                        # Process row by row
        remove_columns=dataset.column_names,  # Optional: Keep only the new column
    )
    
    def apply_chat_template(examples):
        texts = tokenizer.apply_chat_template(examples["conversations"], tokenize=False)
        return {"text": texts}
    
    final_dataset = formatted_dataset.map(apply_chat_template, batched=True)
    
    ############################# Config #########################################
    
    # Load tokenizer and model with QLoRA configuration
    bnb_4bit_compute_dtype = "float16"  # Compute dtype for 4-bit base models
    compute_dtype = getattr(torch, bnb_4bit_compute_dtype)
    
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,  # Activate 4-bit precision base model loading
        bnb_4bit_quant_type="nf4",  # Quantization type (fp4 or nf4)
        bnb_4bit_compute_dtype=compute_dtype,
        bnb_4bit_use_double_quant=False,  # Activate nested quantization for 4-bit base models (double quantization)
    )
    
    # Load base model
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        quantization_config=bnb_config,
        device_map=device_map,
        torch_dtype=torch.float16,
    )
    model.config.use_cache = False
    model.config.pretraining_tp = 1
    
    ############################## Train ##########################################
    
    # Load LoRA configuration
    peft_config = LoraConfig(
        lora_alpha=16,     # Alpha parameter for LoRA scaling
        lora_dropout=0.1,  # Dropout probability for LoRA layers,
        r=8,               # LoRA attention dimension
        bias="none",
        task_type="CAUSAL_LM",
        target_modules=["q_proj", "v_proj"],
    )
    
    # Set training parameters
    training_arguments = TrainingArguments(
        output_dir="./results",
        num_train_epochs=1,
        per_device_train_batch_size=1,  # Batch size per GPU for training
        gradient_accumulation_steps=2,  # Number of update steps to accumulate the gradients for
        optim="paged_adamw_32bit",
        save_steps=0,
        logging_steps=5,
        learning_rate=2e-4,    # Initial learning rate (AdamW optimizer)
        weight_decay=0.001,    # Weight decay to apply to all layers except bias/LayerNorm weights
        fp16=True, bf16=False, # Enable fp16/bf16 training
        max_grad_norm=0.3,     # Maximum gradient normal (gradient clipping)
        warmup_ratio=0.03,     # Ratio of steps for a linear warmup (from 0 to learning rate)
        group_by_length=True,  # Group sequences into batches with same length # Saves memory and speeds up training considerably
        lr_scheduler_type="cosine",
    )
    
    trainer = SFTTrainer(
        model=model,
        train_dataset=final_dataset,
        peft_config=peft_config,
        dataset_text_field="text",
        max_seq_length=512,  # Maximum sequence length to use
        tokenizer=tokenizer,
        args=training_arguments,
        packing=False,       # Pack multiple short examples in the same input sequence to increase efficiency
    )
    
    trainer.train()
    trainer.model.save_pretrained(new_model)
    
    ################################# Save ########################################
    
    # Reload model in FP16 and merge it with LoRA weights
    base_model = AutoModelForCausalLM.from_pretrained(
        model_name,
        low_cpu_mem_usage=True,
        return_dict=True,
        torch_dtype=torch.float16,
        device_map=device_map,
    )
    model = PeftModel.from_pretrained(base_model, new_model)
    model = model.merge_and_unload()
    
    # push results to Cloud Storage
    file_path_to_save_the_model = "/finetune/new_model"
    model.save_pretrained(file_path_to_save_the_model)
    tokenizer.save_pretrained(file_path_to_save_the_model)
    
    
  3. Crea un archivo requirements.txt:
    accelerate==0.34.2
    bitsandbytes==0.45.5
    datasets==2.19.1
    transformers==4.51.3
    peft==0.11.1
    trl==0.8.6
    torch==2.3.0
    
  4. Crear un Dockerfile:
    FROM nvidia/cuda:12.6.2-runtime-ubuntu22.04
    
    RUN apt-get update && \
        apt-get -y --no-install-recommends install python3-dev gcc python3-pip git && \
        rm -rf /var/lib/apt/lists/*
    
    COPY requirements.txt /requirements.txt
    
    RUN pip3 install -r requirements.txt --no-cache-dir
    
    COPY finetune.py /finetune.py
    
    ENV PYTHONUNBUFFERED 1
    
    CMD python3 /finetune.py --device cuda
    
  5. Compila el contenedor en tu repositorio de Artifact Registry:
    gcloud builds submit \
      --tag $REGION-docker.pkg.dev/$PROJECT_ID/$AR_REPO/$IMAGE_NAME \
      --region $REGION
    

5. Implementa y ejecuta el trabajo

En este paso, crearás el trabajo con salida de VPC directa para acelerar las cargas a Google Cloud Storage.

  1. Crea el trabajo de Cloud Run:
    gcloud beta run jobs create $JOB_NAME \
      --region $REGION \
      --image $REGION-docker.pkg.dev/$PROJECT_ID/$AR_REPO/$IMAGE_NAME \
      --set-env-vars BUCKET_NAME=$BUCKET_NAME \
      --set-secrets HF_TOKEN=$SECRET_ID:latest \
      --cpu 8.0 \
      --memory 32Gi \
      --gpu 1 \
      --add-volume name=finetuned_model,type=cloud-storage,bucket=$BUCKET_NAME \
      --add-volume-mount volume=finetuned_model,mount-path=/finetune/new_model \
      --service-account $SERVICE_ACCOUNT_ADDRESS
    
  2. Ejecuta el trabajo:
    gcloud beta run jobs execute $JOB_NAME --region $REGION --async
    

El trabajo tardará unos 10 minutos en completarse. Puedes verificar el estado con el vínculo que se proporciona en el resultado del último comando.

6. Usa un servicio de Cloud Run para entregar tu modelo ajustado con vLLM

En este paso, implementarás un servicio de Cloud Run. Esta configuración usa la VPC directa para acceder al bucket de Cloud Storage a través de la red privada y lograr descargas más rápidas.

  • Implementa tu servicio de Cloud Run:
    gcloud run deploy serve-gemma-emoji \
      --image us-docker.pkg.dev/vertex-ai/vertex-vision-model-garden-dockers/pytorch-vllm-serve:20250601_0916_RC01 \
      --region $REGION \
      --port 8000 \
      --set-env-vars MODEL_ID=new_model,HF_HUB_OFFLINE=1 \
      --cpu 8.0 \
      --memory 32Gi \
      --gpu 1 \
      --add-volume name=finetuned_model,type=cloud-storage,bucket=$BUCKET_NAME \
      --add-volume-mount volume=finetuned_model,mount-path=/finetune/new_model \
      --service-account $SERVICE_ACCOUNT_ADDRESS \
      --max-instances 1 \
      --command python3 \
      --args="-m,vllm.entrypoints.api_server,--model=/finetune/new_model,--tensor-parallel-size=1" \
      --no-gpu-zonal-redundancy \
      --labels=dev-tutorial=codelab-tuning \
      --no-invoker-iam-check
    

7. Prueba tu modelo ajustado

En este paso, le pedirás a tu modelo que pruebe el ajuste con curl.

  1. Obtén la URL de servicio de tu servicio de Cloud Run:
    SERVICE_URL=$(gcloud run services describe serve-gemma-emoji \
        --region $REGION --format 'value(status.url)')
    
  2. Crea la instrucción para tu modelo.
    USER_PROMPT="Translate to emoji: I ate a banana for breakfast, later I'm thinking of having soup!"
    
  3. Llama a tu servicio con curl para solicitarle a tu modelo, y filtra los resultados con jq:
    curl -s -X POST ${SERVICE_URL}/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: bearer $(gcloud auth print-identity-token)" \
    -d @- <<EOF | jq ".choices[0].message.content"
    {   "model": "${NEW_MODEL}",
        "messages": [{
            "role": "user",
            "content": [ { "type": "text", "text": "${USER_PROMPT}"}]
        }]
    }
    EOF
    
    

Deberías ver una respuesta similar a la siguiente:

🍌🤔😋🥣

8. ¡Felicitaciones!

¡Felicitaciones por completar el codelab!

Te recomendamos que revises la documentación de GPU de Cloud Run Jobs.

Temas abordados

  • Cómo realizar un ajuste con la GPU de Cloud Run Jobs
  • Cómo entregar un modelo con Cloud Run y vLLM
  • Cómo usar la configuración de VPC directa para un trabajo de GPU y acelerar la carga y la entrega del modelo

9. Limpia

Para evitar cargos involuntarios, por ejemplo, si los servicios de Cloud Run se invocan de forma accidental más veces que tu asignación mensual de invocaciones de Cloud Run en el nivel gratuito, puedes borrar el servicio de Cloud Run que creaste en el paso 6.

Para borrar el servicio de Cloud Run, ve a la consola de Cloud Run en https://console.cloud.google.com/run y borra el servicio serve-gemma-emoji.

Para borrar todo el proyecto, ve a Administrar recursos, selecciona el proyecto que creaste en el paso 2 y elige Borrar. Si borras el proyecto, deberás cambiar de proyecto en el SDK de Cloud. Para ver la lista de todos los proyectos disponibles, ejecuta gcloud projects list.