Cómo alojar un LLM en un contenedor secundario para una función de Cloud Run

Cómo alojar un LLM en un contenedor secundario para una función de Cloud Run

Acerca de este codelab

subjectÚltima actualización: mar 27, 2025
account_circleEscrito por un Googler

1. Introducción

Descripción general

En este codelab, aprenderás a alojar un modelo gemma3:4b en un contenedor secundario para una función de Cloud Run. Cuando se sube un archivo a un bucket de Cloud Storage, se activa la función de Cloud Run. La función enviará el contenido del archivo a Gemma 3 en el Sidecar para su resumen.

Qué aprenderás

  • Cómo realizar inferencias con una función de Cloud Run y un LLM alojado en un contenedor lateral con GPUs
  • Cómo usar la configuración de salida de VPC directa para una GPU de Cloud Run para subir y entregar el modelo más rápido
  • Cómo usar genkit para interactuar con tu modelo de ollama alojado

2. Antes de comenzar

Para usar la función de GPU, debes solicitar un aumento de cuota para una región admitida. La cuota necesaria es nvidia_l4_gpu_allocation_no_zonal_redundancy, que se encuentra en la API de Cloud Run Admin. Este es el vínculo directo para solicitar una cuota.

3. Configuración y requisitos

Establece las variables de entorno que se usarán en este codelab.

PROJECT_ID=<YOUR_PROJECT_ID>
REGION
=<YOUR_REGION>

AR_REPO
=codelab-crf-sidecar-gpu
FUNCTION_NAME
=crf-sidecar-gpu
BUCKET_GEMMA_NAME
=$PROJECT_ID-codelab-crf-sidecar-gpu-gemma3
BUCKET_DOCS_NAME
=$PROJECT_ID-codelab-crf-sidecar-gpu-docs
SERVICE_ACCOUNT
="crf-sidecar-gpu"
SERVICE_ACCOUNT_ADDRESS
=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
IMAGE_SIDECAR
=$REGION-docker.pkg.dev/$PROJECT_ID/$AR_REPO/ollama-gemma3

Ejecuta este comando para crear la cuenta de servicio:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
 
--display-name="SA for codelab crf sidecar with gpu"

Usaremos esta misma cuenta de servicio que se usa como la identidad de la función de Cloud Run como la cuenta de servicio del activador de Eventarc para invocar la función de Cloud Run. Si lo prefieres, puedes crear un SA diferente para Eventarc.

gcloud projects add-iam-policy-binding $PROJECT_ID \
   
--member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
   
--role=roles/run.invoker

También otorga acceso a la cuenta de servicio para recibir eventos de Eventarc.

gcloud projects add-iam-policy-binding $PROJECT_ID \
   
--member="serviceAccount:$SERVICE_ACCOUNT_ADDRESS" \
   
--role="roles/eventarc.eventReceiver"

Crea un bucket que alojará tu modelo ajustado. En este codelab, se usa un bucket regional. También puedes usar un bucket multirregional.

gsutil mb -l $REGION gs://$BUCKET_GEMMA_NAME

Luego, otorga acceso al bucket al SA.

gcloud storage buckets add-iam-policy-binding gs://$BUCKET_GEMMA_NAME \
--member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
--role=roles/storage.objectAdmin

Ahora, crea un bucket regional que almacenará los documentos que deseas resumir. También puedes usar un bucket multirregional, siempre que actualices el activador de Eventarc según corresponda (se muestra al final de este codelab).

gsutil mb -l $REGION gs://$BUCKET_DOCS_NAME

Luego, otorga acceso al SA al bucket Gemma 3.

gcloud storage buckets add-iam-policy-binding gs://$BUCKET_GEMMA_NAME \
--member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
--role=roles/storage.objectAdmin

y el bucket de Documentos.

gcloud storage buckets add-iam-policy-binding gs://$BUCKET_DOCS_NAME \
--member=serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
--role=roles/storage.objectAdmin

