Cách lưu trữ LLM trong một sidecar cho hàm Cloud Run

1. Giới thiệu

Tổng quan

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách lưu trữ mô hình gemma3:4b trong một ứng dụng hỗ trợ cho hàm Cloud Run. Khi một tệp được tải lên bộ chứa Cloud Storage, tệp đó sẽ kích hoạt hàm Cloud Run. Hàm này sẽ gửi nội dung của tệp đến Gemma 3 trong sidecar để tóm tắt.

Kiến thức bạn sẽ học được

  • Cách suy luận bằng hàm Cloud Run và LLM được lưu trữ trong sidecar bằng GPU
  • Cách sử dụng cấu hình lưu lượng truy cập trực tiếp ra khỏi VPC cho GPU Cloud Run để tải lên và phân phát mô hình nhanh hơn
  • Cách sử dụng genkit để tương tác với mô hình ollama được lưu trữ

2. Trước khi bắt đầu

Để sử dụng tính năng GPU, bạn phải yêu cầu tăng hạn mức cho một khu vực được hỗ trợ. Hạn mức cần thiết là nvidia_l4_gpu_allocation_no_zonal_redundancy, nằm trong Cloud Run Admin API. Sau đây là đường liên kết trực tiếp để yêu cầu hạn mức.

3. Thiết lập và yêu cầu

Đặt các biến môi trường sẽ được dùng trong suốt lớp học lập trình này.

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

Tạo tài khoản dịch vụ bằng cách chạy lệnh sau:

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

Chúng ta sẽ sử dụng cùng một tài khoản dịch vụ đang được dùng làm danh tính của hàm Cloud Run làm tài khoản dịch vụ cho trình kích hoạt Eventarc để gọi hàm Cloud Run. Bạn có thể tạo một SA khác cho Eventarc nếu muốn.

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

Ngoài ra, hãy cấp cho tài khoản dịch vụ quyền truy cập để nhận các sự kiện Eventarc.

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

Tạo một nhóm lưu trữ mô hình được tinh chỉnh. Lớp học lập trình này sử dụng một vùng lưu trữ theo khu vực. Bạn cũng có thể sử dụng một bộ chứa đa khu vực.

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

Sau đó, cấp quyền truy cập cho SA vào bộ chứa.

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

Giờ đây, hãy tạo một vùng lưu trữ theo khu vực để lưu trữ những tài liệu bạn muốn tóm tắt. Bạn cũng có thể sử dụng một bộ chứa đa vùng, miễn là bạn cập nhật điều kiện kích hoạt Eventarc cho phù hợp (như minh hoạ ở cuối lớp học lập trình này).

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

Sau đó, cấp cho SA quyền truy cập vào bộ chứa Gemma 3.

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

và nhóm Tài liệu.

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

Tạo một kho lưu trữ Artifact Registry cho hình ảnh Ollama sẽ được dùng trong vùng chứa phụ

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

4. Tải mô hình Gemma 3 xuống

Trước tiên, bạn cần tải mô hình Gemma 3 4b xuống từ ollama. Bạn có thể thực hiện việc này bằng cách cài đặt ollama rồi chạy mô hình gemma3:4b cục bộ.

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

Giờ đây, trong một cửa sổ dòng lệnh riêng, hãy chạy lệnh sau để kéo mô hình xuống. Nếu đang sử dụng Cloud Shell, bạn có thể mở một cửa sổ dòng lệnh khác bằng cách nhấp vào biểu tượng dấu cộng trong thanh trình đơn trên cùng bên phải.

ollama run gemma3:4b

Sau khi ollama chạy, bạn có thể đặt cho mô hình một số câu hỏi, chẳng hạn như

"why is the sky blue?"

Sau khi trò chuyện xong với ollama, bạn có thể thoát khỏi cuộc trò chuyện bằng cách chạy

/bye

Sau đó, trong cửa sổ dòng lệnh đầu tiên, hãy chạy lệnh sau để ngừng phân phát ollama cục bộ

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

Bạn có thể tìm thấy vị trí Ollama tải các mô hình xuống, tuỳ thuộc vào hệ điều hành của bạn tại đây.

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

Nếu đang sử dụng Cloud Workstations, bạn có thể tìm thấy các mô hình ollama đã tải xuống tại đây /home/$USER/.ollama/models

Xác nhận rằng các mô hình của bạn được lưu trữ tại đây:

ls /home/$USER/.ollama/models

bây giờ, hãy di chuyển mô hình gemma3:4b vào bộ chứa GCS của bạn

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

