
1. 先别急着打包搞清楚这事难在哪你在AWS Lambda里跑一次sentence-transformers这需求听起来简单做起来却让不少人半夜爬起来排查依赖。先把话说明白这里说的Lambda是AWS的无服务器函数计算服务和Java 8那种x - x1的 lambda 语法不是一回事。如果你是从java lambda表达式的热搜词点进来的现在退出还来得及如果你确实想给文本向量化服务做一个Serverless入口那这篇东西你大概率用得上。sentence-transformers是我在语义检索、文本聚类、RAG知识库召回这些场景里最常用的一个Python库一句SentenceTransformer(all-MiniLM-L6-v2)加载模型再一句model.encode(texts)就能拿到句子向量。问题是这个看着很轻量的调用背后挂着两个大块头一是模型权重文件本体小一点的模型也有几十MB大的几百MB二是它的运行依赖 torch 和 transformers这两样装完随便都是两三百MB往上。而Lambda对代码包的大小的定义很小气未压缩代码包加所有层解压后总量不能超过250MB。所以核心矛盾就一句话依赖太肥装不下。这不是一个无解的问题而是需要你重新设计打包方式。我见过太多人直接在本地pip install sentence-transformers然后整个人肉打包上传最后在控制台看到Unzipped size must be smaller than 262144000 bytes提示时才开始研究什么叫Lambda层和容器镜像。下面我把这个项目的完整思路、三种能落地的姿势以及踩坑记录梳理出来你可以当成一份操作手册来用。1.1 sentence-transformers 到底在函数里做了什么在动手之前先了解sentence-transformers一次encode调用经历了什么这决定了我们对内存和时延的预期。整个流程大致是把模型权重从磁盘加载到内存构建tokenizer把文本切成token序列然后送入transformer编码器得到每个token的隐层向量再做一次池化常用的是MEAN pooling或CLS向量最后是一个可选的全连接层映射和归一化。这个流程意味着两件事。第一模型加载是一次比较重的IO和内存操作即使 all-MiniLM 这个级别的模型冷启动时从磁盘读权重到内存也要一两秒换成400MB的模型会更久。第二推理阶段CPU计算为主Lambda上不会有GPU给你用所以torch的CUDA部分纯粹是体积负担。明白了这两点后面选择方案时就清楚该往哪个方向使劲了。1.2 Lambda 的硬约束得一字一句看清楚Lambda对部署的限制从来都不是只有包大小。完整的清单是这样的函数代码加所有Layer未解压总量250MB直接控制台上传的zip还必须压缩到50MB以内如果换成容器镜像上限放宽到10GB临时磁盘/tmp有10GB在较大内存配置下但函数结束后数据不保留超时设置最长15分钟内存128MB到10GB可调。这些约束单独看都不算苛刻合在一起就限定了 sentence-transformers 的落地方式。比如你为了凑够250MB把模型和依赖塞进层里就得放弃torch的完整安装如果改用镜像10GB的空间很宽裕但镜像构建和推送流程又多了几步。另外还要关注/tmp的冷启动行为——如果你图省事把模型放到函数代码里运行时再解压第一个请求会非常痛苦而且/tmp一重建就得重新下载。顺带说一个常被忽略的点Lambda实例是可复用的。同一个实例跑完一次请求后会被冻结下一次请求过来时如果实例还在全局变量和/tmp都还在。这个特性既是福音也是坑福音是我们可以把模型加载放到全局冷启动只付一次坑是如果你的代码里不小心把大对象挂在全局变量上又没释放内存会被慢慢吃满。后面章节我会专门讲如何利用实例复用来做模型预热。2. 动手前的关键准备版本与依赖裁剪路线定了再动手不然来回返工特别消耗耐心。我的建议是先把环境在本地搭一遍确认推理逻辑完全正确再考虑怎么把代码打包进Lambda。2.1 版本选型我实测下来这一套最稳Python版本选3.11理由很朴素Lambda的Python运行时和官方容器镜像目前都支持3.113.12虽然也可以用但sentence-transformers某些旧版本依赖的pkg_resources在3.12的干净环境里会缺犯不上在这个环节给自己埋雷。torch版本不要装最新版追新也不要装带CUDA的默认包。用CPU版并且锁定版本号。我在容器镜像方案里用的是torch2.0.1通过PyTorch官方的CPU index安装。为什么要锁版本因为torch的包结构变化很大新版本可能引入你不需要的编译产物体积膨胀不说还可能跟Lambda的glibc版本不兼容。版本锁定以后镜像构建可以复现出了问题也容易排查。pip install torch2.0.1 --index-url https://download.pytorch.org/whl/cpu --no-cache-dirsentence-transformers版本2.2.x就够用了不需要上最新的2.3或3.x除非你需要特别新的模型特性。装的时候加上--no-cache-dir减少镜像构建体积。2.2 依赖瘦身能砍的都砍掉如果是走容器镜像路线你其实不用太抠体积10GB的上限很宽裕把torch CPU版、sentence-transformers、transformers正常装上就行。真正需要动手裁剪的是Layer路线这条路要求你理解torch安装目录里哪些文件有价值。一个基础认知默认从PyPI装的torch会带上CUDA运行库哪怕你的机型没有GPU这些库也会被装进环境里白白吃掉好几百MB。解决办法就是永远走CPU index安装这样得到的torch体积会小很多而且Lambda也没有GPU可用CPU版本来就是唯一正确选择。其次是sentence-transformers的隐式依赖。它为了支持模型评估会依赖 scikit-learn、scipy 这些重型库。如果你只是做encode向量化不需要评估模块理论上可以手动跳过但实际操作中sentence-transformers在 import 阶段就会去 import sklearn直接砍掉会报错。Layer路线的常规做法是安装全部依赖后实测如果超了250MB就换ONNX方案把torch彻底摘掉这个我在3.3节具体说。2.3 模型选择先掂量一下你的模型有多重模型体重直接影响方案选择。我整理一份常见 sentence-transformers 模型的体积清单按从轻到重排列模型名称权重大小embedding维度适合场景all-MiniLM-L6-v2约90MB384英文短文本速度最快通用相似度paraphrase-multilingual-MiniLM-L12-v2约120MB384多语言句子相似度bge-small-zh-v1.5约100MB512中文语义向量bge-base-zh-v1.5约400MB768中文效果更好但对Lambda不友好all-mpnet-base-v2约420MB768英文质量高不适合Serverless结论很直白如果你面向中文场景bge-small-zh-v1.5 和 all-MiniLM-L6-v2 是Lambda的首选bge-base 那类400MB的模型除非用EFS或容器镜像否则别硬塞。注意这里说的体积是PyTorch格式权重如果转成ONNX还能再小一些。2.4 本地先跑通再谈部署在本地建一个干净的虚拟环境装好上述依赖先验证模型能正常加载、编码结果符合预期。我习惯把模型目录和推理脚本分离模型预先下载好放在固定目录推理代码从环境变量里读模型路径。因为不管是EFS还是容器镜像模型最后都会被放到一个固定目录比如/opt/model或/mnt/model提前在本地模拟这个布局可以避免部署后才发现路径写错的尴尬。本地验证的一个小技巧把TORCH_HOME和HUGGINGFACE_HUB_CACHE指到指定目录提前把模型下载到本地这样后续打镜像或者传EFS时直接把整个模型目录拷走就行不需要在构建阶段联网下载也方便离线环境的复制操作。3. 三种能落地的部署姿势从推荐到备选现在进入正题。我按推荐程度排序把三种方式都讲透你可以按自己的运维习惯和模型大小选。3.1 姿势一容器镜像部署最省心这是我最推荐的方式尤其适合第一次做这个项目的朋友。核心思路是把依赖、模型和代码全部打进一个Docker镜像然后推送到ECRLambda以容器镜像的方式运行。这样Lambda的250MB限制对你来说基本不存在10GB空间装一个模型和一个推理代码绰绰有余。先看Dockerfile这是整个方案的核心FROM public.ecr.aws/lambda/python:3.11 RUN pip install torch2.0.1 --index-url https://download.pytorch.org/whl/cpu --no-cache-dir \ pip install sentence-transformers2.2.2 --no-cache-dir # 把本地预先下好的模型拷进镜像 COPY model /opt/model COPY app.py ./ CMD [app.lambda_handler]有几个细节说明一下。基础镜像public.ecr.aws/lambda/python:3.11是AWS官方维护的Lambda Python镜像里面预置了Lambda运行时接口CMD指定了入口函数。COPY model /opt/model把模型放进镜像注意/opt目录在Lambda里是可读的正好存放这类静态资源。推理代码app.py放在镜像根目录CMD写法是模块名.函数名。然后是推理代码这里有一个必须养成的习惯模型加载放全局利用Lambda实例复用。我第一次做的时候在图省事在lambda_handler里加载模型每次请求都重新读一遍权重慢就算了内存还反复暴涨。正确做法是这样import json import os from sentence_transformers import SentenceTransformer _model None def get_model(): global _model if _model is None: model_dir os.environ.get(MODEL_DIR, /opt/model) _model SentenceTransformer(model_dir) return _model def lambda_handler(event, context): try: body json.loads(event.get(body, {})) input_texts body.get(texts) if input_texts is None or not isinstance(input_texts, list) or len(input_texts) 0: raise ValueError(texts 必须是非空数组) model get_model() # normalize_embeddingsTrue 保证向量做了L2归一化 vectors model.encode(input_texts, normalize_embeddingsTrue) return { statusCode: 200, headers: {Content-Type: application/json}, body: json.dumps({ count: len(vectors), dim: vectors.shape[1], embeddings: vectors.tolist() }) } except Exception as e: return { statusCode: 500, headers: {Content-Type: application/json}, body: json.dumps({error: str(e)}) }这里有两个我特别想强调的点。第一normalize_embeddingsTrue不是装饰性的做了L2归一化之后向量内积就等于余弦相似度后续下游任务用点积还是余弦计算结果一致避免埋坑。第二encode入参建议传数组model.encode(单条文本)虽然有兼容处理但返回的维度行为有时候会让你意外统一传数组更稳。镜像构建和部署命令基本固定这几步# 本地构建 docker build -t sentence-lambda . # 创建ECR仓库 aws ecr create-repository --repository-name sentence-lambda # 登录并推送 aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin account-id.dkr.ecr.ap-southeast-1.amazonaws.com docker tag sentence-lambda:latest account-id.dkr.ecr.ap-southeast-1.amazonaws.com/sentence-lambda:latest docker push account-id.dkr.ecr.ap-southeast-1.amazonaws.com/sentence-lambda:latest # 创建函数 aws lambda create-function \ --function-name sentence-embed \ --package-type Image \ --code ImageUriaccount-id.dkr.ecr.ap-southeast-1.amazonaws.com/sentence-lambda:latest \ --role arn:aws:iam::account-id:role/lambda-basic-role \ --memory-size 3008 \ --timeout 300内存我先给了3008MB具体多少够用后面会有实测数据。超时给300秒是因为冷启动时模型加载有可能慢第一次请求超过默认3秒超时的情况我遇到太多次了。3.2 姿势二EFS挂载模型适合大模型和频繁更新如果你的模型超过400MB或者模型更新频率比较高不希望每次更新都重新构建镜像和推送ECR那EFS方案值得看。思路就是把模型文件放在EFS文件系统里Lambda通过挂载点直接读取函数代码本身只保留推理逻辑和依赖模型完全从EFS加载。先说模型上EFS的操作步骤在Lambda所在的VPC里创建一个EFS文件系统记录它的文件系统ID。创建一个访问点Access Point指定路径/model为挂载路径同时设置POSIX用户ID和组ID避免运行时的权限问题。找一个与EFS同VPC的EC2实例安装amazon-efs-utils把EFS挂载到本地把模型目录拷进去。给Lambda函数配置VPC访问同时添加EFS挂载配置挂载路径设为/mnt/model。这里有个特别容易翻车的细节只要Lambda函数配置了VPC它默认就走不到公网了。如果代码里还依赖从模型仓库下载模型或者偷偷访问外网都会静默失败或超时。所以我的建议是模型提前通过EC2放进EFS函数运行时只需要读/mnt/model不需要任何外网访问。如果业务上确实需要外网就得给VPC配NAT网关那是另一个成本故事能避则避。推理代码全局初始化部分差不多只是模型目录改成从环境变量读取model_dir os.environ.get(MODEL_DIR, /mnt/model)EFS方案的另一个特点是模型加载到内存的过程没法省冷启动时从EFS读取权重到内存速度取决于网络和EFS性能模式实测 all-MiniLM 这个级别的模型加载时间比容器镜像方案多个一两秒400MB的模型差别会更明显。好在后面请求都走实例复用影响被摊薄了。3.3 姿势三Layer极限方案适合把模型塞进250MB当你想完全绕开容器镜像和EFS只想用最简单的zip式Lambda那就得走Layer路线并且大概率要把torch换掉改用ONNX Runtime。思路是把 sentence-transformers 的模型转成ONNX格式推理时不再用torch加载而是用onnxruntime执行。onnxruntime的CPU包只有几十MB加上 tokenizers 和 numpy再加上模型文件整体能压进250MB解压限制内。模型转换用transformers自带的工具一行搞定python -m transformers.onnx \ --model all-MiniLM-L6-v2 \ --feature feature-extraction \ model-onnx/需要注意新版transformers把命令行export能力逐步移到了optimum生态如果你用的版本里transformers.onnx模块不存在就装optimum[exporters]导出逻辑是一样的照着模型卡的输入输出名调整就行。转换完成后会生成model.onnx和 tokenizer 相关文件。然后写一个轻量推理封装替代 sentence-transformers核心是完成池化这一步。因为 sentence-transformers 的模型在导出时往往不包含池化层你需要在代码里手动做MEAN poolingimport numpy as np import onnxruntime as ort from transformers import AutoTokenizer class TinyEmbedder: def __init__(self, model_dir): self.tokenizer AutoTokenizer.from_pretrained(model_dir) self.session ort.InferenceSession( f{model_dir}/model.onnx, providers[CPUExecutionProvider] ) def encode(self, texts): inputs self.tokenizer( texts, paddingTrue, truncationTrue, max_length128, return_tensorsnp ) feeds {input_ids: inputs[input_ids], attention_mask: inputs[attention_mask]} # bge等部分模型需要token_type_ids如果模型没这个输入就不用 if token_type_ids in inputs: feeds[token_type_ids] inputs[token_type_ids] last_hidden_state self.session.run(None, feeds)[0] # 手工MEAN pooling用attention_mask加权求平均 mask inputs[attention_mask] mask np.expand_dims(mask, axis-1).astype(np.float32) sum_embeddings np.sum(last_hidden_state * mask, axis1) sum_mask np.clip(np.sum(mask, axis1), 1e-9, None) embedding sum_embeddings / sum_mask # L2归一化保证与sentence-transformers输出一致 norm np.linalg.norm(embedding, axis1, keepdimsTrue) return embedding / norm话要说在前面ONNX方案看起来很香但有两个代价。一是不同模型导出后的输出张量和输入命名不一样上面的代码是针对 all-MiniLM-L6-v2 写的换模型就要对着model.onnx的输入输出名调整调试有一定成本。二是你失去了一部分 sentence-transformers 的高级API比如CLS池化、多设备切换等得自己动手实现。如果需求就是简单的文本向量化这个取舍很值。Layer的打包结构是固定的依赖放在python/目录压缩后作为一层模型文件放在opt/model/目录压缩后作为另一层或者按路径要求放进opt/。上传时Lambda会按顺序叠加所有层和函数代码最终解压体积总和不超过250MB。# 依赖层 mkdir -p layer_deps/python pip install onnxruntime tokenizers transformers --target layer_deps/python zip -r deps.zip layer_deps/ # 模型层 mkdir -p layer_model/opt/model cp -r model-onnx/* layer_model/opt/model/ zip -r model.zip layer_model/这里有一个非常容易被忽视的权限细节Layer解压后的目录属主不是运行lambda的用户如果模型文件权限不对推理时会报权限错误。所以我建议在层里把所有文件权限统一设置成755和644避免这种莫名其妙的问题。3.4 三种方案横向对比选型看这两点就够了我把三种方式放一起对比你对着自己的场景选维度容器镜像EFS挂载Layer ONNX依赖安装自由度很高和本地一致高依赖可放层或镜像受限依赖需人工裁剪模型大小上限10GB级几乎不限EFS容量决定基本不限250MB内含依赖冷启动加载模型较快稍慢受EFS性能影响较快模型文件小模型更新流程重建镜像、重新推送直接替换EFS文件重新打包Layer运维复杂度中等需维护镜像中等偏上涉及VPC管理较低纯函数式管理适合场景大多数情况首选大模型、频繁换模型追求纯Serverless、模型很小我的选型建议就一句话没有特殊理由第一优先容器镜像。EFS适合模型超过300MB且更新频繁的场景。LayerONNX适合你无论如何都想用纯函数、且能接受手工池化的场景。4. 内存、超时与冷启动实测数据说话部署只是一个开始让函数真正稳定跑起来还得调好三个参数内存、超时和并发。这一节我把我实测的数据和调整逻辑一并写出来。4.1 内存到底给多少我给个实测参考Lambda的内存是计费单位给多了心疼给少了可能会被杀进程。我的做法是渐进式测先用1024MB跑一次观察CloudWatch里的内存使用曲线再往上加到不出现内存溢出的配置再留一点余量。句子向量化是典型的内存敏感操作模型权重要常驻内存推理时还要额外空间存中间张量。实测 all-MiniLM-L6-v21024MB内存下并发个数少、单次encode文本量不超过几十条时勉强能用但稍微来一批长文本就可能内存告急。我最终稳定在2048MB单次encode一批128条短文本时耗时大约几十毫秒到两三百毫秒内存峰值大约在1.2GB左右。3008MB当然更稳但如果你追求成本2048MB是性价比比较好的档位。容器镜像方案里创建函数时指定--memory-size 2048EFS和Layer方案在控制台或CLI的基本设置里改也是一样的。Lambda内存一般按128MB的倍数来调整比如2048MB、3008MB这样好对齐配额逻辑。4.2 冷启动到底慢在哪拆开看冷启动时间是每个Serverless AI服务绕不开的坎。Lambda的启动顺序大致是新实例创建运行时环境、加载代码包或拉取镜像、执行初始化代码、进入handler。对 sentence-transformers 来说最重的是加载模型这一步。我把一次典型的冷启动时间拆解给你看阶段耗时实测约容器启动和运行时初始化1~3秒import torch / transformers1~2秒加载模型权重到内存2~5秒首次encode前的tokenizer初始化几毫秒到几百毫秒冷启动总时长约5~10秒这个数据在第一次请求时体现得非常明显如果你的API Gateway默认超时是3秒第一次调用十有八九直接超时。解决思路有三层第一把Lambda超时调大给足模型加载时间第二利用全局变量缓存模型实例复用的请求直接跳过加载阶段第三配置预留并发Provisioned Concurrency让Lambda提前预热好N个实例把冷启动从用户路径里抠掉。预留并发是按实例数和时长计费的属于花钱买体验的选项。我的建议是对外提供API服务的场景开少量预留比如2~3个并发实例内部批处理任务完全不用开批量场景本来就不在乎多等几秒。4.3 让模型加载只发生一次全局缓存和预热技巧全局变量缓存是这里面的核心技巧。上面代码里我用的_model就是干这个的。Lambda的执行环境复用机制决定了这招有效但要注意一个边界并发多的时候AWS会创建多个实例每个实例各自加载一份模型内存里就会有多份模型副本。所以并发和内存其实是联动关系开高并发前先算算内存预算。预热还有一种常见做法在部署后发一个测试请求强制实例先加载模型让真正的用户请求避开冷启动。我的习惯是在CI/CD的部署流程最后加一个健康检查调用传入一条固定的测试文本验证函数可用的同时也完成了预热。这个方法不花钱效果却不差。5. 常见问题与排查手册照着翻这个项目我前前后后踩了不少坑把最典型的几个整理成速查表再挑三个展开说说免得你走重复路。5.1 报错速查表报错信息根本原因解决办法Unzipped size must be smaller than ...代码包或Layer解压超250MB换容器镜像或转ONNX缩减体积Runtime.ImportModuleError依赖没装全或Python版本不对在干净环境重新安装确认Python版本一致libgomp.so.1: cannot open shared object file缺少OpenMP运行库手动安装libgomp1到依赖层ModuleNotFoundError: pkg_resourcesPython 3.12环境缺setuptools换3.11或显式安装setuptoolsUnable to connect to endpointLambda在VPC内没有外网模型提前放EFS或镜像避免运行时下载Read-only file system你试图写/opt目录模型放/opt只读临时文件写/tmpExecution role does not have permissions to call ecr:GetDownloadUrlForLayer执行角色缺ECR读取权限给角色附加容器镜像仓库只读权限Lambda函数创建后持续报镜像相关错误ECR镜像与函数不在同一区域检查镜像仓库和函数是否同区域5.2 展开聊三个实打实的坑第一个坑是libgomp缺失。这个词很少出现在 sentence-transformers 的安装文档里但它是torch CPU版在Lambda容器环境里非常容易碰到的问题。报错长这样libgomp.so.1: cannot open shared object file。原因在于Lambda基础镜像没有预装OpenMP库而torch编译依赖它。容器镜像方案直接改Dockerfile加一层安装libgomp的命令就行Layer方案则要手动把共享库文件打进层里。第二个坑是Python 3.12环境下的pkg_resources坑。sentence-transformers和transformers的某些版本会 importpkg_resources而这个模块在新版Python里默认不再随标准库安装。我身边不止一个朋友在3.12环境里折腾失败。解决方式简单粗暴用3.11而不是3.12或者预先安装setuptools。第三个坑是EFS方案的VPC网络黑洞。Lambda一旦加入VPC默认就丧失了公网访问能力如果你代码里还有任何运行时下载模型的逻辑就会卡在连接超时。而且这个超时往往还不是立刻报错而是等到HTTP请求最长等待时间后才失败排查起来特别迷惑。我的处理原则是EFS方案里运行时的一切外部下载全部禁用模型、tokenizer文件全部提前放在EFS如果确实需要外网再考虑给VPC加NAT网关但那是额外开支能免则免。5.3 成本账怎么算心里有个数最后说说成本因为很多人在Lambda上跑模型的第一反应是贵不贵。Lambda的费用主要花在三个地方请求次数、GB-秒的计算时间、预留并发的保底费用。我用2048MB内存、128条文本一批、平均耗时150毫秒来算一万次请求大约消耗的GB-秒是2GB乘以0.15秒再乘以10000约3000 GB-秒。按当前大部分区域的单价这笔费用不到一美元对于很多小项目来说完全不是负担。真正贵的往往是预留并发。假设你开了3个预留实例每个按3008MB规格一天就是3个实例乘以24小时乘以约3GB的保底时间开销这笔钱是无论有没有流量都要付的。所以我的建议是小流量阶段不开预留并发靠超时设置和全局缓存扛过冷启动等到线上延迟指标确实难看再逐步加预留实例。省钱和体验的平衡点要在实际压测里找而不是拍脑袋。最后分享一点我自己的体会。这个项目做了几轮之后我最大的感触是别把 sentence-transformers 当成一个装完就能用的库在Serverless环境里它要适配的不只是模型推理还有部署包体积、实例复用、冷启动延迟这些平时根本不会去想的约束。容器镜像方案虽然多学了一点镜像构建知识但换来的是几乎不需要操心依赖兼容性的安心这笔账怎么算都划算。如果你也在这个方向折腾建议先从 all-MiniLM 级别的模型加容器镜像起步跑通一条完整的流程之后再根据业务需要去碰EFS和ONNX。模型小一点、流程简单一点初期踩坑的密度会低很多后期扩展时也有清晰的路径可循。