Crea un repositorio de Artifact Registry para la imagen de Ollama que se usará en el contenedor secundario

gcloud artifacts repositories create $AR_REPO \
   
--repository-format=docker \
   
--location=$REGION \
   
--description="codelab for CR function and gpu sidecar" \
   
--project=$PROJECT_ID

4. Descarga el modelo de Gemma 3

Primero, descarga el modelo Gemma 3 4b de ollama. Para ello, instala ollama y, luego, ejecuta el modelo gemma3:4b de forma local.

curl -fsSL https://ollama.com/install.sh | sh
ollama serve

Ahora, en otra ventana de la terminal, ejecuta el siguiente comando para descargar el modelo. Si usas Cloud Shell, puedes abrir una ventana de terminal adicional haciendo clic en el ícono de signo más en la barra de menú de la parte superior derecha.

ollama run gemma3:4b

Una vez que ollama esté en ejecución, no dudes en hacerle algunas preguntas al modelo, p.ej.,

"why is the sky blue?"

Cuando termines de chatear con ollama, puedes salir del chat ejecutando

/bye

Luego, en la primera ventana de la terminal, ejecuta el siguiente comando para dejar de entregar ollama de forma local:

# on Linux / Cloud Shell press Ctrl^C or equivalent for your shell

Aquí puedes encontrar dónde descarga Ollama los modelos según tu sistema operativo.

https://github.com/ollama/ollama/blob/main/docs/faq.md#where-are-models-stored

Si usas Cloud Workstations, puedes encontrar los modelos de ollama descargados aquí /home/$USER/.ollama/models

Confirma que tus modelos se almacenen aquí:

ls /home/$USER/.ollama/models

Ahora, mueve el modelo gemma3:4b a tu bucket de GCS.

gsutil cp -r /home/$USER/.ollama/models gs://$BUCKET_GEMMA_NAME

5. Crea la función de Cloud Run

Crea una carpeta raíz para tu código fuente.

mkdir codelab-crf-sidecar-gpu &&
cd codelab
-crf-sidecar-gpu &&
mkdir cr
-function &&
mkdir ollama
-gemma3 &&
cd cr
-function

Crea una subcarpeta llamada src. Dentro de la carpeta, crea un archivo llamado index.ts.

mkdir src &&
touch src
/index.ts

Actualiza index.ts con el siguiente código:

//import util from 'util';
import { cloudEvent, CloudEvent } from "@google-cloud/functions-framework";
import { StorageObjectData } from "@google/events/cloud/storage/v1/StorageObjectData";
import { Storage } from "@google-cloud/storage";

// Initialize the Cloud Storage client
const storage = new Storage();

import { genkit } from 'genkit';
import { ollama } from 'genkitx-ollama';

const ai = genkit({
   
plugins: [
       
ollama({
           
models: [
               
{
                   
name: 'gemma3:4b',
                   
type: 'generate', // type: 'chat' | 'generate' | undefined
               
},
           
],
           
serverAddress: 'http://127.0.0.1:11434', // default local address
       
}),
   
],
});


// Register a CloudEvent callback with the Functions Framework that will
// be triggered by Cloud Storage.

