[控场AI]
· 11 分钟阅读· 5,520 字

EmbeddingGemma 2 在 Cloud Run 上:int8 CPU 与 NVIDIA L4 对决

EmbeddingGemma 2 在 Cloud Run 上:int8 CPU 与 NVIDIA L4 对决

同一嵌入模型在 Cloud Run 上以 int8 CPU 与 NVIDIA L4 部署,成本与延迟的取舍取决于工作负载形态。

EmbeddingGemma 2 项目在 Google Cloud Run 上用同一模型做了三种部署对比:int8 量化的 CPU 服务按请求计费,float32 CPU 作基准,NVIDIA L4 按实例计费。int8+int4 量化保留了原版 97.9% 的 SciFact nDCG@10,逐文本余弦 0.9888-0.9978。L4 文档吞吐是 CPU repack 的 297 倍,满载时每百万文档仅 0.87 美元,但只要在线时间超过 3.4% 在忙碌就比 CPU 划算。CPU repack 每百万查询 3.91 美元、空闲零成本,代价是 58-84 秒的冷启动。结论是按负载选型:白天零散查询用 CPU repack,批量索引用 L4。

EmbeddingGemma 2 在 Cloud Run 上:int8 CPU 与 NVIDIA L4 对决

将一个开源文本嵌入模型部署到云端,到底该选 CPU 还是 GPU?EmbeddingGemma 2 项目的作者在 Google Cloud Run 上做了这套完整的实验:同一个模型,三种部署形态,从请求延迟到每百万文档的成本,全部用真实账单数据算清楚。

EmbeddingGemma 2 on Cloud Run

为什么值得自己跑嵌入模型

托管决策 API 正在把嵌入模型多年来的工作打包出售。TypeSafe 的 Jev 于 2026 年 9 月 15 日上线,能对一段文本做类型化问答:从标签列表中选一个、把文本放到某个刻度上、或返回一句话成立的概率,定价为每百万输入 token 0.042 美元,通过 waitlist、OpenRouter 和 Vercel 的 AI Gateway 访问。

而按查询检索段落、按最接近的描述给文本打标签,本就是嵌入模型的看家本领。EmbeddingGemma 2 是开放模型,可以跑在自己的 Google Cloud 项目里,按 Cloud Run 的费率计费,文本全程不出项目边界。本文正是把这条路径按 Jev 的计价单位算清楚,并测量其速度与部署成本。

EmbeddingGemma 2 是什么

EmbeddingGemma 2 把文本转成 768 维向量,语义相近的文本在向量空间中彼此靠近。它自带视觉与音频编码器,但这个项目把它们丢掉,只加载文本塔。

向量是 Matryoshka 向量:768 维中的前 128、256 或 512 维本身就是可用的嵌入,搜索索引可以用一点质量换取更小的存储占用。模型还接受 prompt 名称——提问用 SearchQuery,待检索的段落用 Document。

Matryoshka 表示学习是一种让单个嵌入模型在不同维度截断下都能保持可用性的训练技术,名称来自俄罗斯套娃。训练时模型被要求让前若干维(如 128、256、512)各自构成一个有效的、可独立使用的嵌入,而不是只有完整 768 维才有意义。好处是搜索索引可以用更低的维度换取更小的存储和更快的最近邻检索,而无需重新训练或维护多个模型。代价是截断维度越低,检索质量下降越明显,因此需要在召回率与存储成本之间做权衡。

三种服务,一个 Dockerfile

服务权重实例计费
emb-cpu-repack仅文本,int8 线性层,int4 token 表,203 MiB1 vCPU, 2 GiB按请求
emb-cpu-original仅文本,float321 vCPU, 4 GiB按请求
emb-l4-original仅文本,bf164 vCPU, 16 GiB, 1 L4按实例

repack 由仓库中的 repack/repack_text_w8a8_emb4.py 构建:文本模型的每个线性层以 int8 存储、每个输出通道一个 scale,262,144 行的 token 表用 int4。int8 线性层跑在 PyTorch 的 fbgemm 核上,这是 CPU 核,所以 L4 服务的是 bf16 原始权重。4 vCPU 加 16 GiB 是 Cloud Run 挂载 L4 所需的最小实例规格。

