Come ospitare un LLM in un sidecar per una funzione Cloud Run

1. Introduzione

Panoramica

In questo codelab, imparerai a ospitare un modello gemma3:4b in un sidecar per una funzione Cloud Run. Quando un file viene caricato in un bucket Cloud Storage, viene attivata la funzione Cloud Run. La funzione invierà i contenuti del file a Gemma 3 nel riquadro laterale per la creazione del riassunto.

Cosa imparerai a fare

  • Come eseguire l'inferenza utilizzando una funzione Cloud Run e un LLM ospitato in un sidecar utilizzando le GPU
  • Come utilizzare la configurazione dell'uscita VPC diretto per una GPU Cloud Run per un caricamento e una pubblicazione più rapidi del modello
  • Come utilizzare Genkit per interagire con il modello Ollama ospitato

2. Prima di iniziare

Per utilizzare la funzionalità delle GPU, devi richiedere un aumento della quota per una regione supportata. La quota necessaria è nvidia_l4_gpu_allocation_no_zonal_redundancy, che si trova in API Cloud Run Admin. Ecco il link diretto per richiedere la quota.

3. Configurazione e requisiti

Imposta le variabili di ambiente che verranno utilizzate durante questo 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

Crea il service account eseguendo questo comando:

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

Utilizzeremo lo stesso service account utilizzato come identità della funzione Cloud Run come service account per il trigger Eventarc per richiamare la funzione Cloud Run. Se preferisci, puoi creare un service account diverso per Eventarc.

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

Concedi inoltre all'account di servizio l'accesso per ricevere eventi Eventarc.

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

Crea un bucket che ospiterà il modello ottimizzato. Questo codelab utilizza un bucket regionale. Puoi anche utilizzare un bucket multiregionale.

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

e poi concedi all'SA l'accesso al bucket.

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

Ora crea un bucket regionale in cui archiviare i documenti di cui vuoi riassumere il contenuto. Puoi utilizzare anche un bucket multiregionale, a condizione che aggiorni il trigger Eventarc di conseguenza (come mostrato alla fine di questo codelab).

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

e poi concedi all'account di servizio l'accesso al bucket Gemma 3.

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

e il bucket Docs.

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

Crea un repository Artifact Registry per l'immagine Ollama che verrà utilizzata nel sidecar

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

4. Scarica il modello Gemma 3

Per prima cosa, scarica il modello Gemma 3 4b da ollama. Per farlo, installa ollama ed esegui il modello gemma3:4b localmente.

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

Ora, in una finestra del terminale separata, esegui il seguente comando per scaricare il modello. Se utilizzi Cloud Shell, puoi aprire una finestra del terminale aggiuntiva facendo clic sull'icona Più nella barra dei menu in alto a destra.

ollama run gemma3:4b

Una volta eseguito ollama, puoi fare qualche domanda al modello, ad esempio:

"why is the sky blue?"

Al termine della chat con ollama, puoi uscire dalla chat eseguendo

/bye

Quindi, nella prima finestra del terminale, esegui questo comando per interrompere la pubblicazione di ollama in locale

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

Puoi trovare dove Ollama scarica i modelli a seconda del tuo sistema operativo qui.

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

Se utilizzi Cloud Workstations, puoi trovare i modelli ollama scaricati qui /home/$USER/.ollama/models

Verifica che i tuoi modelli siano ospitati qui:

ls /home/$USER/.ollama/models

ora sposta il modello gemma3:4b nel bucket GCS

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

5. Crea la funzione Cloud Run

Crea una cartella principale per il codice sorgente.

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

Crea una sottocartella denominata src. All'interno della cartella, crea un file denominato index.ts

mkdir src &&
touch src/index.ts

Aggiorna index.ts con il seguente codice:

//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);
    }
});

Ora, nella directory radice crf-sidecar-gpu, crea un file denominato package.json con il seguente contenuto:

{
    "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 file tsconfig.json anche a livello di directory radice con il seguente contenuto:

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

6. esegui il deployment della funzione

In questo passaggio, eseguirai il deployment della funzione Cloud Run eseguendo il seguente comando.

Nota: il numero massimo di istanze deve essere impostato su un numero inferiore o uguale alla quota di 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 il file sidecar

Puoi scoprire di più sull'hosting di Ollama all'interno di un servizio Cloud Run all'indirizzo https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama

Sposta nella directory del tuo sidecar:

cd ../ollama-gemma3

Crea un file Dockerfile con il seguente contenuto:

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

Crea l'immagine

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

8. Aggiorna la funzione con il sidecar

Per aggiungere un sidecar a un servizio, un job o una funzione esistenti, puoi aggiornare il file YAML in modo che contenga il sidecar.

Recupera il file YAML per la funzione Cloud Run di cui hai appena eseguito il deployment eseguendo:

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

Ora aggiungi il sidecar al CR aggiornando il file YAML come segue:

  1. inserisci il seguente frammento YAML direttamente sopra la riga runtimeClassName: run.googleapis.com/linux-base-image-update. -image deve essere allineato all'elemento del container di ingresso -image
    - 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. Esegui questo comando per aggiornare il frammento YAML con le tue variabili di ambiente:
sed -i "s|YOUR_IMAGE_SIDECAR|$IMAGE_SIDECAR|; s|YOUR_BUCKET_GEMMA_NAME|$BUCKET_GEMMA_NAME|" add-sidecar-service.yaml

Il file YAML completato dovrebbe avere un aspetto simile al seguente:

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

Ora aggiorna la funzione con il sidecar eseguendo il seguente comando.

gcloud run services replace add-sidecar-service.yaml

Infine, crea il trigger Eventarc per la funzione. Questo comando lo aggiunge anche alla funzione.

Nota: se hai creato un bucket multiregionale, ti consigliamo di modificare il parametro --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. Testare la funzione

Carica un file di testo normale per il riepilogo. Non sai cosa riassumere? Chiedi a Gemini una breve descrizione di 1-2 pagine della storia dei cani. Poi carica il file di testo normale nel bucket $BUCKET_DOCS_NAME per consentire al modello Gemma3:4b di scrivere un riepilogo nei log della funzione.

Nei log vedrai un messaggio simile al seguente:

---------------
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. Risoluzione dei problemi

Ecco alcuni errori o refusi che potresti riscontrare:

  1. Se ricevi un errore che indica PORT 8080 is in use, assicurati che il Dockerfile per il sidecar Ollama utilizzi la porta 11434. Assicurati inoltre di utilizzare l'immagine sidecar corretta nel caso in cui tu abbia più immagini Ollama nel repository AR. La funzione Cloud Run viene eseguita sulla porta 8080 e, se hai utilizzato un'immagine Ollama diversa come sidecar che viene eseguita anche sulla porta 8080, si verifica questo errore.
  2. Se ricevi l'errore failed to build: (error ID: 7485c5b6): function.js does not exist, assicurati che i file package.json e tsconfig.json si trovino allo stesso livello della directory src.
  3. Se ricevi l'errore 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., nel file YAML, modifica autoscaling.knative.dev/maxScale: '100' impostando il valore 1 o un valore inferiore o uguale alla quota GPU.