如何在 Cloud Run 函数的边车中托管 LLM

如何在 Cloud Run 函数的边车中托管 LLM

关于此 Codelab

subject上次更新时间:3月 27, 2025
account_circleGoogle 员工编写

1. 简介

在此 Codelab 中,您将学习如何在 Cloud Run 函数的边车中托管 gemma3:4b 模型。将文件上传到 Cloud Storage 存储分区时,系统会触发 Cloud Run 函数。该函数会将文件内容发送到侧边栏中的 Gemma 3 进行总结。

  • 如何使用 Cloud Run 函数和使用 GPU 托管在边车中的 LLM 进行推理
  • 如何为 Cloud Run GPU 使用直接 VPC 出站流量配置,以更快地上传和分发模型
  • 如何使用 genkit 与托管的 Ollama 模型交互

2. 准备工作

如需使用 GPU 功能,您必须为受支持的区域申请增加配额。所需配额为 nvidia_l4_gpu_allocation_no_zonal_redundancy,该配额位于 Cloud Run Admin API 下。以下是用于申请配额的直接链接

3. 设置和要求

设置将在此 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

运行以下命令来创建服务账号:

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

我们将使用作为 Cloud Run 函数身份的同一服务账号作为 eventarc 触发器的服务账号来调用 Cloud Run 函数。如果您愿意,可以为 Eventarc 创建其他 SA。

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

此外,还要向该服务账号授予接收 Eventarc 事件的权限。

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

创建一个存储经过微调的模型的存储分区。此 Codelab 使用的是区域存储分区。您也可以使用多区域存储分区。

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

然后,向 SA 授予对存储分区的访问权限。

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

现在,创建一个区域存储分区,用于存储您要总结的文档。您也可以使用多区域存储分区,前提是相应地更新 Eventarc 触发器(此 Codelab 的最后部分会介绍)。

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

然后,向 SA 授予对 Gemma 3 存储分区的访问权限。

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

和文档存储分区。

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

为将在边车中使用的 Ollama 映像创建 Artifact Registry 仓库

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

4. 下载 Gemma 3 模型

首先,您需要从 ollama 下载 Gemma 3 4b 模型。为此,您可以安装 ollama,然后在本地运行 gemma3:4b 模型。

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

现在,在另一个终端窗口中,运行以下命令以拉取模型。如果您使用的是 Cloud Shell,可以点击右上角菜单栏中的加号图标,打开其他终端窗口。

ollama run gemma3:4b

ollama 运行后,您可以随意向模型提出一些问题,例如

"why is the sky blue?"

与 ollama 聊天结束后,您可以通过运行以下命令退出聊天

/bye

然后,在第一个终端窗口中,运行以下命令以停止在本地提供 ollama

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

您可以在此处找到 Ollama 下载模型的位置(具体取决于您的操作系统)。

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

如果您使用的是 Cloud Workstations,可以在此处找到下载的 ollama 模型 /home/$USER/.ollama/models

确认您的模型托管在此处:

ls /home/$USER/.ollama/models

现在,将 gemma3:4b 模型移至您的 GCS 存储分区

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

5. 创建 Cloud Run 函数

为源代码创建一个根文件夹。

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

创建一个名为 src 的子文件夹。在该文件夹中,创建一个名为 index.ts 的文件

mkdir src &&
touch src
/index.ts

使用以下代码更新 index.ts:

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

现在,在根目录 crf-sidecar-gpu 中,创建一个名为 package.json 的文件,其中包含以下内容:

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

在根目录级别创建一个 tsconfig.json,其中包含以下内容:

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

6. 部署该函数

在此步骤中,您将通过运行以下命令部署 Cloud Run 函数。

注意:实例数上限应设置为小于或等于 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. 创建 Sidecar

如需详细了解如何在 Cloud Run 服务中托管 Ollama,请访问 https://cloud.google.com/run/docs/tutorials/gpu-gemma-with-ollama

进入您的 Sidecar 目录:

cd ../ollama-gemma3

创建一个包含以下内容的 Dockerfile 文件:

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

构建映像

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

8. 使用 Sidecar 更新函数

如需向现有服务、作业或函数添加边车,您可以更新 YAML 文件以包含边车。

运行以下命令,检索您刚刚部署的 Cloud Run 函数的 YAML:

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

现在,通过更新 YAML 将边车添加到 CRf,如下所示:

  1. 直接在 runtimeClassName: run.googleapis.com/linux-base-image-update 行上方插入以下 YAML 片段。-image 应与入站容器项 -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. 运行以下命令,使用您的环境变量更新 YAML 片段:
sed -i "s|YOUR_IMAGE_SIDECAR|$IMAGE_SIDECAR|; s|YOUR_BUCKET_GEMMA_NAME|$BUCKET_GEMMA_NAME|" add-sidecar-service.yaml

完成后的 YAML 文件应如下所示:

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

现在,运行以下命令,使用边车更新函数。

gcloud run services replace add-sidecar-service.yaml

最后,为该函数创建 Eventarc 触发器。此命令还会将其添加到函数中。

注意:如果您创建了多区域存储分区,则需要更改 --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. 测试函数

上传要总结的纯文本文件。不知道要总结什么?让 Gemini 快速生成 1-2 页的狗狗历史简介!然后,将该纯文本文件上传到 Gemma3:4b 模型的 $BUCKET_DOCS_NAME 存储分区,以便将摘要写入函数日志。

在日志中,您会看到如下内容:

---------------
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. 问题排查

以下是您可能会遇到的一些拼写错误:

  1. 如果您收到 PORT 8080 is in use 错误,请确保 Ollama 边车的 Dockerfile 使用的是端口 11434。此外,如果您的 AR 代码库中有多个 Ollama 映像,请确保您使用的是正确的 Sidecar 映像。Cloud Run 函数在端口 8080 上提供服务,如果您使用其他 Ollama 映像作为也通过 8080 提供服务的边车,就会遇到此错误。
  2. 如果您收到 failed to build: (error ID: 7485c5b6): function.js does not exist 错误,请确保 package.json 和 tsconfig.json 文件与 src 目录位于同一级别。
  3. 如果您收到错误 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.,请在 YAML 文件中将 autoscaling.knative.dev/maxScale: '100' 更改为 1 或小于或等于您的 GPU 配额的值。