//functions.cloudEvent('helloGCS', await cloudEvent => {
cloudEvent("gcs-cloudevent", async (cloudevent: CloudEvent<StorageObjectData>) => {
   
console.log("---------------\nProcessing for ", cloudevent.subject, "\n---------------");

   
if (cloudevent.data) {

       
const data = cloudevent.data;

       
if (data && data.bucket && data.name) {
           
const bucketName = cloudevent.data.bucket;
           
const fileName = cloudevent.data.name;
           
const filePath = `${cloudevent.data.bucket}/${cloudevent.data.name}`;

           
console.log(`Attempting to download: ${filePath}`);

           
try {
               
// Get a reference to the bucket
               
const bucket = storage.bucket(bucketName!);

               
// Get a reference to the file
               
const file = bucket.file(fileName!);

               
// Download the file's contents
               
const [content] = await file.download();

               
// 'content' is a Buffer. Convert it to a string.
               
const fileContent = content.toString('utf8');

               
console.log(`Sending file to Gemma 3 for summarization`);
               
const { text } = await ai.generate({
                   
model: 'ollama/gemma3:4b',
                   
prompt: `Summarize the following document in just a few sentences ${fileContent}`,
               
});

               
console.log(text);

           
} catch (error: any) {

               
console.error('An error occurred:', error.message);
           
}
       
} else {
           
console.warn("CloudEvent bucket name is missing!", cloudevent);
       
}
   
} else {
       
console.warn("CloudEvent data is missing!", cloudevent);
   
}
});

Ahora, en el directorio raíz crf-sidecar-gpu, crea un archivo llamado package.json con el siguiente contenido:

