摘要: 产品从 Web 端扩展到移动端之后,同一个模型、同一段输入,移动端推理结果和 Web 端对不上------评测通过率从九成出头掉到七成多。本文从格式转换、多端适配、加载优化、灰度发布到版本管理,分享一套 Web + 移动端统一部署流水线的搭建过程,以及我在每个环节踩过的坑。
文章目录
先说背景
产品上线之后,反馈来得比预期快------用户说"电脑上用得挺好,手机上有吗?"
当时我们只在 Web 端做了推理,用 Transformers.js 2.17.2 加载量化后的模型,跑在 WebGPU 上。用户量不大时问题不大,但移动端的需求一冒出来,事情就变了。
第一版部署完,我拿手机测了一下,同一个模型、同一段输入,移动端推理结果和 Web 端对不上。不是差一点点,是差得离谱------Web 端生成结果的评测通过率九成出头,移动端掉到了七成多。
排查了三天,发现表面上是一个问题,实际上整个部署链路里藏着好几个隐患。后来我搭了一套统一的部署流水线,把格式转换、多端适配、灰度发布、版本管理串在了一起。这篇文章就聊聊这条流水线是怎么搭的,以及每步踩了什么坑。
核心挑战:多端部署要解决三个问题
先说结论,再展开。端侧模型做多端部署,归根结底要解决三个问题:
| 挑战 | 影响 | 解决方案 |
|---|---|---|
| 模型格式怎么统一 | 一套模型维护多套格式,版本管理会乱 | 统一转成 ONNX,作为唯一事实来源 |
| 推理后端怎么兼容 | Web 端可跑 WebGPU,手机浏览器不一定支持,原生 App 更是不同引擎 | 分端适配,共享同一份 ONNX 模型文件 |
| 版本怎么对齐 | 模型更新了,Web 端和移动端不能同时更新,灰度怎么控制 | 远程配置中心统一控制灰度比例和版本号 |
下图是我最终搭出来的多端部署架构,箭头表示数据流方向:
#mermaid-svg-ExYC8BZJ3TanrzqR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ExYC8BZJ3TanrzqR .error-icon{fill:#552222;}#mermaid-svg-ExYC8BZJ3TanrzqR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ExYC8BZJ3TanrzqR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ExYC8BZJ3TanrzqR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ExYC8BZJ3TanrzqR .marker.cross{stroke:#333333;}#mermaid-svg-ExYC8BZJ3TanrzqR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ExYC8BZJ3TanrzqR p{margin:0;}#mermaid-svg-ExYC8BZJ3TanrzqR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster-label text{fill:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster-label span{color:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster-label span p{background-color:transparent;}#mermaid-svg-ExYC8BZJ3TanrzqR .label text,#mermaid-svg-ExYC8BZJ3TanrzqR span{fill:#333;color:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR .node rect,#mermaid-svg-ExYC8BZJ3TanrzqR .node circle,#mermaid-svg-ExYC8BZJ3TanrzqR .node ellipse,#mermaid-svg-ExYC8BZJ3TanrzqR .node polygon,#mermaid-svg-ExYC8BZJ3TanrzqR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ExYC8BZJ3TanrzqR .rough-node .label text,#mermaid-svg-ExYC8BZJ3TanrzqR .node .label text,#mermaid-svg-ExYC8BZJ3TanrzqR .image-shape .label,#mermaid-svg-ExYC8BZJ3TanrzqR .icon-shape .label{text-anchor:middle;}#mermaid-svg-ExYC8BZJ3TanrzqR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ExYC8BZJ3TanrzqR .rough-node .label,#mermaid-svg-ExYC8BZJ3TanrzqR .node .label,#mermaid-svg-ExYC8BZJ3TanrzqR .image-shape .label,#mermaid-svg-ExYC8BZJ3TanrzqR .icon-shape .label{text-align:center;}#mermaid-svg-ExYC8BZJ3TanrzqR .node.clickable{cursor:pointer;}#mermaid-svg-ExYC8BZJ3TanrzqR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ExYC8BZJ3TanrzqR .arrowheadPath{fill:#333333;}#mermaid-svg-ExYC8BZJ3TanrzqR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ExYC8BZJ3TanrzqR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ExYC8BZJ3TanrzqR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ExYC8BZJ3TanrzqR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ExYC8BZJ3TanrzqR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ExYC8BZJ3TanrzqR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster text{fill:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR .cluster span{color:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ExYC8BZJ3TanrzqR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ExYC8BZJ3TanrzqR rect.text{fill:none;stroke-width:0;}#mermaid-svg-ExYC8BZJ3TanrzqR .icon-shape,#mermaid-svg-ExYC8BZJ3TanrzqR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ExYC8BZJ3TanrzqR .icon-shape p,#mermaid-svg-ExYC8BZJ3TanrzqR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ExYC8BZJ3TanrzqR .icon-shape .label rect,#mermaid-svg-ExYC8BZJ3TanrzqR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ExYC8BZJ3TanrzqR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ExYC8BZJ3TanrzqR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ExYC8BZJ3TanrzqR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始模型
Optimum 转换
ONNX 模型库
onnxruntime-web (Web端)
onnxruntime-react-native (移动端)
远程配置中心
灰度比例 / 版本号
这套架构里,ONNX 模型库是唯一的事实来源,Web 端和移动端共享同一份模型文件,灰度比例和版本号由远程配置中心统一控制。
Step 1:模型格式转换
为什么要转
Transformers.js 可以直接从 HuggingFace 加载模型,在 Web 端开发体验确实好。但移动端不会认这个格式------不管是 ONNX Runtime Mobile 还是 TFLite,都需要 ONNX 格式的模型文件。
所以第一步就是把模型从原始格式转成 ONNX。我用的是 HuggingFace 官方的 Optimum 工具链,一条命令就能跑完(转换流程参考 Transformers.js 官方文档):
bash
python -m scripts.convert --quantize --model_id Qwen/Qwen2.5-0.5B-Instruct
这个命令来自 Transformers.js 官方仓库,它会自动下载模型、转成 ONNX、再做 INT8 量化。输出目录结构是这样的:
text
models/Qwen2.5-0.5B-Instruct/
├── config.json
├── tokenizer.json
├── tokenizer_config.json
└── onnx/
├── model.onnx
└── model_quantized.onnx
model.onnx 是 FP32 版本,model_quantized.onnx 是 INT8 量化版。移动端一般用量化版,体积小很多,加载速度也快。
边界说明
不是所有模型都能用这条命令直接转。一些自定义算子或者特殊的 attention 实现(比如 Qwen2 使用的 GQA,Grouped Query Attention),在 ONNX 导出时如果 Optimum 的转换脚本没有自动识别 num_key_value_heads 配置,就需要手动指定,否则会报错。我一开始没注意这个细节,卡了半天。
另外,INT8 量化会有精度损失。我的经验是分类这类简单任务损失不大(1-2 个点),但生成式任务在长文本场景下差异会更明显。如果精度敏感,建议先跑一遍对齐测试。
Step 2:多端推理后端适配
选型策略
模型转好了,但 Web 端和移动端用的是不同的推理后端。我的做法是分端做适配,共享同一份 ONNX 模型文件:
Web 端 用 onnxruntime-web,推理后端按优先级降级:WebGPU → WebGL → CPU。逻辑很简单------先试最快的,不行就降级(后端的完整列表见 ONNX Runtime JavaScript API 文档)。
javascript
/**
* 创建 ONNX Runtime 推理会话(初版,存在资源泄漏问题)
*
* 作用:按优先级依次尝试 WebGPU → WebGL → wasm(CPU) 创建推理会话,
* 优先使用最快的后端,失败时自动降级到较慢的后端。
*
* 注意:这个版本有个隐患------每次 try/catch 之间没有释放失败的 session,
* 降级时可能因 GPU 资源未释放而报 context lost,详见下方修复版。
*/
async function createSession(modelPath) {
// 优先 WebGPU,不行就降级
let backend = 'webgpu';
try {
// 尝试用 WebGPU 创建会话;executionProviders 指定推理后端
return await ort.InferenceSession.create(modelPath, {
executionProviders: [backend]
});
} catch (e) {
// WebGPU 初始化失败(如浏览器不支持、驱动异常),降级到 WebGL
console.warn(`${backend} 初始化失败,降级到 webgl`);
backend = 'webgl';
}
try {
// 尝试用 WebGL 创建会话
return await ort.InferenceSession.create(modelPath, {
executionProviders: [backend]
});
} catch (e) {
// WebGL 也失败,最后降级到 wasm(CPU 的 WebAssembly 加速)
console.warn(`${backend} 初始化失败,降级到 wasm`);
return await ort.InferenceSession.create(modelPath, {
executionProviders: ['wasm']
});
}
}
这段代码看着简单,但少了一个关键细节------每次 try/catch 之间要清理上一次失败的 session 上下文,否则 WebGL 降级时可能因为 GPU 资源没释放而失败。我第一次写的时候没注意,WebGPU 降级到 WebGL 时一直报 context lost,排查了半天才发现是 session 没销毁。修复后的代码要显式释放失败的 session:
javascript
/**
* 创建 ONNX Runtime 推理会话(修复版,显式释放失败的 session)
*
* 改进点:用 for 循环统一遍历后端列表,避免重复代码;
* 每次 catch 中显式调用 session.release() 释放 GPU/内存资源,
* 防止降级时因资源未释放导致 context lost。
*
* 降级策略:按 ['webgpu', 'webgl', 'wasm'] 顺序尝试,
* 第一个创建成功的后端即为最终使用的推理后端。
*/
async function createSession(modelPath) {
let session = null;
// 按优先级从快到慢依次尝试各推理后端
for (const backend of ['webgpu', 'webgl', 'wasm']) {
try {
// 尝试用当前后端创建会话
session = await ort.InferenceSession.create(modelPath, {
executionProviders: [backend]
});
// 创建成功,直接返回,不再尝试更慢的后端
return session;
} catch (e) {
// 关键:释放失败的 session,避免 GPU 资源泄漏
// 如果上一次循环已创建了 session 但后续操作失败,必须手动 release
if (session) {
await session.release();
session = null;
}
console.warn(`${backend} 初始化失败,降级`);
}
}
// 所有后端都失败,抛出明确错误,便于上层捕获处理
throw new Error('所有推理后端均不可用');
}
```运行结果:
```text
// WebGPU 可用时,没有提示,正常创建 session
// WebGPU 不可用时,控制台依次输出:
[WARN] webgpu 初始化失败,降级到 webgl
// 如果 WebGL 也不可用:
[WARN] webgl 初始化失败,降级到 wasm
// 注意:如果没清理 session,WebGPU 降级时会报
[ERROR] context lost
小提示: onnxruntime-web 的 execution provider 名称是
wasm(对应 CPU 的 WebAssembly 加速)、webgl、webgpu。没有cpu这个写法,写['cpu']会直接初始化失败。
移动端 我用的是 onnxruntime-react-native,原生绑定,性能和 Web 端不在一个量级。同一个 INT8 量化模型,Web 端 WebGPU 推理不到一秒,移动端原生推理只要零点几秒。但代价是打包体积增大了,ONNX Runtime 的 native 库本身就有几十兆。
边界说明
这套降级策略在主流浏览器上表现稳定,但在一些特定场景下会出问题:
- Chrome 在部分 Android 机型上 WebGPU 后端有 bug------我遇到过 Chrome 133 在骁龙 8 Gen 2 上 WebGPU 初始化成功但推理结果全是 NaN 的情况。降级到 WebGL 才正常。
- iOS 的 WebGPU 支持来得晚------iOS 18 才全面支持 WebGPU,之前只能用 WebGL 或者 CPU。如果用户群体 iPhone 占比高,WebGPU 的覆盖率其实没想象中那么高。
- onnxruntime-react-native 的版本兼容性------这个库的版本和 React Native 版本有绑定关系,新版 ONNX Runtime(比如 1.24.x)通常要求较新的 RN 版本,升级 RN 时 ONNX Runtime 也要跟着升,否则编译报错。
Step 3:模型加载优化
问题在哪
模型转好了、后端选好了,但用户打开页面还是要等。一个 INT8 量化后的 5 亿参数模型约 500MB,走 CDN 加载也要好几秒,加上模型初始化时间,用户看到的第一屏就是白屏。
我的做法
分三步走:
第一,模型分片加载。 把一个大 ONNX 文件拆成多个分片,并行下载。onnxruntime-web 支持 External Data 格式,可以把权重数据拆到多个 .bin 文件里,模型结构文件单独加载。
javascript
import { pipeline, env } from '@xenova/transformers';
// 指定本地模型路径:模型文件放在 /models/ 目录下,走本地加载
env.localModelPath = '/models/';
// 禁止从远程下载模型:只从本地加载,避免运行时意外请求 HuggingFace
env.allowRemoteModels = false;
/**
* 创建文本生成 pipeline(模型分片加载 + 进度回调)
*
* 作用:加载 Qwen2.5-0.5B-Instruct 模型并创建 text-generation 管道。
* 模型权重被拆成多个分片并行下载,通过 progress_callback 实时上报进度。
*
* 注意:
* 1. progress_callback 是 Transformers.js 提供的接口,加载过程中会多次回调;
* 2. progress 对象包含 loaded(已加载字节数)和 total(总字节数)两个字段;
* 3. 计算百分比时注意 loaded/total 可能为小数,需用 Math.round 取整;
* 4. 分片加载能显著缩短首屏等待时间,但首次加载仍需下载完整模型。
*/
const pipe = await pipeline('text-generation', 'Qwen/Qwen2.5-0.5B-Instruct', {
// 注册加载进度回调:每次下载一个分片都会触发
progress_callback: (progress) => {
// progress 包含 loaded / total 字段
// 计算已加载百分比(0-100),并更新页面上的进度条
updateLoadingBar(Math.round(progress.loaded / progress.total * 100));
}
});
progress_callback 是 Transformers.js 提供的接口,加载进度可以实时展示给用户。我跑了大概几十次加载测试,平均加载时间从好几秒降到了一两秒,用户感知好很多。
运行结果示例:
text
// progress_callback 每次回调输出(total 约为 500MB,progress 是 0-100 的百分比):
{ loaded: 1048576, total: 524288000, progress: 0.2 }
{ loaded: 2097152, total: 524288000, progress: 0.4 }
// ... 中间省略几百次回调 ...
{ loaded: 524288000, total: 524288000, progress: 100 }
// 加载完成,模型初始化成功
第二,模型预缓存。 用户第一次加载完成后,把模型文件缓存到 IndexedDB 或者 Cache Storage。下次打开页面时先检查本地缓存,有就直接加载。首次加载好几秒,后续打开基本在一秒以内。
第三,模型预加载策略。 如果用户访问的是首页,可以在后台提前加载模型资源。等用户真正进入推理页面时,模型已经准备好了。这个做法的代价是首页加载流量会增加,但换来的是用户进入推理页面的"零等待"体验。
边界说明
预缓存方案在 Web 端好用,但移动端原生环境不一样------移动端没有 IndexedDB,用的是本地文件系统。模型文件存哪、怎么管理缓存过期,需要额外处理。我的做法是把模型文件集成到 App 的 assets 目录里,随版本发布,不走远程下载。缺点是模型更新需要发版,不能热更新。
Step 4:灰度发布策略
为什么需要灰度
模型不是写死的。今天跑 INT8 量化,明天可能跑 INT4,后天可能换更大的模型。如果直接全量推送给所有用户,出了问题连回滚的窗口都没有。
我的方案
用户分桶 + 按模型版本号分发。分桶逻辑按用户 ID 的 hash 值取模,决定用户落在哪个模型版本组里。
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 全量部署 | 所有用户用同一版本 | 简单 | 出了问题影响面大 |
| 灰度发布 | 按用户分桶逐步推送 | 风险可控 | 需要版本管理 |
| 蓝绿部署 | 两套环境同时在线,切流量 | 回滚快 | 资源消耗翻倍 |
我用的是灰度发布。第一阶段推一小部分用户,观察一天;没问题扩到两成;再观察一天;最后全量。每个阶段都有自动回滚条件------如果某模型版本导致错误率明显上升,自动切回上一版本。
第一次翻车
灰度上线的第一天,我设置了一小部分灰度比例,但第二天一看数据,绝大部分用户都在用新模型。排查发现,模型版本号缓存在 CDN 层,旧的缓存被新模型的请求覆盖了,导致后续用户请求都命中了新版本。
修复方案是加了一层版本号校验:前端从远程配置中心拉取当前用户的模型版本号,再和本地缓存的版本号对比,不一致就重新下载。同时把模型版本号和代码版本号绑定,代码回滚时模型也跟着回。
Step 5:热更新与版本管理
模型版本和代码版本对齐
模型更新最怕的是"模型换了,代码没换"。我遇到过这种情况:新模型输出的 logits 分布变了,但代码里 softmax 的温度参数还是旧值,推理结果直接错乱。
我的做法是维护一个版本兼容性矩阵:
| 模型版本 | 代码版本 v1.0 | 代码版本 v1.1 | 代码版本 v2.0 |
|---|---|---|---|
| v1.0 | ✅ 兼容 | ✅ 兼容 | ❌ 不兼容 |
| v1.1 | ✅ 兼容 | ✅ 兼容 | ❌ 不兼容 |
| v2.0 | ❌ 不兼容 | ❌ 不兼容 | ✅ 兼容 |
每次发布新模型时,必须指定最低兼容的代码版本。如果用户当前代码版本低于这个值,走 CDN 下载新模型之前先弹更新提示,让用户升级 App 或刷新页面。
热更新实现
热更新不做全量模型替换,而是用远程配置控制模型版本号。用户打开页面时,先请求一个轻量级的配置接口,返回当前用户应该使用的模型版本号,然后根据这个版本号去加载本地缓存或远程下载。
json
{
"model_version": "v2.0",
"min_code_version": "1.2.0",
"rollout_percentage": 20,
"fallback_version": "v1.1"
}
这个配置接口返回的内容包含灰度比例、模型版本、回滚版本。前端根据这些信息决定加载哪个模型。如果接口请求失败,默认使用上一个已知的稳定版本。
边界说明
这套热更新机制在 Web 端效果好,因为页面刷新就是一次"重新拉配置"的机会。但移动端原生 App 不一样------用户可能一个月不重启 App,配置一直走缓存,新模型发了一个月用户还没收到。我的做法是给移动端加一个定时刷新机制,每天或每次 App 从后台切到前台时重新拉配置。
整体效果
部署流水线跑通之后,多端部署从"手动操作"变成了"一次配置、自动分发"。同一套 ONNX 模型文件,Web 端和移动端共享,灰度比例和版本号由远程配置控制,回滚一条命令。
| 指标 | 部署前 | 部署后 |
|---|---|---|
| 模型部署耗时 | 半天(手动上传) | 十分钟(CI/CD) |
| 多端一致性 | 经常对不上 | 精度对齐测试通过 |
| 回滚速度 | 半小时 | 一分钟 |
| 灰度控制 | 没有 | 按比例 + 自动回滚 |
常见问题
Q:模型转换成 ONNX 后,推理速度比原始 PyTorch 慢?
A:ONNX Runtime 的推理速度通常比 PyTorch jit 快,但 Web 端受限于 WebAssembly 和 WebGPU 的线程模型,不一定比原生快。如果 Web 端慢,建议检查是否跑在 CPU 后端上。
Q:移动端用 ONNX Runtime 还是 TFLite?
A:如果你的模型有 TFLite 版本且量化工具链成熟,TFLite 在 Android 上性能更好。ONNX Runtime 的优势是跨平台统一------Web 和移动端共用 ONNX 格式,减少维护成本。
Q:灰度发布的比例怎么定?
A:我的经验是第一阶段别超过 5%,而且最好选内部用户或者测试账号。第二阶段 20%,观察 24 小时。如果模型涉及生成式任务,我会在灰度期间加人工抽检,因为自动监控指标不一定能捕捉到"回答质量下降"这类问题。
局限性
总结
这条部署流水线的核心,是把「格式转换 → 多端适配 → 加载优化 → 灰度发布 → 版本管理」串成一条可复用的链路:ONNX 模型库作为唯一事实来源,Web 端和移动端共享同一份模型文件,灰度比例和版本号由远程配置中心统一控制。每一步都有对应的坑------格式转换的算子兼容、后端降级的资源泄漏、CDN 缓存导致的灰度失控、模型与代码版本错位------但踩过之后,多端部署就从"手动操作"变成了"一次配置、自动分发"。
这套方案有几个前提:
- 用户量需要有一定规模------灰度发布和版本管理带来的复杂度,在日活几百的场景下收益不大,全量部署加手动回滚反而更省事。
- 模型体积不能太大------超过两个 G 的模型在 Web 端加载体验很差,即使分片加载也扛不住。大规模模型建议走服务端推理。
- 团队需有 DevOps 基础------CI/CD 流水线、远程配置中心、监控告警,这些基础设施缺了任何一个,部署流水线就跑不起来。
下一步我打算做的是模型蒸馏------把 5 亿参数模型蒸馏到 2 亿参数以下,这样移动端和 Web 端都能跑得更快,甚至不需要降级到 CPU。