Jak hostować LLM w kontenerze pomocniczym dla funkcji Cloud Run

1. Wprowadzenie

Przegląd

Z tego ćwiczenia dowiesz się, jak hostować model gemma3:4b w kontenerze pomocniczym funkcji Cloud Run. Gdy plik zostanie przesłany do zasobnika Cloud Storage, uruchomi to funkcję Cloud Run. Funkcja wyśle zawartość pliku do modelu Gemma 3 w pliku pomocniczym w celu podsumowania.

Czego się nauczysz

  • Jak przeprowadzać wnioskowanie za pomocą funkcji Cloud Run i LLM hostowanego w kontenerze pomocniczym z użyciem GPU
  • Jak używać konfiguracji ruchu wychodzącego z bezpośredniej sieci VPC w przypadku GPU w Cloud Run, aby szybciej przesyłać i udostępniać model
  • Jak używać genkit do komunikacji z hostowanym modelem ollama

2. Zanim zaczniesz

Aby korzystać z funkcji GPU, musisz poprosić o zwiększenie limitu w obsługiwanym regionie. Wymagany limit to nvidia_l4_gpu_allocation_no_zonal_redundancy, który znajduje się w Cloud Run Admin API. Oto bezpośredni link do prośby o zwiększenie limitu.

3. Konfiguracja i wymagania

Ustaw zmienne środowiskowe, które będą używane w tym module.

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

Utwórz konto usługi, uruchamiając to polecenie:

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

Użyjemy tego samego konta usługi, które jest używane jako tożsamość funkcji Cloud Run, jako konta usługi aktywatora Eventarc do wywoływania funkcji Cloud Run. Jeśli chcesz, możesz utworzyć inną SA dla Eventarc.

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

Przyznaj też kontu usługi dostęp do otrzymywania zdarzeń Eventarc.

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

Utwórz zasobnik, w którym będzie przechowywany dostrojony model. W tym ćwiczeniu używamy zasobnika regionalnego. Możesz też użyć zasobnika wieloregionowego.

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

Następnie przyznaj SA dostęp do zasobnika.

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

Teraz utwórz regionalny zasobnik, w którym będą przechowywane dokumenty, które chcesz podsumować. Możesz też użyć zasobnika wieloregionowego, pod warunkiem że odpowiednio zaktualizujesz aktywator Eventarc (pokazany na końcu tego ćwiczenia).

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

Następnie przyznaj SA dostęp do zasobnika Gemma 3.

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

i zasobnik Dokumentów.

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

Utwórz repozytorium Artifact Registry dla obrazu Ollama, który będzie używany w kontenerze pomocniczym.

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

4. Pobieranie modelu Gemma 3

Najpierw pobierz model Gemma 3 4b z ollama. Aby to zrobić, zainstaluj ollama, a potem uruchom lokalnie model gemma3:4b.

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

Teraz w osobnym oknie terminala uruchom to polecenie, aby pobrać model. Jeśli korzystasz z Cloud Shell, możesz otworzyć dodatkowe okno terminala, klikając ikonę plusa na pasku menu w prawym górnym rogu.

ollama run gemma3:4b

Gdy ollama będzie działać, możesz zadać modelowi pytania, np.

"why is the sky blue?"

Gdy skończysz czatować z ollamą, możesz zamknąć czat, wpisując

/bye

Następnie w pierwszym oknie terminala uruchom to polecenie, aby zatrzymać lokalne udostępnianie usługi Ollama:

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

Tutaj znajdziesz informacje o tym, gdzie Ollama pobiera modele w zależności od systemu operacyjnego.

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

Jeśli używasz Cloud Workstations, pobrane modele Ollama znajdziesz tutaj: /home/$USER/.ollama/models

Sprawdź, czy Twoje modele są hostowane tutaj:

ls /home/$USER/.ollama/models

teraz przenieś model gemma3:4b do zasobnika GCS.

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

5. Tworzenie funkcji Cloud Run

