Cloud Run 작업을 사용하여 LLM을 미세 조정하는 방법

1. 소개

개요

이 Codelab에서는 Cloud Run 작업을 사용하여 Gemma 3 모델을 미세 조정하고 vLLM을 사용하여 Cloud Run에서 결과를 제공합니다.

실습할 내용

EmojiLM: Modeling the New Emoji Language의 일환으로 구축된 KomeijiForce/Text2Emoji 데이터 세트를 사용하여 특정 문구에 특정 결과로 응답하는 모델을 학습시킵니다.

학습 후 모델은 'Translate to emoji: '로 시작하는 문장에 해당 문장에 상응하는 일련의 그림 이모티콘으로 대답합니다.

학습할 내용

  • Cloud Run Jobs GPU를 사용하여 미세 조정하는 방법
  • vLLM을 사용하여 Cloud Run으로 모델을 제공하는 방법
  • 모델의 업로드 및 제공 속도를 높이기 위해 GPU 작업에 직접 VPC 구성을 사용하는 방법

2. 시작하기 전에

API 사용 설정

이 Codelab을 시작하기 전에 다음을 실행하여 다음 API를 사용 설정하세요.

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

GPU 할당량

GPU 할당량 문서를 검토하여 할당량을 요청하는 방법을 확인하세요.

'GPU 사용 할당량이 없습니다' 오류가 발생하면 g.co/cloudrun/gpu-quota에서 할당량을 확인하세요.

참고: 새 프로젝트를 사용하는 경우 API를 사용 설정하고 할당량이 할당량 페이지에 표시되기까지 몇 분 정도 걸릴 수 있습니다.

Hugging Face

이 Codelab에서는 Hugging Face에서 호스팅되는 모델을 사용합니다. 이 모델을 가져오려면 '읽기' 권한이 있는 Hugging Face 사용자 액세스 토큰을 요청하세요. 이 주소를 나중에 YOUR_HF_TOKEN로 참조합니다.

gemma-3-1b-it 모델을 사용하려면 사용 약관에 동의해야 합니다.

3. 설정 및 요구사항

다음 리소스를 설정합니다.

  • IAM 서비스 계정 및 연결된 IAM 권한
  • Hugging Face 토큰을 저장할 Secret Manager 보안 비밀
  • 미세 조정된 모델을 저장할 Cloud Storage 버킷
  • 모델을 미세 조정하기 위해 빌드할 이미지를 저장할 Artifact Registry 저장소
  1. 이 Codelab의 환경 변수를 설정합니다. 여러 변수가 미리 입력되어 있습니다. 프로젝트 ID, 리전, 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. 다음 명령어를 실행하여 서비스 계정을 만듭니다.
    gcloud iam service-accounts create $SERVICE_ACCOUNT \
      --display-name="Service account for fine-tuning codelab"
    
  3. Secret Manager를 사용하여 Hugging Face 액세스 토큰을 저장합니다.
    gcloud secrets create $SECRET_ID \
          --replication-policy="automatic"
    
    printf $HF_TOKEN | gcloud secrets versions add $SECRET_ID --data-file=-
    
  4. 서비스 계정에 Secret Manager 보안 비밀 접근자 역할을 부여합니다.
    gcloud secrets add-iam-policy-binding $SECRET_ID \
      --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
      --role='roles/secretmanager.secretAccessor'
    
  5. 세부 조정된 모델을 호스팅할 버킷을 만듭니다.
    gcloud storage buckets create -l $REGION gs://$BUCKET_NAME
    
  6. 서비스 계정에 버킷 액세스 권한을 부여합니다.
    gcloud storage buckets add-iam-policy-binding gs://$BUCKET_NAME \
      --member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
      --role=roles/storage.objectAdmin
    
  7. 컨테이너 이미지를 저장할 Artifact Registry 저장소를 만듭니다.
    gcloud artifacts repositories create $AR_REPO \
        --repository-format=docker \
        --location=$REGION \
        --description="codelab for finetuning using CR jobs" \
        --project=$PROJECT_ID
    