部署的关键细节

CPU 服务用 --cpu-throttling 选择按请求计费——CPU 只在请求进行时分配和计费;--min-instances 0 让服务缩到零,空闲不花钱;--concurrency 1 让单个 vCPU 一次只处理一个请求。容器能看到宿主机所有核心,而 Cloud Run 配额只给一个 vCPU,因此必须设 TORCH_THREADS=1,否则 PyTorch 会按宿主核心数起线程。

L4 服务则需要 --no-gpu-zonal-redundancy,放弃第二个可用区的故障转移容量,费率从每 L4 秒 0.0002909 美元降到 0.0001867 美元。还有一个容易踩的坑:Cloud Run 保留以 z 结尾的路径,常规的 /healthz 根本到不了容器,前端直接返回 404,所以健康检查路由改用 /health。

GPU 配额按区域划分。 镜像放在 us-central1,但同一部署在该区域被拒绝——该区没有非冗余 L4 配额。作者改用 us-east4 完成部署,两地 Cloud Run 的 CPU、内存与 L4 费率在账单目录中完全一致。部署前先用 gcloud beta quotas info describe NvidiaL4GpuAllocNoZonalRedundancyPerProjectRegion 查配额。

按请求计费与按实例计费是 Cloud Run 上两种根本不同的成本模型。按请求计费(配合 --cpu-throttling)只在请求处理期间分配并结算 CPU,实例空闲时几乎不产生计算费用,适合流量稀疏、突发性强的场景。按实例计费则从实例启动到关闭的每一秒都计费,无论是否有请求进来,但换来了持续的算力与更稳定的延迟。GPU 实例目前只能用按实例计费,这也是 L4 方案在低忙碌比例下成本急剧攀升的根本原因。

精度验证:int8 量化会损失多少

这是整个项目最值得细看的部分。三个服务都要通过同一套九项检查:健康状态、四种 Matryoshka 维度下的向量长度与单位范数、128 维向量必须是 768 维的前 128 个数、非法输入被拒、以及相关段落排在两个无关段落之前。全部通过。

repack 与 float32 原版逐文本余弦相似度在 0.9888 到 0.9978 之间;L4 的 bf16 与原版 float32 几乎完全一致,余弦达 0.9999 到 1.0000。但余弦接近 0.99 只说明向量方向一致,真正的检验是检索。作者在 SciFact 上评测(300 条查询对 5,183 篇科学摘要,768 维):

权重nDCG@10recall@10与 float32 共享的 top 10
float32 原版0.8540.944100%
仅 int4 token 表0.8500.93992.7%
repack(int8 + int4)0.8360.92685.5%

repack 保留了原版 97.9% 的 nDCG@10。量化带来的精度损失明确、可控,这是它敢用在生产检索里的前提。

nDCG@10(归一化折损累计增益)是衡量检索排序质量的标准指标:它给排名靠前的结果更高权重,并把位置靠前的相关文档排在靠后时给予折扣,最终归一化到 0 到 1 之间。recall@10 则只看前 10 个结果里是否包含相关文档,不关心顺序。两者结合能较全面地刻画检索效果——nDCG 反映排序好坏,recall 反映是否漏检。文中 repack 保留原版 97.9% 的 nDCG@10,说明排序质量损失很小,但共享 top 10 只有 85.5%,说明部分具体文档的排名发生了变化,对以排序为最终目标的检索影响有限。

性能与成本实测

作者对每个变体跑了三次基准,每次用新 revision(即新实例),记录服务端自报的耗时。中位数结果如下:

变体查询延迟文档/秒每百万查询成本每百万文档成本
cpu-original251 ms0.98.93 美元39.05 美元
cpu-repack121 ms1.13.91 美元25.87 美元
l4-original56 ms33316.27 美元0.87 美元

同样是 1 vCPU,repack 的查询耗时只有 float32 的一半,文档吞吐是它的 1.29 倍,内存还减半。L4 的文档吞吐是 repack 的 297 倍。

成本曲线的交叉点在哪

按请求计费的 CPU 服务,空闲时成本为零。L4 按实例计费,无论是否繁忙,每一个实例在线的秒都在烧钱——每小时 1.0465 美元,约为 1 vCPU 实例的十倍。所以真正的比较落在两个数上:L4 快多少,以及它有多少时间在忙。

