端侧小模型上线三周就回退:五个维度的算账与判据
一个 1.8B 的量化模型塞进手机,离线可用、单次边际成本几乎为零,评审会上一路绿灯。上线三周后我们把这条链路砍回云端——不是因为它跑不起来,而是跑起来之后,每一项隐性成本都在超支。「能不能跑」是工程可行性,「该不该跑」是成本与体验的分配,用同一套指标衡量这两件事,就会系统性误判。
先看账本:那次回退亏在什么地方
场景很典型:文档类 App 的划词改写 + 意图识别,输入通常是 30~300 token,输出 20~200 token。上线前的估算是「端侧零网络开销,延迟更低,还省云端推理费」,上线三周后的实测是:
| 维度 | 上线前估算 | 实测 | 偏差来源 |
|---|---|---|---|
| 延迟 | P95 < 1s | P95 6~18s | 只算了「不用等网络」,没算手机算力 |
| 内存 | 峰值 1.2GB | 峰值 1.9GB,低端机 OOM | KV cache 与运行时开销漏算 |
| 功耗 | 忽略 | 30s 生成升温 8~11℃,二次调用降频 | 热设计功耗与持续负载 |
| 效果 | 量化掉点 < 2% | 改写采纳率 92% → 78% | 只测 perplexity,没测业务指标 |
| 运维 | 一次适配 | 每版本强绑定 App 发版 | 设备矩阵与可观测性 |
五条里有四条不是「跑不动」,而是「跑了不划算」。下面逐条拆。
延迟:端侧赢的是确定性,不是速度
这是最容易被「零 RTT」这个直觉骗过去的维度。把延迟拆成两段算:
def budget(prompt_tokens, out_tokens, *, prefill_tps, decode_tps, fixed_ms):
ttft = fixed_ms / 1000 + prompt_tokens / prefill_tps
return round(ttft, 2), round(ttft + out_tokens / decode_tps, 2)
# 云端:RTT 80ms + 排队 200ms,prefill 2000 tok/s,decode 80 tok/s
print(budget(300, 200, prefill_tps=2000, decode_tps=80, fixed_ms=280)) # (0.43, 2.93)
# 中端机端侧 1.8B INT4:无网络,prefill 60 tok/s,decode 15 tok/s
print(budget(300, 200, prefill_tps=60, decode_tps=15, fixed_ms=50)) # (5.05, 18.38)
按同一套参数铺开看不同 token 规模:
| 输入/输出 token | 云端 TTFT | 云端总时长 | 端侧 TTFT | 端侧总时长 |
|---|---|---|---|---|
| 40 / 20 | 0.30s | 0.55s | 0.72s | 2.05s |
| 150 / 60 | 0.36s | 1.11s | 2.55s | 6.55s |
| 300 / 200 | 0.43s | 2.93s | 5.05s | 18.4s |
结论反直觉但很稳定:在正常网络下,端侧几乎在任何 token 规模上都不赢延迟,因为手机 NPU 与云端 GPU 的 prefill 吞吐差一到两个数量级,而 RTT 只有几十毫秒。端侧真正提供的是三件东西——延迟方差小(不受网络抖动、弱网、跨境路由影响)、离线可用、数据不出端。所以延迟维度的判据不该是「端侧更快吗」,而是:
- 用户的网络环境里,云端 P95 是否超过端侧 P95?只有弱网场景(地铁、电梯、跨境)成立;
- 你是否能接受首字 1s 以内但总时长 2s 以上?短输出可接受,长输出不可接受;
- 是否可流式?不可流式的任务(必须整段返回再校验)会把端侧的总时长劣势放大成卡顿感。
内存:算峰值,不要算权重
权重只是内存账的第一项。一次 2048 上下文的会话,实际占用是权重 + KV cache + 激活 + 运行时缓冲:
def kv_gb(layers, kv_heads, head_dim, ctx, bytes_per=2):
return 2 * layers * kv_heads * head_dim * ctx * bytes_per / 1e9
print(round(kv_gb(24, 8, 128, 2048), 3)) # 0.201 GB (fp16 KV)
print(round(1.8 * 4 / 8, 2), "+ 0.2 + 运行时") # 0.9 GB 权重
实测峰值 1.9GB,其中权重约占 1.1GB(embedding 与 lm_head 常常不量化),KV cache 0.2GB,剩下是算子临时缓冲、tokenizer、以及推理框架自身的常驻开销。判定门槛建议写死两条:
- 常驻内存 ≤ 目标机型内存分档最低档的 25%,峰值 ≤ 40%;
- 模型必须可被系统随时回收,且回收后能在 500ms 内冷启或优雅降级到云端。
第二条比第一条更容易出事。端侧模型常驻会被 LMK(低内存杀手)优先干掉,用户切回 App 时表现成「功能坏了」,而不是「慢了一点」。
功耗:发热是延迟的第二重税
功耗不在于耗掉多少电,而在于热积累导致的降频曲线。5W 负载持续 30 秒,耗电约 42mWh,对 17Wh 电池只有 0.25%,用户完全无感;但机身表面温度会爬 8℃ 以上,SoC 触发降频后第二次调用延迟直接翻倍,用户感知是「手机烫」+「越用越卡」。
def thermal_debt(watts, seconds, ratio_at_throttle=0.6):
steady = seconds * ratio_at_throttle
return round(watts * seconds / 3600 * 1000, 1), round(seconds - steady, 1)
print(thermal_debt(5, 30)) # (41.7 mWh, 12.0s 的等效性能损失)
可操作判据:单次连续推理 ≤ 10s,日内单用户累计端侧推理时长设上限,超限自动上云。任何需要「一次生成几百字」的任务都不该走端侧——它必然踩到热降频区。
效果损耗:换成分母可算的业务指标
量化掉点、上下文裁剪、蒸馏压缩会叠乘。用 perplexity 涨 3% 判断是自欺,因为端侧失败往往是「看起来还行但微妙地不对」,用户不会报错,只会不用。我们当时看的三个指标比离线评测有用得多:
- 采纳率:改写结果被直接接受的比例,92% → 78%;
- 回退率:用户撤销、重新生成、手改的比例;
- 可检测性:错误能否被 schema、语法或规则自动判出。
第三项是分水岭。结构化抽取类任务(JSON、字段归一、分类)有校验器兜底,错误能当场拦下来重试,端侧损耗是可承受的;生成类任务没有校验器,错误直接进用户视野,端侧损耗会全部转成体验损失。经验阈值:关键指标相对损失 ≤ 3%,且失败样本 90% 以上能被自动检出,达不到就不要下沉。
运维成本:被低估最严重的一项
云端模型灰度以小时计,端侧模型灰度以「App 发版周期」计。一次下沉,等于把自己绑进了移动端发布节奏,并且要维护 芯片 × OS 版本 × 内存档位 的适配矩阵——几百种组合,回归测试只能抽样,线上问题只能靠采样日志回溯,没有 trace,复现依赖用户机型。
粗略折算:
端侧总成本 = 多端适配人力 × 版本周期
+ 模型增量包的分发带宽
+ 回退率 × 每次回退的云端推理成本
+ 因发热/卡顿造成的卸载与留存损失
最后一项通常是最大项,也是上线前最不会进预算表的一项。判断标准:如果这个任务的调用频次不足够高、单次价值不足够低,端侧省下的推理费永远覆盖不了适配与流失成本。
判据清单:什么下沉,什么留云端
先过三条硬门槛,任一不满足直接放弃端侧:
- 峰值内存 ≤ 最低目标机型可用内存的 40%;
- 单次推理 ≤ 10s,且可流式或可异步;
- 关键业务指标相对损失 ≤ 3%,失败可自动检出并回退。
再打分(每项 0~3 分,加权后 ≥ 12 分才值得下沉):
| 因子 | 权重 | 3 分 | 1 分 | 0 分 |
|---|---|---|---|---|
| 调用频次 | ×2 | 人均日调用 > 20 | 2~20 | < 2 |
| 单次 token 预算 | ×2 | < 100 | 100~400 | > 400 |
| 隐私等级 | ×2 | 数据不可出端 | 敏感但可脱敏 | 公开数据 |
| 错误代价 | ×2 | 可校验可重试 | 用户可感知可修正 | 直接影响结论 |
| 网络环境 | ×1 | 大量弱网/离线 | 偶发弱网 | 稳定网络 |
| 迭代速度要求 | ×1 | 季度级 | 月度级 | 周级以内 |
对照下来边界相当干净:
- 该下沉:输入法联想与纠错、端侧意图路由、离线语音唤醒与命令词、敏感文本的本地脱敏与打码、相机/相册的本地打标与检索、表单字段归一化。共同特征是短、高频、可校验、隐私敏感。
- 必须留云端:长文档理解与摘要、多轮规划与工具调用、需要强推理的代码/数学、高质量生成与改写、需要周级迭代的策略模型、有审计留存要求的场景。共同特征是长上下文、不可校验、快速迭代。
介于两者之间的,用分层回退而不是二选一:
def route(task, device):
if task.tokens > 400 or task.needs_tools:
return "cloud"
if device.mem_class < 6 or not device.npu_ready:
return "cloud"
if task.privacy == "strict" or task.offline:
return "edge_with_cloud_check" # 端侧出结果,云端只做校验
return "edge" if task.verifiable else "cloud"
落地建议
- 先用 token 预算做粗筛。 输入 + 输出超过 400 token 的任务直接排除端侧候选,不必进入后续评估。这一条能砍掉大半错误方案。
- 把延迟指标从 P50 改成 P95 与方差。 端侧的价值是延迟确定性,不是平均更快。监控里同时上报云端 P95 与端侧 P95,只有弱网分桶里端侧占优才保留。
- 内存与热预算写进准入清单,作为版本门禁。 常驻 ≤ 25%、峰值 ≤ 40%、单次 ≤ 10s 三条不过就不许上车,别指望上线后调优。
- 端侧默认走「端侧出草稿 + 云端校验」的混合链路。 有校验器的任务才允许端侧直出,其余一律带云端复核通道,回退率超阈值自动切回云端并告警。
- 埋点三件事再谈收益。 设备档位、端侧耗时分布、回退率。这三个数据不齐,任何「端侧省了多少钱」的结论都是估算,不是账。
- 点赞
- 收藏
- 关注作者
评论(0)