4. Cloud Run 작업 이미지 만들기

다음 단계에서는 다음 작업을 실행하는 코드를 만듭니다.

  • Hugging Face에서 Gemma 모델을 가져옵니다.
  • Hugging Face의 데이터 세트를 사용하여 모델을 미세 조정합니다. 작업에서 미세 조정을 위해 단일 L4 GPU를 사용합니다.
  • new_model라는 미세 조정된 모델을 Cloud Storage 버킷에 업로드합니다.
  1. 미세 조정 작업 코드의 디렉터리를 만듭니다.
    mkdir codelab-finetuning-job
    cd codelab-finetuning-job
    
  2. 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. 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. 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. Artifact Registry 저장소에서 컨테이너를 빌드합니다.
    gcloud builds submit \
      --tag $REGION-docker.pkg.dev/$PROJECT_ID/$AR_REPO/$IMAGE_NAME \
      --region $REGION
    

5. 작업 배포 및 실행

이 단계에서는 Google Cloud Storage에 더 빠르게 업로드할 수 있도록 직접 VPC 이그레스가 있는 작업을 만듭니다.

  1. Cloud Run 작업을 만듭니다.
    gcloud 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. 작업 실행:
    gcloud run jobs execute $JOB_NAME --region $REGION --async
    

작업이 완료되는 데 약 10분이 소요됩니다. 마지막 명령어의 출력에 제공된 링크를 사용하여 상태를 확인할 수 있습니다.

6. Cloud Run 서비스를 사용하여 vLLM으로 미세 조정된 모델 제공

이 단계에서는 Cloud Run 서비스를 배포합니다. 이 구성은 직접 VPC를 사용하여 비공개 네트워크를 통해 Cloud Storage 버킷에 액세스하여 다운로드 속도를 높입니다.

  • 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. 미세 조정된 모델 테스트

이 단계에서는 curl을 사용하여 미세 조정 테스트를 수행하도록 모델에 프롬프트를 표시합니다.

  1. Cloud Run 서비스의 서비스 URL을 가져옵니다.
    SERVICE_URL=$(gcloud run services describe serve-gemma-emoji \
        --region $REGION --format 'value(status.url)')
    
  2. 모델의 프롬프트를 만듭니다.
    USER_PROMPT="Translate to emoji: I ate a banana for breakfast, later I'm thinking of having soup!"
    
  3. curl을 사용하여 서비스를 호출하여 모델을 프롬프트하고 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
    
    

다음과 비슷한 응답이 표시됩니다.

🍌🤔😋🥣

8. 수고하셨습니다

축하합니다. Codelab을 완료했습니다.

Cloud Run Jobs GPU 문서를 검토하는 것이 좋습니다.

학습한 내용

  • Cloud Run Jobs GPU를 사용하여 미세 조정하는 방법
  • vLLM을 사용하여 Cloud Run으로 모델을 제공하는 방법
  • 모델의 업로드 및 제공 속도를 높이기 위해 GPU 작업에 직접 VPC 구성을 사용하는 방법

9. 삭제

예를 들어 Cloud Run 서비스가 무료 등급의 월별 Cloud Run 호출 할당량보다 더 많이 호출되는 경우와 같이 의도치 않은 요금이 청구되지 않도록 하려면 6단계에서 만든 Cloud Run 서비스를 삭제하면 됩니다.

Cloud Run 서비스를 삭제하려면 https://console.cloud.google.com/run에서 Cloud Run Cloud Console로 이동하여 serve-gemma-emoji 서비스를 삭제합니다.

전체 프로젝트를 삭제하려면 리소스 관리로 이동하여 2단계에서 만든 프로젝트를 선택하고 삭제를 선택합니다. 프로젝트를 삭제하면 Cloud SDK에서 프로젝트를 변경해야 합니다. gcloud projects list를 실행하여 사용 가능한 모든 프로젝트의 목록을 볼 수 있습니다.