Utwórz folder główny dla kodu źródłowego.

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

Utwórz podfolder o nazwie src. W folderze utwórz plik o nazwie index.ts.

mkdir src &&
touch src/index.ts

Zaktualizuj plik index.ts za pomocą tego kodu:

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

Teraz w katalogu głównym crf-sidecar-gpu utwórz plik o nazwie package.json z tą zawartością:

{
    "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"
    }
}

Utwórz plik tsconfig.json w katalogu głównym z następującą treścią:

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

6. Wdrażanie funkcji

W tym kroku wdrożysz funkcję Cloud Run, uruchamiając to polecenie.

Uwaga: maksymalna liczba instancji powinna być mniejsza lub równa limitowi 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. Tworzenie pliku pomocniczego

Więcej informacji o hostowaniu Ollamy w usłudze Cloud Run znajdziesz na stronie https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama

Przejdź do katalogu z aplikacją pomocniczą:

cd ../ollama-gemma3

Utwórz plik Dockerfile o tej zawartości:

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

Tworzenie obrazu

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

8. Aktualizowanie funkcji za pomocą kontenera dodatkowego

Aby dodać kontener pomocniczy do istniejącej usługi, zadania lub funkcji, możesz zaktualizować plik YAML, aby zawierał kontener pomocniczy.

Pobierz plik YAML wdrożonej właśnie funkcji Cloud Run, uruchamiając to polecenie:

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

Teraz dodaj kontener dodatkowy do CRf, aktualizując plik YAML w ten sposób:

  1. wstaw ten fragment YAML bezpośrednio nad wierszem runtimeClassName: run.googleapis.com/linux-base-image-update. -image musi być wyrównany z elementem kontenera Ingress -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. Aby zaktualizować fragment YAML za pomocą zmiennych środowiskowych, uruchom to polecenie:
sed -i "s|YOUR_IMAGE_SIDECAR|$IMAGE_SIDECAR|; s|YOUR_BUCKET_GEMMA_NAME|$BUCKET_GEMMA_NAME|" add-sidecar-service.yaml

Ukończony plik YAML powinien wyglądać mniej więcej tak:

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

Teraz zaktualizuj funkcję za pomocą kontenera dodatkowego, uruchamiając to polecenie.

gcloud run services replace add-sidecar-service.yaml

Na koniec utwórz aktywator Eventarc dla funkcji. To polecenie dodaje też wartość do funkcji.

Uwaga: jeśli utworzysz zasobnik w wielu regionach, musisz zmienić parametr --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. Testowanie funkcji

Prześlij plik w formacie zwykłego tekstu do podsumowania. Nie wiesz, co streścić? Zapytaj Gemini o szybki opis historii psów na 1–2 stronach. Następnie prześlij ten plik tekstowy do zasobnika $BUCKET_DOCS_NAME, aby model Gemma3:4b zapisał podsumowanie w logach funkcji.

W dziennikach zobaczysz tekst podobny do tego:

---------------
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. Rozwiązywanie problemów

Oto niektóre błędy, które możesz napotkać:

  1. Jeśli pojawi się błąd PORT 8080 is in use, upewnij się, że plik Dockerfile dla kontenera pomocniczego Ollama korzysta z portu 11434. Sprawdź też, czy używasz prawidłowego obrazu pomocniczego, jeśli w repozytorium AR masz kilka obrazów Ollamy. Funkcja Cloud Run działa na porcie 8080, a jeśli jako kontener dodatkowy używasz innego obrazu Ollamy, który też działa na porcie 8080, wystąpi ten błąd.
  2. Jeśli zobaczysz błąd failed to build: (error ID: 7485c5b6): function.js does not exist, sprawdź, czy pliki package.json i tsconfig.json znajdują się na tym samym poziomie co katalog src.
  3. Jeśli pojawi się błąd 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., w pliku YAML zmień wartość autoscaling.knative.dev/maxScale: '100' na 1 lub na wartość mniejszą lub równą limitowi GPU.