按百万文档成本随忙碌比例变化:

忙碌比例L4 每百万文档cpu-repack 每百万文档
100%0.87 美元25.87 美元
50%1.75 美元25.87 美元
10%8.73 美元25.87 美元
1%87.28 美元25.87 美元

盈亏平衡点在 3.4%:只要 L4 在线时间中有超过 3.4% 在干活,它每文档就比 repack 便宜。

换成 Jev 使用的每百万输入 token 单位(141 token 的文档):cpu-original 为 0.2770 美元,cpu-repack 为 0.1835 美元,L4 满载时仅 0.0062 美元,10% 忙碌时 0.0619 美元,而 Jev 列表价为 0.0420 美元。也就是说,L4 在线时间忙碌超过 14.7% 就能追平 Jev 的单价,满载时 Jev 的价格是它的 6.8 倍。CPU repack 则是 Jev 单价的 4.4 倍——但它在空闲时一分钱不花。

冷启动:CPU 的软肋

--min-instances 0 意味着空闲后第一个请求要现起实例。实测数据相当刺眼:L4 首次请求总耗时 17.3 秒(模型加载 4.8 秒);float32 CPU 服务 48 秒(模型加载 15.5 秒);repack 则要 58 到 84 秒。

模型加载只占其中一小部分,大头是实例调度、镜像拉取与容器启动。L4 镜像最大(压缩后 4092 MB),冷启动却最快,说明 CPU 服务慢在别处。无法接受这种延迟的场景,可以用 --min-instances 1 保温,代价是闲置实例照常计费。

该选哪一个

结论取决于工作负载的形状。

白天陆续到达的搜索查询,选 CPU repack。 它每查询成本最低(3.91 美元/百万),请求之间不花钱,不需要 GPU 配额,且保留了原版 97.9% 的检索分数。

批量索引语料库,选 L4。 它嵌入速度快 297 倍,忙碌时每百万文档仅 0.87 美元,只要在线时间超过 3.4% 在干活就比 repack 划算。用完让它缩到零即可。

float32 CPU 服务则是另外两个的对照基准,本身不是生产选择。

几个容易忽略的工程细节

bf16 向量必须用 float32 归一化。 Sentence Transformers 的 normalize_embeddings=True 会按模型自身精度归一化。L4 上模型跑 bf16,本该是单位长度的向量范数在 128 维时只有 0.9975、512 维时是 1.0015,拿点积当余弦用就会带上同样的误差。作者的解法是取原始向量,再在 float32 下重新归一化,改完后三个服务的范数都打印为 1.0000。

repack 的向量会随宿主机漂移。 它的 int8 核依赖宿主 CPU,同一个检查在不同 Cloud Run 主机上给出 0.986 到 0.998 的结果。对检索无碍,但值得知道。

L4 未做并发测试。 作者明确说明基准是单请求串行,一种文档长度(141 token),每变体三次运行,冷启动是单次测量。Jev 的对比只用其列表价,两者在同一批数据上的准确率对比留待后续。

这个项目的价值不在于给出一个绝对的「CPU 好还是 GPU 好」,而在于把成本模型的变量摊开:计费模式(按请求 vs 按实例)、忙碌比例、量化带来的精度折损、冷启动的隐藏代价。选型时把这些数代进去,答案会自己浮现。

bf16 与 float32 在向量归一化上的差异源于浮点精度的舍入累积。归一化要求把向量除以其 L2 范数,使长度为 1;但 bf16 只有 8 位尾数,比 float32 的 23 位少得多,累加平方和时误差更大,导致算出的范数偏离 1(文中 128 维时 0.9975、512 维时 1.0015)。若直接用 bf16 向量做点积当作余弦相似度,就会带上这个系统性偏差。解决办法是取出 bf16 原始向量后转成 float32 再归一化,让范数回到 1.0000,从而保证跨服务、跨精度的相似度可比。

#EmbeddingGemma 2#Cloud Run#文本嵌入模型部署#int8 量化#NVIDIA L4#向量检索#GPU 成本优化#Matryoshka 嵌入
分享:

相关推荐