端侧AI能省多少服务器钱:把账换成字节算

摘要

「端侧 AI 不花服务器钱」这句话我听过很多遍。我想给自己的小工具站加一个 AI 抠图,动手前先把这句话拆开算了一遍账:省掉的只有推理那台机器。其余开销没有消失而是换了形态:每个新访客的一次下载、用户屏幕前的等待、用户电脑里的内存,外加回退与兼容的成本。这篇全程只拿实测的字节数和耗时做算术。快速 AI 每个新访客首次要下载 50.18 MB,1000 个新访客就是 50.18 GB;其中 44.23 GB 走模型所在的第三方 CDN,5.95 GB 走网站自己。快速 AI 冷启动要等 14.6--16.0 s;专业 AI 要等 34.8 s 和 45.1 s。专业 AI 回退到 WASM 时渲染进程峰值到了 3.58 GB。服务器推理那一侧的账我没测,文中也不编数。

版本声明

数据是 2026-09-28 晚上跑的。机器只有一台:16 GB 统一内存的 Apple M4 Mac,系统 macOS 26.5.2。浏览器用的是 Chromium 149 开源构建(149.0.7827.55)而 WebGPU 走 Metal。跑模型的库是 1.27.0 版 ONNX Runtime Web。网络是我当时那条普通家用宽带。所有耗时只代表这台机器、这条网络和那一个晚上的状态。换 Windows、独显、手机、Safari 或 Firefox 之后数字都可能完全不同;那些环境我一个都没测。文中的 MB 与 GB 按十进制算:1 MB 是 10⁶ 字节而 1 GB 是 10⁹ 字节。

适用边界

这篇只回答一个问题:AI 推理从服务器挪进浏览器以后成本到底挪去了哪里。它不讲 WebGPU 和 WASM 两个后端怎么切换,也不讲模型怎么选型。服务端推理的账我没有测:提到服务器那一侧的地方我只讲账的结构,不给数字也不写任何云厂商的价格。样本是一张程序生成的 1600×1200 瓶子商品图。它不是真实照片也不是哪个客户的图。推理耗时和图片内容关系不大,因为输入会先缩放到模型的固定尺寸;字节数则与图片内容完全无关。

文章目录

一、端侧 AI 真的不用服务器吗

  • 1.1 「端侧 AI 省服务器钱」省的是哪一块
  • 1.2 我这次量了什么、没量什么

二、端侧 AI 模型多大:每个新访客下载多少字节

  • 2.1 快速 AI 第一次打开拉了哪几个文件
  • 2.2 代码一:1000 个新访客是多少 GB
  • 2.3 专业 AI 和骨灰级 AI 的账
  • 2.4 回访为什么是 0 字节:代码二看缓存落在哪

三、模型放 CDN 还是自托管:流量落到谁头上

  • 3.1 模型走第三方 CDN 而运行时走本站
  • 3.2 代码三:自托管忘开 gzip 要多传多少
  • 3.3 代码四:模型文件还能不能再压

四、端侧 AI 冷启动要等多久

  • 4.1 快速 AI 冷启动 14.6--16.0 s:慢的不是模型
  • 4.2 专业 AI 冷启动 34.8 s 和 45.1 s
  • 4.3 回访和同页再跑要多久

五、浏览器跑 AI 模型占多少内存

六、没有 GPU 的访客多付了什么

七、端侧 AI 到底省了什么:一张账单

八、我的小站打算怎么上

九、适用场景 / 不适用场景 / 风险提示

十、测量方法自身的坑

十一、还没解决的

参考资料

一、端侧 AI 真的不用服务器吗

1.1 「端侧 AI 省服务器钱」省的是哪一块

我靠外包和两个小工具站过日子。站上跑的是最便宜那一档云主机,带宽也按最低档买,这台机器连一张 GPU 都没有。家里另有一只橘猫叫「服务器」,它倒是从来不要带宽费。上个月有两个外包客户问我能不能顺手做个商品图去背景。我第一反应是跑不动:抠图模型放服务器上推理就得租带 GPU 的机器或者按次调别人的接口,两条路都是每来一张图多一笔钱。这笔钱我出得心疼。

后来我在好几篇文章里看到同一句话:「端侧 AI 把推理放进用户浏览器,服务器一分钱不花。」前半截我信:模型在用户电脑上跑时我那台主机确实不用管推理。后半截我不太信:做了十几年网站,我知道「不花钱」的东西通常只是把钱挪到了别处。挪到哪了得算。

这件事我换了个问法:我不问「省了多少钱」,因为服务器推理那一侧我没测过。我问的是推理挪走以后哪些东西没有消失而只是换了形态。能换成字节和秒的我都换成字节和秒:字节可以乘访客数而秒可以直接看。钱这一层留给读者按自己的单价去乘。

