gcp.vertex.AiEndpoint自身不接收模型,模型需通过DeployModel API单独部署;network参数要求project NUMBER而非project ID,这些是常见陷阱。
gcp.vertex.AiEndpoint 只创建推理表面,不做其他任何事。Pulumi 的 registry 文档写得很清楚:模型是通过 API 的 EndpointService.DeployModel 和 EndpointService.UndeployModel 操作在之后部署到端点中的。该资源暴露了 trafficSplit,这是一个从已部署模型 ID 到百分比的映射——而这些 ID 是由该资源本身从未发出的 deploy 调用生成的。
因此,只包含 AiEndpoint 的程序最终会得到一个不接受任何预测请求的空端点。它没有坏,只是做完了,而这不是你想要的全部。有几个属性值得了解,因为其中一些一旦设置就不可变:
us-central1。不可原地修改。projects/PROJECT_NUMBER/global/networks/NAME 的完整 VPC 路径。它接收的是项目编号,不是项目 ID,这是一个常见的静默失败场景。如果模型是 Model Garden 发布商模型或 Hugging Face 模型,有一个资源可以完成全部工作:gcp.vertex.AiEndpointWithModelGardenDeployment。它将端点创建和模型部署作为一个单元完成,这正是你最初想要的。
import * as gcp from "@pulumi/gcp";
const served = new gcp.vertex.AiEndpointWithModelGardenDeployment("served", {
location: "us-central1",
publisherModelName: "publishers/google/models/paligemma@paligemma-224-float32",
modelConfig: {
acceptEula: true,
modelDisplayName: "paligemma-224",
},
deployConfig: {
dedicatedResources: {
machineSpec: {
machineType: "g2-standard-16",
acceleratorType: "NVIDIA_L4",
acceleratorCount: 1,
},
minReplicaCount: 1,
},
},
});
acceptEula 不是装饰。Model Garden 条目附有许可条款,没有这个标志部署会失败,所以对大多数模型来说这个标志实际上是必填的。对于 Hugging Face 模型,换掉 publisherModelName 为 huggingFaceModelId,并为受限制仓库提供 modelConfig.huggingFaceAccessToken。
对于你自己训练或容器化的模型,没有等价单资源可用,这是人们写出有问题程序的地方。正确的形态是:声明式创建端点,上传模型,然后将 deploy 作为 Pulumi 触发而不是建模的显式步骤执行。
gcp.vertex.AiEndpoint 创建端点,导出其 ID。import * as command from "@pulumiverse/command"; // or @pulumi/command
import * as pulumi from "@pulumi/pulumi";
const endpoint = new gcp.vertex.AiEndpoint("inference", {
name: "inference",
displayName: "inference",
location: "us-central1",
region: "us-central1",
});
const deploy = new command.local.Command("deploy-model", {
create: pulumi.interpolate`gcloud ai endpoints deploy-model ${endpoint.name} \
--region=us-central1 \
--model=${modelId} \
--display-name=inference-v${modelVersion} \
--machine-type=g2-standard-8 \
--accelerator=type=nvidia-l4,count=1 \
--min-replica-count=1 --max-replica-count=3 \
--traffic-split=0=100`,
triggers: [modelVersion],
});
这是一个折中方案,应该在代码中标注出来。command 资源没有读步骤,所以 Pulumi 无法检测部署中的漂移——如果有人手动取消了模型的部署,下次 pulumi up 不会注意到。接受这一点,或者写一个动态 provider 对 Vertex API 做真实的读操作。同样的权衡也出现在 Cloudflare Vectorize 案例中,那个 provider 同样缺少一个资源。
machineSpec.machineType 和 machineSpec.acceleratorType 不是独立自由的。Google 约束了哪些加速器可以挂载到哪些机器系列——G2 系列是用来承载 L4 GPU 的,A2 或 A3 机器类型是获取 A100 或 H100 类硬件的方式。选择不匹配的组合会在 deploy 时而不是 plan 时产生错误,因为 Pulumi 没有验证这个组合,是 API 在验证。
minReplicaCount 是值得认真讨论的数字。Vertex 对专用端点按已配置副本数收费,而不是按请求收费,所以在开发端点上设置 minReplicaCount: 1 就是一台持续运行着 GPU 的机器,不管有没有人调用它。这和空闲 GPU 节点池的失败模式相同,只是经由不同的门进来了。
机器类型和加速器可用性因区域而异,且会随着新硬件出现而变化。部署前查看目标区域的 Vertex AI 文档,不要复用示例中的机器类型。
前文列出的几个端点属性无法原地修改,所以编辑它们是删除加创建。在一个生产端点上,这会造成停机,而预览是你发现问题的地方:Pulumi 打印 replace 并标记 diff,而不是 update。在确认之前读一下那个词。region、VPC 网络和加密规范是常见的罪魁祸首,这三个都是有人以为只是 cosmetic 编辑但实际会产生破坏的属性。
保护你承受不起丢失的那些。Pulumi 的 resource options 是机制,其中三个在生产端点上值得使用:
const endpoint = new gcp.vertex.AiEndpoint("inference", {
name: "inference",
displayName: "inference",
location: "us-central1",
region: "us-central1",
}, {
protect: true, // 在移除之前 destroy 会被拒绝
ignoreChanges: ["trafficSplit"], // 由 deploy 步骤拥有,不由这个程序拥有
retainOnDelete: false,
});
protect 使 pulumi destroy 在该资源上失败,直到有人故意取消它,这正是你在误输入栈名和删除生产端点之间想要的摩擦力。retainOnDelete 是另一种更锋利的工具:它从状态中删除资源但将其保留在云中,这在将所有权移交给另一个栈时有用,否则是危险的,因为资源会继续计费而没有任何东西追踪它。
ignoreChanges 对 trafficSplit 是针对这个资源特定问题的特定修复。流量拆分是一个以已部署模型 ID 为 key 的 map,而这些 ID 是由你的 Pulumi 程序从未发出的 deploy 调用生成的。如果你声明了这个字段,每次带外 deploy 都会使状态和现实不同步;如果你忽略它,deploy 步骤拥有它,程序就停止了争执。声明了它然后和 diff 搏斗是唯一没有好处的选项。
pulumi refresh 是将状态与实际存在的资源协调一致的命令,在有风险的更新之前值得运行。但要清楚它的限制:refresh 让每个 provider 读取其资源,所以它只能看到 provider 能读取的内容。驱动自定义模型 deploy 的 command 资源完全没有读实现,这意味着 refresh 干净地返回,但对模型是否仍在部署状态什么都没验证。这个栈上的一次绿色 refresh 是一个比看起来更弱的声明。
最后是并发问题。Pulumi Cloud 对栈授予租约,这样同一时间最多只有一个更新在运行,第二个会失败并返回 409 和"另一个更新当前正在进行中"的消息。pulumi cancel 撤销那个租约——Pulumi 自己的故障排除文档警告说,取消别人的更新会使他们的更新立即失败。用对待 Terraform force-unlock 的方式对待它:先确认没有东西在运行。
Vertex 端点和托管 API 模型在调用代码中看起来不一样:认证不同、请求结构不同、流式包装不同、错误分类不同。如果你在自托管端点旁边还保留了托管模型——出于成本、微调或作为备用的考虑——必须有东西将这两个规范化为一个接口,否则每个调用点都要学习两者。这种规范化正是 Multigrid 等 LLM 网关所做的;自己写也完全合理,只要只写一次。
仍有模型部署在上面的端点无法删除。在 Model Garden 路径上,AiEndpointWithModelGardenDeployment 暴露了一个 deletionPolicy,控制底层端点和模型是否随它一起删除——主动设置它,因为默认值可能会留下孤儿继续计费。
在自定义路径上,command 资源需要一个匹配的 delete 来在 Pulumi 销毁端点之前取消部署模型,否则 pulumi destroy 会在中途失败并留下一个需要手动理清的半销毁状态。这是将开发和生产端点放在不同栈中的最强论据:开发中的一次失败 destroy 永远不应该能碰到生产状态。