{
    "main": "lib/index.js",
    "name": "ingress-crf-genkit",
    "version": "1.0.0",
    "scripts": {
        "build": "tsc"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "description": "",
    "dependencies": {
        "@google-cloud/functions-framework": "^3.4.0",
        "@google-cloud/storage": "^7.0.0",
        "genkit": "^1.1.0",
        "genkitx-ollama": "^1.1.0",
        "@google/events": "^5.4.0"
    },
    "devDependencies": {
        "typescript": "^5.5.2"
    }
}

Crea un tsconfig.json también a nivel del directorio raíz con el siguiente contenido:

{
 
"compileOnSave": true,
 
"include": [
   
"src"
 
],
 
"compilerOptions": {
   
"module": "commonjs",
   
"noImplicitReturns": true,
   
"outDir": "lib",
   
"sourceMap": true,
   
"strict": true,
   
"target": "es2017",
   
"skipLibCheck": true,
   
"esModuleInterop": true
 
}
}

6. Implementa la función

En este paso, ejecutarás el siguiente comando para implementar la función de Cloud Run.

Nota: La cantidad máxima de instancias debe establecerse en un número menor o igual que tu cuota de GPU.

gcloud beta run deploy $FUNCTION_NAME \
  --region $REGION \
  --function gcs-cloudevent \
  --base-image nodejs22 \
  --source . \
  --no-allow-unauthenticated \
  --max-instances 2 # this should be less than or equal to your GPU quota

7. Crea el sidecar

Puedes obtener más información sobre cómo alojar Ollama en un servicio de Cloud Run en https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama

Ingresa al directorio de tu Sidecar:

cd ../ollama-gemma3

Crea un archivo Dockerfile con el siguiente contenido:

FROM ollama/ollama:latest

# Listen on all interfaces, port 11434
ENV OLLAMA_HOST 0.0.0.0:11434

# Store model weight files in /models
ENV OLLAMA_MODELS /models

# Reduce logging verbosity
ENV OLLAMA_DEBUG false

# Never unload model weights from the GPU
ENV OLLAMA_KEEP_ALIVE -1

# Store the model weights in the container image
ENV MODEL gemma3:4b
RUN ollama serve & sleep 5 && ollama pull $MODEL

# Start Ollama
ENTRYPOINT ["ollama", "serve"]

Compila la imagen

gcloud builds submit \
   --tag $REGION-docker.pkg.dev/$PROJECT_ID/$AR_REPO/ollama-gemma3 \
   --machine-type e2-highcpu-32

8. Actualiza la función con el sidecar

Para agregar un contenedor secundario a un servicio, trabajo o función existente, puedes actualizar el archivo YAML para que contenga el contenedor secundario.

Ejecuta el siguiente comando para recuperar el archivo YAML de la función de Cloud Run que acabas de implementar:

gcloud run services describe $FUNCTION_NAME --format=export > add-sidecar-service.yaml

Ahora, agrega el archivo adicional al CRf actualizando el archivo YAML de la siguiente manera:

  1. Inserta el siguiente fragmento YAML directamente sobre la línea runtimeClassName: run.googleapis.com/linux-base-image-update. El -image debe alinearse con el elemento -image del contenedor de entrada.
    - image: YOUR_IMAGE_SIDECAR:latest
        name
: gemma-sidecar
        env
:
       
- name: OLLAMA_FLASH_ATTENTION
          value
: '1'
        resources
:
          limits
:
            cpu
: 6000m
            nvidia
.com/gpu: '1'
            memory
: 16Gi
        volumeMounts
:
       
- name: gcs-1
          mountPath
: /root/.ollama
        startupProbe
:
          failureThreshold
: 2
          httpGet
:
            path
: /
            port: 11434
          initialDelaySeconds: 60
          periodSeconds: 60
          timeoutSeconds: 60
      nodeSelector:
        run.googleapis.com/
accelerator: nvidia-l4
      volumes
:
       
- csi:
            driver
: gcsfuse.run.googleapis.com
            volumeAttributes
:
              bucketName
: YOUR_BUCKET_GEMMA_NAME
          name
: gcs-1
  1. Ejecuta el siguiente comando para actualizar el fragmento YAML con tus variables de entorno:
sed -i "s|YOUR_IMAGE_SIDECAR|$IMAGE_SIDECAR|; s|YOUR_BUCKET_GEMMA_NAME|$BUCKET_GEMMA_NAME|" add-sidecar-service.yaml

Tu archivo YAML completo debería verse de la siguiente manera:

##############################################
# DO NOT COPY - For illustration purposes only
##############################################

apiVersion
: serving.knative.dev/v1
kind
: Service
metadata
:
  annotations
:    
    run
.googleapis.com/build-base-image: us-central1-docker.pkg.dev/serverless-runtimes/google-22/runtimes/nodejs22
    run
.googleapis.com/build-enable-automatic-updates: 'true'
    run
.googleapis.com/build-function-target: gcs-cloudevent
    run
.googleapis.com/build-id: f0122905-a556-4000-ace4-5c004a9f9ec6
    run
.googleapis.com/build-image-uri:<YOUR_IMAGE_CRF>
    run
.googleapis.com/build-name: <YOUR_BUILD_NAME>
    run
.googleapis.com/build-source-location: <YOUR_SOURCE_LOCATION>
    run
.googleapis.com/ingress: all
    run
.googleapis.com/ingress-status: all
    run
.googleapis.com/urls: '["<YOUR_CLOUD_RUN_FUNCTION_URLS"]'
  labels
:
    cloud
.googleapis.com/location: <YOUR_REGION>
  name
: <YOUR_FUNCTION_NAME>
 
namespace: '392295011265'
spec
:
 
template:
    metadata
:
      annotations
:
        autoscaling
.knative.dev/maxScale: '4'
        run
.googleapis.com/base-images: '{"":"us-central1-docker.pkg.dev/serverless-runtimes/google-22/runtimes/nodejs22"}'
        run
.googleapis.com/client-name: gcloud
        run
.googleapis.com/client-version: 514.0.0
        run
.googleapis.com/startup-cpu-boost: 'true'
      labels
:
        client
.knative.dev/nonce: hzhhrhheyd
        run
.googleapis.com/startupProbeType: Default
    spec
:
      containerConcurrency
: 80
      containers
:
     
- image: <YOUR_FUNCTION_IMAGE>
        ports
:
       
- containerPort: 8080
          name
: http1
        resources
:
          limits
:
            cpu
: 1000m
            memory
: 512Mi
        startupProbe
:
          failureThreshold
: 1
          periodSeconds
: 240
          tcpSocket
:
            port
: 8080
          timeoutSeconds
: 240
     
- image: <YOUR_SIDECAR_IMAGE>:latest
        name
: gemma-sidecar
        env
:
       
- name: OLLAMA_FLASH_ATTENTION
          value
: '1'
        resources
:
          limits
:
            cpu
: 6000m
            nvidia
.com/gpu: '1'
            memory
: 16Gi
        volumeMounts
:
       
- name: gcs-1
          mountPath
: /root/.ollama
        startupProbe
:
          failureThreshold
: 2
          httpGet
:
            path
: /
            port: 11434
          initialDelaySeconds: 60
          periodSeconds: 60
          timeoutSeconds: 60
      nodeSelector:
        run.googleapis.com/
accelerator: nvidia-l4
      volumes
:
       
- csi:
            driver
: gcsfuse.run.googleapis.com
            volumeAttributes
:
              bucketName
: <YOUR_BUCKET_NAME>
          name
: gcs-1
      runtimeClassName
: run.googleapis.com/linux-base-image-update
      serviceAccountName
: <YOUR_SA_ADDRESS>
      timeoutSeconds
: 300
  traffic
:
 
- latestRevision: true
    percent
: 100

##############################################
# DO NOT COPY - For illustration purposes only
##############################################

Ahora, ejecuta el siguiente comando para actualizar la función con el contenedor secundario.

gcloud run services replace add-sidecar-service.yaml

Por último, crea el activador de Eventarc para la función. Este comando también lo agrega a la función.

Nota: Si creaste un bucket multirregional, te recomendamos que cambies el parámetro --location.

gcloud eventarc triggers create my-crf-summary-trigger  \
    --location=$REGION \
    --destination-run-service=$FUNCTION_NAME  \
    --destination-run-region=$REGION \
    --event-filters="type=google.cloud.storage.object.v1.finalized" \
    --event-filters="bucket=$BUCKET_DOCS_NAME" \
    --service-account=$SERVICE_ACCOUNT_ADDRESS

9. Prueba la función

Sube un archivo de texto sin formato para que se resuma. ¿No sabes qué resumir? Pídele a Gemini una descripción rápida de 1 o 2 páginas sobre la historia de los perros. Luego, sube ese archivo de texto sin formato a tu bucket de $BUCKET_DOCS_NAME para que el modelo Gemma3:4b escriba un resumen en los registros de la función.

En los registros, verás algo similar a lo siguiente:

---------------
Processing for objects/dogs.txt
---------------
Attempting to download: <YOUR_PROJECT_ID>-codelab-crf-sidecar-gpu-docs/dogs.txt
Sending file to Gemma 3 for summarization
...
Here's a concise summary of the document "Humanity's Best Friend":
The dog's domestication, beginning roughly 20,000-40,000 years ago, represents a unique, deeply intertwined evolutionary partnership with humans, predating the domestication of any other animal
<
...>
solidifying their long-standing role as humanity's best friend.

10. Solución de problemas

Estos son algunos errores de escritura que podrías encontrar:

  1. Si recibes un error que indica PORT 8080 is in use, asegúrate de que tu Dockerfile para el contenedor lateral de Ollama use el puerto 11434. Además, asegúrate de usar la imagen del Sidecar correcta en caso de que tengas varias imágenes de Ollama en tu repositorio de RA. La función de Cloud Run se entrega en el puerto 8080 y, si usaste una imagen de Ollama diferente como el contenedor auxiliar que también se entrega en 8080, verás este error.
  2. Si obtienes el error failed to build: (error ID: 7485c5b6): function.js does not exist, asegúrate de que los archivos package.json y tsconfig.json estén en el mismo nivel que el directorio src.
  3. Si recibes el error ERROR: (gcloud.run.services.replace) spec.template.spec.node_selector: Max instances must be set to 4 or fewer in order to set GPU requirements., en tu archivo YAML, cambia autoscaling.knative.dev/maxScale: '100' a 1 o a un valor inferior o igual a tu cuota de GPU.