5. Tạo hàm Cloud Run

Tạo một thư mục gốc cho mã nguồn của bạn.

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

Tạo một thư mục con có tên là src. Trong thư mục này, hãy tạo một tệp có tên là index.ts

mkdir src &&
touch src/index.ts

Cập nhật index.ts bằng mã sau:

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

Bây giờ, trong thư mục gốc crf-sidecar-gpu, hãy tạo một tệp có tên là package.json với nội dung sau:

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

Tạo tsconfig.json ở cấp thư mục gốc với nội dung sau:

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

6. Triển khai hàm

Trong bước này, bạn sẽ triển khai hàm Cloud Run bằng cách chạy lệnh sau.

Lưu ý: bạn nên đặt số lượng phiên bản tối đa thành một số nhỏ hơn hoặc bằng hạn mức GPU của bạn.

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. Tạo tệp trợ giúp

Bạn có thể tìm hiểu thêm về cách lưu trữ Ollama trong một dịch vụ Cloud Run tại https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama

Di chuyển vào thư mục cho sidecar:

cd ../ollama-gemma3

Tạo tệp Dockerfile có nội dung sau:

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

Xây dựng hình ảnh

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

8. Cập nhật hàm bằng sidecar

Để thêm một tệp trợ giúp vào dịch vụ, công việc hoặc hàm hiện có, bạn có thể cập nhật tệp YAML để chứa tệp trợ giúp.

Truy xuất tệp YAML cho hàm Cloud Run mà bạn vừa triển khai bằng cách chạy:

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

Bây giờ, hãy thêm sidecar vào CRf bằng cách cập nhật YAML như sau:

  1. chèn đoạn mã YAML sau đây ngay phía trên dòng runtimeClassName: run.googleapis.com/linux-base-image-update. -image phải thẳng hàng với mục vùng chứa đầu vào -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. Chạy lệnh sau để cập nhật đoạn mã YAML bằng các biến môi trường của bạn:
sed -i "s|YOUR_IMAGE_SIDECAR|$IMAGE_SIDECAR|; s|YOUR_BUCKET_GEMMA_NAME|$BUCKET_GEMMA_NAME|" add-sidecar-service.yaml

Tệp YAML hoàn chỉnh sẽ có dạng như sau:

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

Bây giờ, hãy cập nhật hàm bằng sidecar bằng cách chạy lệnh sau.

gcloud run services replace add-sidecar-service.yaml

Cuối cùng, hãy tạo trình kích hoạt eventarc cho hàm. Lệnh này cũng thêm hàm vào đó.

Lưu ý: nếu đã tạo một bộ chứa đa khu vực, bạn nên thay đổi tham số --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. Kiểm thử hàm

Tải tệp văn bản thuần tuý lên để tóm tắt. Bạn không biết nên tóm tắt nội dung nào? Yêu cầu Gemini mô tả nhanh lịch sử của loài chó trong 1 đến 2 trang! Sau đó, hãy tải tệp văn bản thuần tuý đó lên vùng chứa $BUCKET_DOCS_NAME để mô hình Gemma3:4b ghi nội dung tóm tắt vào nhật ký hàm.

Trong nhật ký, bạn sẽ thấy nội dung tương tự như sau:

---------------
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. Khắc phục sự cố

Sau đây là một số lỗi chính tả mà bạn có thể gặp phải:

  1. Nếu bạn gặp lỗi PORT 8080 is in use, hãy đảm bảo rằng Dockerfile cho Ollama sidecar đang sử dụng cổng 11434. Ngoài ra, hãy đảm bảo rằng bạn đang sử dụng tệp trợ giúp hình ảnh chính xác trong trường hợp bạn có nhiều hình ảnh Ollama trong kho lưu trữ thực tế tăng cường. Hàm Cloud Run hoạt động trên cổng 8080 và nếu bạn sử dụng một hình ảnh Ollama khác làm vùng chứa phụ cũng hoạt động trên cổng 8080, thì bạn sẽ gặp phải lỗi này.
  2. Nếu bạn gặp lỗi failed to build: (error ID: 7485c5b6): function.js does not exist, hãy đảm bảo rằng các tệp package.json và tsconfig.json nằm ở cùng cấp với thư mục src.
  3. Nếu bạn gặp lỗi 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., trong tệp YAML, hãy thay đổi autoscaling.knative.dev/maxScale: '100' thành 1 hoặc thành một giá trị nhỏ hơn hoặc bằng hạn mức GPU của bạn.