模型部署:Java 服务需要关心的那部分
原来那套「用 TorchServe 或 Flask 把模型包成 HTTP 接口」的做法已经过时了——TorchServe 的仓库在 2025 年 8 月被归档,官方明确说明不再有更新、缺陷修复和安全补丁。现在的问题变成了:选托管 API、自建 vLLM 还是本地 Ollama,以及 Java 侧要为此做哪些准备。
对 Java 服务而言,模型部署本质上就是一个延迟高、成本按量计、偶尔不可用的下游依赖。选形态先看数据合规与用量:能用托管就用托管,必须自建就用 vLLM,本地开发用 Ollama,非 LLM 的传统模型(分类、排序、CV)仍然需要 Triton 这类推理框架。接入层的超时、限流、降级、成本归因都在服务端这一侧,不是算法团队替你做的事。
一、四种形态
| 形态 | 谁负责扩缩容与显存 | 适合 | 主要代价 |
|---|---|---|---|
| 托管 API(云厂商或模型厂商) | 服务方 | 绝大多数业务场景 | 按 token 计费;数据出网;速率限制 |
| 自建 vLLM | 自己 | 数据不能出网、用量大、需要特定模型 | GPU 成本与运维;容量规划 |
| 本地 Ollama | 自己(单机) | 开发、演示、离线评测 | 并发与吞吐有限 |
| Triton 等推理框架 | 自己 | 非 LLM 模型、多框架混合部署 | 配置复杂,运维成本最高 |
1.1 关于 TorchServe
PyTorch 官方仓库的说明是「Limited Maintenance——不再积极维护,没有计划中的更新、缺陷修复、新功能和安全补丁」,仓库已于 2025 年 8 月 7 日归档为只读。已有部署可以继续跑,但新项目不应该再选它;如果现有系统依赖它,要把迁移列进计划,至少要评估安全补丁缺失的风险。
1.2 为什么 vLLM 成了默认
它解决的是 LLM 推理特有的两个问题:KV 缓存的内存管理(PagedAttention)和连续批处理(不同长度的请求动态组批)。对使用方来说,最直接的好处是暴露 OpenAI 兼容的 /v1/chat/completions 接口——Java 侧的客户端代码不需要为自建和托管写两套。
# 典型启动方式(参数按显存与模型调整)
vllm serve <model> --max-model-len 8192 --gpu-memory-utilization 0.9自建之前要算清楚三件事:显存够不够(模型权重 + KV 缓存)、并发上限是多少(超过就排队)、谁值班(GPU 节点故障、驱动升级、模型更新)。自建也不一定更便宜:要算上 GPU 常驻成本、运维投入和闲置率,低用量时托管通常更划算。
1.3 交付给推理服务的是模型产物,不是训练检查点
自建部署要从训练侧拿到一个「模型」,这里有两种很不一样的东西:
- 模型产物:推理需要的权重,加上预处理参数、词表、类别映射与版本号,交给推理服务加载。
- 训练检查点:在此之上还有优化器状态、学习率调度器的位置、随机数状态、已训练的步数,用于中断后继续训练。
用一个带 dropout、AdamW 与 StepLR 的小网络在 PyTorch 2.14.1(CPU)上验证:训练到第 15 步保存,在一个随机数种子不同的新进程里恢复,再训练 25 步,与不间断训练 40 步比较。
推理产物(只有 model.state_dict)7 KiB;完整检查点 33 KiB(AdamW 为每个参数保存两份动量)
只恢复权重:续训第一步的学习率 0.01(中断时为 0.005),学习率在第 [25, 35] 步减半(不间断训练是 [20, 30]);续训 25 步后与不间断训练的参数 最大差 0.1470
权重 + 优化器:续训第一步的学习率 0.005(中断时为 0.005),学习率在第 [25, 35] 步减半(不间断训练是 [20, 30]);续训 25 步后与不间断训练的参数 最大差 0.0392
权重 + 优化器 + 调度器:……学习率在第 [20, 30] 步减半……;续训 25 步后与不间断训练的参数 最大差 0.0336
再加随机数状态:……;续训 25 步后与不间断训练的参数 逐位一致
检查点里有自定义类的对象:torch.load 默认参数(weights_only=True)拒绝加载:UnpicklingError只存权重时,续训相当于换了一套超参数重新开始:学习率回到初始值,衰减点整体后移。每多恢复一层状态,结果就离不间断训练更近一步,全部恢复后逐位一致。推理服务只需要模型产物,体积小得多;训练侧要续跑、要复现,则需要完整的检查点。两者应该分开保存、分开命名,不要用检查点直接上线,也不要以为有了上线的权重就能继续训练。
另外,PyTorch 从 2.6 起 torch.load 默认 weights_only=True,检查点里有任意 Python 对象时会拒绝加载。这是为了防止加载来路不明的文件时执行任意代码,所以不要为了省事改回 weights_only=False,而应该把元数据存成普通的字典、字符串和张量。这个实验没有覆盖 GPU 与多线程下的非确定性、DataLoader 采样器的位置和分布式训练的分片状态,它们都要另行恢复和验证。
二、Java 侧的接入要点
无论后端是哪种形态,这些问题都要在 Java 服务里解决。
2.1 超时与重试
模型调用的耗时分布与普通接口完全不同:p50 可能 1 秒,p99 可能 30 秒,长文本生成更久。
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3)) // 连接快速失败
.build();
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(60)) // 读超时要按生成长度设置
.POST(...)
.build();重试要非常克制:生成类请求不是幂等的,重试会产生两次计费和两份不同的结果。可以重试的只有明确的网络错误和 429(限流),并且要用指数退避。如果用的是厂商 SDK,先查清它默认重试几次:openai 的 Python SDK 默认会重试 2 次,应用层再包一层就变成了相乘,见SDK 自带的重试。
2.2 流式响应
用户体验上,流式(SSE)几乎是必需的——首字延迟远小于整段生成时间。Java 侧要注意:
- 响应是长连接,占用的是连接而不是线程(用异步或虚拟线程),见 虚拟线程迁移;
- 中途断开要能取消上游请求,否则会白白消耗 token;
- 网关、负载均衡器的缓冲和超时配置要允许长连接与分块传输。
把超时设得很长并不能换来更少的失败:长超时会占满连接与线程,要配合流式输出和中途取消。
2.3 并发与成本
- 给模型调用单独的线程池或信号量,不要和普通业务共用,避免慢调用拖垮整个服务。
- 设置输入与输出的长度上限,这是成本控制最有效的手段。
- 按用户或租户限流,防止单个调用方耗尽配额。
- 记录每次调用的 token 数,成本要能按业务维度归因。
2.4 降级
模型服务不可用或超时的时候,业务要能继续:返回兜底文案、跳过智能环节走原有流程、或者把请求放进队列稍后处理。不要让一个「智能摘要」功能挂掉整个页面。
2.5 评测与灰度
换模型、改提示词都相当于一次发布,需要:
- 固定的评测集:几十到几百条真实样例加期望结果,每次变更跑一遍;
- 灰度:先放一小部分流量,对比关键指标(准确率、人工修正率、成本);
- 可回滚:模型和提示词都应该是配置,不是硬编码。
Java 侧的工具调用与可控性设计,见 给 Java 服务接一个可控的 Agent。
配套实验
- codesphere-labs/ai/checkpoint-resume-state:Python 3.14.8、PyTorch 2.14.1 CPU:只恢复权重、加优化器、加调度器、加随机数状态时续训结果与不间断训练的差,
weights_only默认值(验证记录)
参考资料