一开始我以为账很简单:模型文件多大,每个访客就多下载多少。算到一半发现漏了一个文件:它比模型小得多却是冷启动里最慢的一段,第四章细说。

1.2 我这次量了什么、没量什么

我自己手上没有现成的端侧抠图可以量。拿来当样本的我用的是图映 ImgIng(https://imging.cn/)的 AI 智能抠图,机器是 Apple M4 / 16 GB,内核是 Chromium 149 开源构建,模型是快速 AI(ISNet INT8)和专业 AI(BEN2 FP16),样本是上面那张合成的瓶子图。它的模型按需下载并在浏览器里推理,正好是我想做的形态。计时来自直接调用页面里的抠图函数 TYBG.segment,它和点「开始 AI 抠图」按钮调的是同一个函数,但我没有走点击界面。每种情况跑三次:第一次是全新的浏览器配置而什么都要下载;第二次是同一个页面再跑一次;第三次是同一配置下新开页面,这时缓存已经命中。

浏览器状态分两种:一种是开着硬件加速、WebGPU 走 Metal。另一种是没有可用 GPU 适配器的 Chromium(关掉浏览器的「使用图形加速」就是这种状态)。这时 navigator.gpu 还在但申请不到适配器,推理会回退到 WASM。全部六次调用里页面发出的非 GET 请求是 0 次,图片没有离开浏览器。这一点我单独数过,它是这篇的前提:确实没往服务器传图。这点我放心了。

没量的东西先列在这里:服务器推理的耗时、机器占用和费用我都没测。骨灰级 AI(BiRefNet 约 447 MB)我只拿到文件字节而没跑下载和推理。手机、Windows 和其他浏览器都没测。显存也没量到,第五章的内存数字是进程 RSS。

二、端侧 AI 模型多大:每个新访客下载多少字节

2.1 快速 AI 第一次打开拉了哪几个文件

先看第一次打开:快速 AI 用的模型是 ISNet INT8,文件 44 229 662 字节,界面上写的是「约 42 MB」。我拿 44 229 662 连除两次 1024 得到 42.18。快速档这个数是按 1024 进制标的。我这篇统一按十进制写成 44.23 MB。两种写法差了两 MB 多;第一次看我还以为下错了文件。

光有模型还跑不起来:浏览器里还得有一个推理运行时,这里是 ONNX Runtime Web。它的主体是一个解压后 24 254 953 字节的 WASM 文件,走 gzip 传输是 5 953 924 字节即 5.95 MB。我原先完全没把它算进去。第一次冷启动的网络记录里就这几样:20 KB 量级的运行时入口脚本、17 KB 量级的 .mjs 胶水脚本、44 MB 的模型和这个 5.95 MB 的 WASM。两个小脚本加起来不到 40 KB,下文的算术里我把它们略掉。

这张图要看什么。只看字节那一列:两个大文件是模型和运行时 WASM,它们占了首次下载的几乎全部。另外两个小脚本加起来不到 40 KB,算账时可以忽略。合计一栏是 50.3 MB,这是首访真正要付的量。表里四个文件三个来自图映自己的站点,只有模型走 ModelScope 的 CDN。

WebGPU 路径也要下这个 WASM;我原本以为走 GPU 就用不着它。网络记录里开着硬件加速的那次同样请求了这个文件。ORT 的 WebGPU 执行提供程序就在同一个 WASM 里。不管访客有没有 GPU 这 5.95 MB 都跑不掉。

2.2 代码一:1000 个新访客是多少 GB

字节有了剩下的就是乘法。我写了个很土的脚本:三档模型的文件字节和运行时的 gzip 字节写死在里面,再乘访客数。

python 复制代码
MODEL_BYTES = {
    "quick_isnet_int8": 44_229_662,
    "pro_ben2_fp16": 219_121_675,
    "ultimate_birefnet_fp16": 447_261_189,
}
ORT_WASM_GZIP = 5_953_924

def bill(tier, visitors):
    return MODEL_BYTES[tier] * visitors, ORT_WASM_GZIP * visitors

for tier in MODEL_BYTES:
    m, r = bill(tier, 1)
    print(f"{tier}: 每个新访客 {(m+r)/1e6:.2f} MB(模型 {m/1e6:.2f} + 运行时 {r/1e6:.2f})")
    for n in (1000, 10000):
        m, r = bill(tier, n)
        print(f"  {n:>6} 人  模型 {m/1e9:8.2f} GB  运行时 {r/1e9:6.2f} GB  合计 {(m+r)/1e9:8.2f} GB")
复制代码
quick_isnet_int8: 每个新访客 50.18 MB(模型 44.23 + 运行时 5.95)
    1000 人  模型    44.23 GB  运行时   5.95 GB  合计    50.18 GB
   10000 人  模型   442.30 GB  运行时  59.54 GB  合计   501.84 GB
pro_ben2_fp16: 每个新访客 225.08 MB(模型 219.12 + 运行时 5.95)
    1000 人  模型   219.12 GB  运行时   5.95 GB  合计   225.08 GB
   10000 人  模型  2191.22 GB  运行时  59.54 GB  合计  2250.76 GB
ultimate_birefnet_fp16: 每个新访客 453.22 MB(模型 447.26 + 运行时 5.95)
    1000 人  模型   447.26 GB  运行时   5.95 GB  合计   453.22 GB
   10000 人  模型  4472.61 GB  运行时  59.54 GB  合计  4532.15 GB

代码说明。字节全部来自实测:模型取文件字节,运行时取 gzip 后的传输字节。这里的「新访客」指第一次用这个功能、浏览器里没有缓存的人。脚本把模型和运行时分开算,因为这两份流量落在不同的地方,第三章要用。快速 AI 这一档 1000 个新访客是 50.18 GB,其中模型那 44.23 GB 占了八成八。我盯着这个数看了挺久:我那个站一个月的全部流量也就是它的零头。这账我原先没想过。

这个数还有一个容易漏的前提:它只算「用了 AI 抠图的新访客」。只打开页面不点抠图的人不会触发下载,因为模型是按需拉的。要是把模型放在页面加载时预取,这个数就得换成「所有新访客」。两种写法差多少取决于你站上多大比例的人真会点那个按钮。这个比例我没有数据也没法替你估。

2.3 专业 AI 和骨灰级 AI 的账

快速 AI 是最小的一档;专业 AI 用 BEN2 FP16,文件 219 121 675 字节。骨灰级 AI 用 BiRefNet HR-Matting FP16,文件 447 261 189 字节。同样按新访客乘:专业 AI 每人 225.08 MB 而 1000 人就是 225.08 GB;骨灰级每人 453.22 MB 而 1000 人就是 453.22 GB。骨灰级这一档我只有文件字节,下文的耗时都只到专业 AI 为止。

这张图要看什么。看三档模型旁边标的体积:从约 42 MB 到约 447 MB 差了十倍多。再看它的组织方式:档位由用户自己选而不选就不下。这对账很重要。用户默认停在快速档的话大部分人付的就是 50.18 MB。只有主动切到专业档的人才会多付那 170 多 MB。我拿文件字节对了一下这三个标注。快速档的约 42 MB 对得上 1024 进制;专业档的约 219 MB 和骨灰级的约 447 MB 却对得上十进制(219.12 和 447.26)。三个数用的不是同一种进制。差得不多,但自己算账时我不再直接抄界面上的数。

还有一个细节我没想到。图映在手机上只开放快速 AI,专业和骨灰级在手机端直接不支持。我在页面脚本里看到的判断是「移动端且不是快速档就不支持」。它为什么这么定我不知道;我猜和第五章的内存有关但这只是推测。

2.4 回访为什么是 0 字节:代码二看缓存落在哪

同一个人第二次来时网络记录里模型那一行没了,模型改从浏览器的 Cache Storage 里读。运行时文件带的是 Cache-Control: public, max-age=31536000, immutable,缓存命中后也不再请求。回访的首次下载是 0 字节。听着很美。

这 0 字节不是凭空来的:模型没有消失而是躺在了访客的硬盘上。为了知道到底占多少,我在自己本地的测试页上做了一遍同样的事:把 INT8 模型取下来塞进 Cache Storage,前后各读一次存储用量。

js 复制代码
(async () => {
  const before = (await navigator.storage.estimate()).usage;
  const box = await caches.open('my-model-cache');
  await box.put('isnet-int8', await fetch('models/isnet-general-int8.onnx'));
  const after = (await navigator.storage.estimate()).usage;
  const hit = await box.match('isnet-int8');
  console.log({
    before, after, grew: after - before,
    cached_body: hit ? (await hit.arrayBuffer()).byteLength : 0,
    persisted: await navigator.storage.persisted(),
    device_memory: navigator.deviceMemory,
  });
})();
复制代码
{
 "before": 0,
 "after": 44230153,
 "grew": 44230153,
 "cached_body": 44229662,
 "persisted": false,
 "device_memory": 16
}

代码说明 。这段在浏览器控制台里就能跑,模型路径换成你自己站上的文件即可。存进缓存以后这个源的用量多了 44 230 153 字节,比模型文件多出的 491 字节是缓存条目本身的开销。真正要看的是 persisted: false。它的意思是这块存储属于「尽力而为」:磁盘紧张时浏览器可以把它清掉。清掉以后这个访客下次来又是新访客,又要下 50.18 MB。浏览器在什么条件下会清我这轮没测。device_memory 顺手打出来是 16,和这台机器的内存一致,第八章我会用到它。

算到这里第一笔换形态的账就清楚了。推理机器省掉了;换来的是每个新访客一次 50 MB 起步的下载外加访客硬盘上一块随时可能被清的 44 MB 缓存。

三、模型放 CDN 还是自托管:流量落到谁头上

3.1 模型走第三方 CDN 而运行时走本站

50.18 MB 是访客下载的量;它从谁的服务器出去决定了这份流量记在谁的账上。我读了图映线上的抠图脚本。里面写了三个源。下载源的顺序是先 ModelScope 的 CDN、再 ModelScope 的备用域名、最后才是本站的 models/background-removal/ 目录。下完按字节数和 SHA-256 校验,不过就不用。

网络记录印证了这个顺序:模型请求先打到 modelscope.cn 再 302 跳到 cdn-lfs-cn-1.modelscope.cn,响应体 44 279 201 字节。运行时 WASM 走的是 imging.cn 本站,网络记录里是 5 955 745 字节。它比 curl 量到的 5 953 924 略多一点,两种记录的口径不完全一样。

拿 1000 个新访客那一行来分:44.23 GB 从第三方 CDN 出去而不在网站自己的账上;5.95 GB 从本站出去,这份是网站自己出的。模型那份到底谁付、付多少要看模型托管方的政策。这我不知道也不替任何一方表态。我只能说字节确实是从那边的域名出去的。

换成我自己的站就是两条路:第一条是模型也放自己站上,那 1000 个新访客的 50.18 GB 全从我的带宽出去。我那台最低配主机上一个 44 MB 的文件同时被几个人下,别的页面基本就打不开了。第二条是模型放到公开的模型托管平台而我只托管运行时,这样我自己出的是每人 5.95 MB。代价是多了一个依赖:对方的 CDN 慢了、改了规则或者挂了,我的功能就跟着坏。图映的做法是三个源轮着试,最后回到本站兜底。它为什么这么排我只能推测:大概是想让大头流量走外面,外面挂了还能用。

3.2 代码三:自托管忘开 gzip 要多传多少

运行时这 5.95 MB 是 gzip 以后的数。要是自托管时忘了开压缩它会是多少?我在本地起了一个不压缩的静态服务,再用浏览器的 Resource Timing 看实际走了多少线上字节。

js 复制代码
(async () => {
  await (await fetch('rt/ort-wasm-simd-threaded.asyncify.wasm')).arrayBuffer();
  await (await fetch('models/isnet-general-int8.onnx')).arrayBuffer();
  const heavy = performance.getEntriesByType('resource').filter(e => e.transferSize > 1e6);
  console.table(heavy.map(e => ({
    file: e.name.split('/').pop(),
    wire_mb: +(e.transferSize / 1e6).toFixed(2),
    body_mb: +(e.decodedBodySize / 1e6).toFixed(2),
  })));
})();
复制代码
┌─────────┬────────────────────────────────────────┬─────────┬─────────┐
│ (index) │ file                                   │ wire_mb │ body_mb │
├─────────┼────────────────────────────────────────┼─────────┼─────────┤
│ 0       │ 'ort-wasm-simd-threaded.asyncify.wasm' │ 24.26   │ 24.25   │
│ 1       │ 'isnet-general-int8.onnx'              │ 44.23   │ 44.23   │
└─────────┴────────────────────────────────────────┴─────────┴─────────┘

代码说明 。transferSize 是这次请求在网络上实际走的字节(含响应头),decodedBodySize 是解压后的内容大小。两列几乎一样,说明这个本地服务什么都没压,运行时原样传了 24.26 MB。图映站上同一个文件 gzip 后是 5.95 MB,差了四倍多。这是白扔的带宽。放回 1000 个新访客那一行,运行时这一项就从 5.95 GB 变成 24.25 GB 左右。这段代码在你自己站上跑一遍就知道压没压,比翻服务器配置快。跨域资源要对方带 Timing-Allow-Origin 头才读得到 transferSize,否则读出来是 0。

我拿 curl 对图映站上那个运行时文件又核了一遍。带 Accept-Encoding: gzip 下回来 5 953 924 字节,响应头有 Content-Encoding: gzip;不带就是 24 254 953 字节。它的站上开了压缩。Nginx 的 gzip_types 默认只管 text/html。WASM 要自己把 application/wasm 加进去才会压。我以前一直以为写了 gzip on 就全压了,这回才去翻文档确认。

3.3 代码四:模型文件还能不能再压

运行时压完只剩四分之一。模型能不能也压一压?我在本地对两个文件跑了一遍 gzip。

bash 复制代码
for f in isnet-general-int8.onnx ort-wasm-simd-threaded.asyncify.wasm; do
  printf "%s raw=%s gzip6=%s\n" "$f" "$(stat -f %z "$f")" "$(gzip -6 -c "$f" | wc -c | tr -d ' ')"
done
复制代码
isnet-general-int8.onnx raw=44229662 gzip6=30293896
ort-wasm-simd-threaded.asyncify.wasm raw=24254953 gzip6=5953961

代码说明 。stat -f %z 是 macOS 的写法,Linux 上换成 stat -c %s。运行时 gzip -6 以后是 5 953 961 字节,和线上传输的 5 953 924 几乎一样,线上用的应该就是差不多的压缩级别。模型压完是 30 293 896 字节,剩原来的 68.5%。INT8 的权重是一堆小整数,还能压掉三成。这我没想到。FP16 的模型我这轮没压,那个比例我不知道,68.5% 不能拿去套。

再对照网络记录:模型那条从 ModelScope CDN 回来的响应体是 44 279 201 字节,比文件本身还略大。按字节数推断这条链路上模型没有走压缩传输。这是我从字节数推的而没去看那个响应的 Content-Encoding 头。我要是自托管模型就预压一份 .gz 放着,每个新访客能少下约 14 MB。服务器实时压 44 MB 的文件会吃 CPU,预压更省。这一条是按经验判断,服务器那一侧的占用我没量。

四、端侧 AI 冷启动要等多久

4.1 快速 AI 冷启动 14.6--16.0 s:慢的不是模型

字节换形态以后第二笔账是时间。服务器推理时用户等的是上传、排队和推理。端侧推理时用户第一次等的是下载、建会话和推理,这些全发生在他自己的屏幕前。

快速 AI 的冷启动:开着硬件加速是 14 594 ms;没有可用 GPU 适配器的 Chromium 是 16 015 ms。两者都从调用抠图函数那一刻起算。

这张图要看什么。先看蓝色段:44 MB 的模型在 4.2 s 左右就下完了。再看橙色段:它从模型下完才开始并一直拖到 13 s 以后。这一段主要在等那个 5.95 MB 的运行时 WASM。最后看下面那条的红色小段。没有 GPU 适配器时先试 WebGPU 失败再切到 WASM,这段本身很短。绿色的推理段在 WASM 那条明显更长。图底部那行小字是缓存命中和同页再跑的耗时。

这张图是我这篇最意外的地方:44 279 201 字节的模型在 +4.2 s 下完。随后才开始请求运行时 WASM,到 +13.2 s 才下完。一个不到 6 MB 的文件比 44 MB 的模型慢了一倍多。我一开始不信。我用 curl 单独复测了那个运行时文件两次:9.19 s 和 10.78 s,折合 552--648 KB/s。同一时段 curl 下 ModelScope 上的 44 MB 模型只要 4.20 s(大约 10.5 MB/s)。

这只是那一晚那条家用宽带的速度。不同地区、不同运营商到这两个源的速度可能完全不一样。后来有一轮在关掉 GPU 的浏览器里重跑:运行时 WASM 到 +34.2 s 才下完,模型还是 +4.3 s。同一个文件前后差了三倍多。我不替任何一方判断这是哪里的问题。我只陈述看到的:这一晚冷启动的等待大头压在运行时文件上,而它和模型是一前一后串着下的。

对我自己的站这条的意思很直接:我原以为冷启动只要盯模型大小。实际上运行时文件放在哪台机器、那台机器出口多快、它能不能和模型并行下,这几件事比模型小几兆重要得多。我那台最低配主机的出口恐怕比这一晚测到的 552--648 KB/s 好不到哪去。

4.2 专业 AI 冷启动 34.8 s 和 45.1 s

专业 AI 的模型在网络记录里是 219 363 264 字节。开着硬件加速时模型 +20.5 s 下完、运行时 WASM +29.8 s 下完,冷启动总共 34.8 s。没有可用 GPU 适配器时模型 +21.4 s、运行时 +30.8 s,总共 45.1 s。

两者差的 10 秒主要在推理。同页再跑一次时 WebGPU 是 1 987 ms 而回退到 WASM 是 13 645 ms。拆出来的推理段 WebGPU 约 1.93 s、WASM 约 13.6 s,差了 7 倍。这个 7 倍只在这台 M4 和这个模型上成立,换机器换模型我不敢说。

四十五秒是什么概念?我给客户做的上传页超过五秒没反应客户就会发微信问「是不是卡了」。图映的进度会分阶段报文字:先是「正在从 ModelScope CDN 下载 快速 AI模型」,带已下载的字节数;然后是校验 SHA-256;再然后是「正在初始化 快速 AI」。我把快速 AI 那次的进度事件逐条对了一遍时间。「正在初始化」这一句从 +4.5 s 一直挂到 +13.6 s,这 9 秒里页面实际在等那个运行时 WASM 下完。用户看到的是「初始化」而其实是在下载。有进度文字能缓解一点,等待本身还是实打实记在用户那边。

4.3 回访和同页再跑要多久

冷启动只发生在第一次。缓存命中后新开一个页面,快速 AI 开着硬件加速是 930 ms 而没有 GPU 适配器是 2 643 ms。同一个页面再跑一次(会话已经建好)分别是 409 ms 和 1 951 ms,也就是 0.41 s 和 1.95 s。专业 AI 缓存命中的新页面是 2 891 ms 和 14 334 ms。

这组数说明端侧 AI 的等待账很不均匀。第一次要十几秒到四十几秒,之后只要零点几秒到一两秒。第一次的等待是「下载加建会话」的固定成本,之后只剩推理本身。一个用户只来抠一张图就走的话他付的全是固定成本。一个用户一次抠二十张的话固定成本就摊薄了。我站上的访客大多是前一种:搜进来、处理一张、关掉。这一点让我对端侧方案的热情降了一截。

五、浏览器跑 AI 模型占多少内存

第三笔换形态的是内存:服务器推理时内存是我那台机器的,端侧推理时内存是用户那台电脑的。

我记的是整个过程里浏览器进程的 RSS 峰值。快速 AI 开着硬件加速时渲染进程峰值 976 MB、GPU 进程 147 MB;没有 GPU 适配器时渲染进程 1 217 MB、GPU 进程 102 MB。专业 AI 开着硬件加速时渲染进程 1 637 MB、GPU 进程 232 MB;回退到 WASM 时渲染进程 3 580 MB、GPU 进程 99 MB。

3.58 GB 是这篇里我最想让同行记住的数。一个网页标签页为了抠一张图峰值吃掉了 3.58 GB。就为一张图。这是回退到 WASM 的专业 AI,是开着硬件加速时的 2.2 倍。16 GB 的 Mac 扛得住。别的机器难说。8 GB 内存又同时开着几十个标签页和一个微信的办公电脑会怎样,我没测也不敢下结论。图映在手机上直接不开放专业 AI,我猜跟这个数有关,但这只是猜。

这组数得带一个限定:它是进程 RSS 而不是显存。M4 的 CPU 和 GPU 共用一块内存,Metal 申请的缓冲区未必记在进程 RSS 头上。开着硬件加速时渲染进程更低未必是真的省,也可能是有一部分挪到了没被计进来的地方。显存到底占多少我没量到。这组数只能看趋势:同一个模型回退到 WASM 以后渲染进程明显更胖。

内存这笔账放在服务器上是我付而放在端侧是用户付。用户不会收到账单,他只会觉得电脑变卡、风扇转了、别的标签页被系统回收了。这种成本不出现在任何报表里,但会出现在跳出率里。

六、没有 GPU 的访客多付了什么

第四笔是回退与兼容:这里我只讲账而不讲回退代码怎么写。

navigator.gpu 存在不等于 WebGPU 能用。关掉浏览器的硬件加速以后 navigator.gpu 还在,requestAdapter() 却返回 null。普通用户真会遇到这种状态:有人关过硬件加速,也有的显卡会被浏览器拉黑。遇到这种访客时图映先试 WebGPU,失败了再切 WASM。缓存命中时这个判定本身只花 130 ms 左右,在关掉 GPU 的有界面浏览器里重测是 160 ms。判定本身不贵。

贵的是切过去以后:同样是快速 AI 同页再跑,WebGPU 是 0.41 s 而 WASM 是 1.95 s。专业 AI 的推理段是 1.93 s 对 13.6 s。内存是 1 637 MB 对 3 580 MB。没有 GPU 的访客下载一样多,却等得更久、内存吃得更多。

还有一笔是隐性的:页面没配 COOP/COEP 这两个响应头时浏览器不让 WASM 开多线程。ORT 只在控制台打一条 warning 然后单线程跑。基准里 ISNet INT8 在这种页面上是 6 318 ms,而配了头的四线程是 2 133 ms。慢三倍而且不报错。这一条最坑。图映的站点头是配了的。我自己的站要上端侧 AI 就得先加上这两个头。加了以后站上所有跨域资源都得带对应的头,不然会被拦。这个改动的工作量我估计比写抠图那段代码还大。这就是兼容成本:它不是钱而是我的工时。

七、端侧 AI 到底省了什么:一张账单

把前面的数摆在一起。下面这张表只有字节和秒而没有钱。

项 服务器推理 端侧推理(快速 AI) 记在谁头上
推理机器 要 不要 端侧省掉的就是这一项
新访客首次下载 不需要模型 50.18 MB / 人 模型托管方 + 本站
1000 个新访客 我没测 50.18 GB 44.23 GB 第三方 CDN;5.95 GB 本站
访客硬盘 无 约 44.23 MB 缓存 访客
冷启动等待 我没测 14.6--16.0 s 访客
回访等待 我没测 0.93 / 2.64 s 访客
同页再跑 我没测 0.41 / 1.95 s 访客
渲染进程内存峰值 我没测 976 / 1 217 MB 访客
回退与兼容 基本没有 COOP/COEP、两套后端 开发者的工时

服务器推理那一列我大面积写了「我没测」。那一侧的耗时、机器、费用我一样都没跑过,这里不编数。能确定的只有结构:服务器推理是「每张图一笔」而端侧推理是「每个新访客一笔」。前者按图数涨而后者按新访客数涨。

两种账的交点我能给一个式子却给不了答案。设你站上一张图上传加结果回传的平均字节是 B,端侧首次下载是 D(快速 AI 是 50 183 586 字节)。一个新访客处理的图超过 D ÷ B 张,端侧在流量上才开始划算。我那张合成样本只有 43 670 字节。这种压得特别小的图代进去没有意义。你得拿自己站上真实图片的平均大小去算。这还只是流量:服务器推理的机器费用在另一边,我没测也放不进这个式子。

算完我对那句话的理解改了。「端侧 AI 不花服务器钱」不算错,它只说了推理机器那一格。剩下几格的账没有消失而是大部分挪到了用户身上:他的带宽、他的等待、他的内存。挪到用户身上的成本不会出现在我的账单里。这就是它看起来「省」的原因。我原先只算了第一格。

八、我的小站打算怎么上

账算完我给自己定了几条;它们只对我这种小站成立。

第一条是默认只给快速档而且不预取。用户点了抠图按钮才开始下载,按钮旁边直接写「首次需下载约 50 MB」。专业档放在要主动展开的地方,旁边写「首次约 225 MB,内存吃得多」。我宁可少几个人用专业档也不想让误点的人白下 225 MB。

第二条是模型不放自己的主机。我的出口扛不住 44 MB 的并发下载,这一份交给公开的模型托管平台。运行时我自己托管:开 gzip、加上 application/wasm、设成 immutable 长缓存。托管平台挂了怎么办我还没想好,也没定要不要像图映那样留一个本站兜底。留了的话兜底那一刻的流量又回到我头上。

第三条是运行时和模型并行下。冷启动那张图让我记住了:两个文件串着下时等待是相加的。我的页面会在用户点按钮的同时把两个请求都发出去。这一条的效果我还没实测过。

第四条是看内存再决定开不开专业档。navigator.deviceMemory 在这台机器上报的是 16。低于某个值我就不显示专业档。阈值定多少我还没数据,先拍一个 8 以后看反馈再改。

第五条是服务器推理的方案我不删。先留着对照。等我真把两边都跑起来再拿同一批图算一次完整的账。那时候才轮得到钱。

九、适用场景 / 不适用场景 / 风险提示

9.1 适用场景

  • 访客大多一次处理很多张图,冷启动的固定成本能被摊薄。
  • 你没有 GPU 服务器也不想按次调接口,愿意让用户承担一次下载。
  • 模型能交给第三方托管,自己只出运行时那 5.95 MB。
  • 用户主要在内存不太紧的桌面浏览器上。

9.2 不适用场景

  • 访客大多搜进来、处理一张就走。每个人都付 50 MB 和十几秒的固定成本,却只用一次。
  • 目标用户在手机上。图映自己在手机上都只开放快速档;手机上的表现我没测。
  • 打算自托管大模型而主机出口很窄。1000 个新访客的专业档就是 225.08 GB 从你这里出去。

9.3 风险提示

  • 缓存是 best-effort 的:persisted() 为 false。被浏览器清掉以后老访客会重新变成新访客。
  • 第三方模型托管的规则可能变。变了以后流量会落回你自己的源站。
  • 没配 COOP/COEP 时 WASM 静默降成单线程。慢三倍也不报错。
  • 这篇的耗时全部来自一台 M4 Mac 和一条家用宽带。别把倍数当成普适结论。

十、测量方法自身的坑

量的过程中我踩了几个坑,有的差点让结论跑偏。挑六个说。

第一个是 MB 的进制:界面写「约 42 MB」而文件是 44 229 662 字节。按 1024 进制是 42.18 MiB,按十进制是 44.23 MB。我一开始混着用,算出来的 1000 个访客差了两 GB 多。后来我统一按十进制而界面的数只拿来对照。

第二个是字节有好几个版本:模型的文件字节是 44 229 662;网络记录里的响应体是 44 279 201;存进缓存后用量增长是 44 230 153。运行时 WASM 的 gzip 传输 curl 量到 5 953 924 而网络记录里是 5 955 745。差额我猜是头部、分块和缓存条目开销,但没有逐项拆。我的算术统一取文件字节和 curl 的 gzip 字节,这样别人能复现。

第三个是网速:那一晚运行时文件要 9 s 多,另一轮到了 34.2 s。冷启动的 14.6--16.0 s 是那一刻的网络给的而不是这个方案固有的。评估自己的站得在自己的服务器和自己用户所在的网络上量。

第四个是 RSS 不等于显存:统一内存的机器上 GPU 那部分分配不一定算进进程 RSS。开着硬件加速时渲染进程更低,但我不敢说它就更省内存。

第五个是计时方式:耗时来自直接调用页面里的抠图函数而不是点按钮。两者调的是同一个函数。点按钮还会多一点界面层的开销,这部分我没算。

第六个是样本:那张瓶子图是程序生成的。推理耗时和图片内容关系不大,但第七章交点公式里的 B 完全取决于真实图片。我没拿合成图的 43 670 字节去算交点就是这个原因。

十一、还没解决的

服务器推理那一侧的账我一直没算。那一侧的耗时、机器占用、费用这篇里一个数都没有。没有它第七章那张表永远只有半张。我打算用同一张图、同一个模型在服务器上跑一遍,把「每张图一笔」那一格也换成秒和字节。

骨灰级 AI 我只有文件字节。它下载要多久、推理吃多少内存都没测。按字节看它每个新访客要 453.22 MB。我不太会在自己站上开这一档。

缓存什么时候会被浏览器清掉我也没测。这直接决定「新访客」到底有多少。清得勤的话 2.2 节那个 50.18 GB 会被低估。

还有一件事让我拿不准。关掉硬件加速的用户到底占多少?我手上没有任何数据。一条都没有。占比很小的话第六章那些回退成本只是小概率事件;占比不小的话 3.58 GB 的内存峰值就得认真对待。

在那之前我能做的很具体:先把代码三在自己站上跑一遍,看运行时和模型到底压没压。再拿代码一把模型字节换成你打算用的那个文件,乘上你站上每月的新访客数。最后对照第七章那张表,把「我没测」那一列换成你自己服务器上的实测。

参考资料

  • ONNX Runtime Web 文档:执行提供程序(WebGPU / WASM)与 env.wasm.numThreads 说明
  • MDN:StorageManager.estimate()、StorageManager.persisted()
  • MDN:PerformanceResourceTiming.transferSize、Timing-Allow-Origin
  • MDN:Cross-Origin-Opener-Policy、Cross-Origin-Embedder-Policy 与 crossOriginIsolated
  • MDN:Navigator.deviceMemory
  • 模型:ISNet(IS-Net / DIS)、BEN2、BiRefNet 各自的开源仓库与许可说明
相关推荐
风华同学1 小时前
免密SSH登录Ubuntu
linux·运维·ubuntu
꯭自꯭闭꯭1 小时前
达梦守护集群手工切换及故障切换
linux·运维·服务器·数据库
IT_陈寒1 小时前
Java的HashMap线程安全问题让我深夜掉光了头发
前端·人工智能·后端
哈__1 小时前
日志散在多台服务器怎么查?用 Promtail + Loki + Grafana 搭一套集中检索平台
运维·服务器·grafana
CHENKONG_CK1 小时前
恶劣工况下 RFID 赋能喷涂产线自动化分拣与作业
运维·网络·人工智能·自动化·汽车·rfid·rfid
北京中科新远科技1 小时前
AI集群交换网络容量怎么算:端口、收敛比与Leaf-Spine验收
服务器·网络·人工智能
水如烟1 小时前
孤能子视角:EIS判据观测表——可对AI读数的操作接口
人工智能
BullSmall1 小时前
全套补充材料:评测报告模板 + Bad Case 分类方案 + 评测数据集构建规范
人工智能·分类·数据挖掘
wdfk_prog1 小时前
Wi-Fi Direct 教程 04:Interface 协议子系统初始化——WPA/EAPOL、WPS、DPP/NAN、GAS 与 P2P callback
android·运维·服务器·ubuntu·p2p·wps·wifi-direct