本地部署大模型启动慢:先测盘,别急着换显卡
本地部署大模型启动慢:先测盘,别急着换显卡
📌 本文部分内容由 AI 辅助整理,已经人工核对。文中所有带单位的数都来自我在自己机器上真跑的脚本,脚本全文在第十节,零依赖、标准库就能跑,你可以原样复制下来在自己的盘上验一遍。引用的两处外部资料(Linux
open(2)手册、llama.cpp 的加载参数)我抓了原文,逐字贴在下面。
有人问我,为什么同样一台机器,早上第一次起服务要等好几分钟,下午再起就几秒钟。他怀疑是显卡有问题。
不是显卡的事。
这台机器上我量了一下:同一个文件,绕开缓存读是 109.30 MiB/s,命中缓存读是 4.35 GiB/s,差 40.7 倍。早上那次付的是第一个价,下午那次付的是第二个价。显卡在这两次里干的活一模一样。
这篇把这条链路拆开算一遍。结论先放这儿:在很多本地部署的场景里,第一个撞到的瓶颈不在算力,在存储。 而这一层多数人根本没量过。
一、先把两件事分开
「慢」这个字底下压着两件完全不同的事:
- 等多久才开始吐第一个字 —— 权重要从盘读出来、进内存、进显存。这是存储的事。
- 开始吐之后每秒几个字 —— 每生成一个 token 要把权重过一遍。这是显存带宽的事。
这两件事的瓶颈不在一个地方,优化手段也完全不一样。换显卡只解决第二件。 如果你的痛点是「点了启动然后去接了杯水回来还在转」,换卡一点用都没有。
本文只讲第一件。
二、权重从盘到内存,中间隔着一个 page cache
Linux 会把读过的文件页留在内存里。下次再读同一个文件,直接从内存给你,根本不碰盘。这就是「第二次快」的全部原因。
想量出盘的真实速度,就得绕开它。open(2) 手册里给了办法:
O_DIRECT (since Linux 2.4.10)
Try to minimize cache effects of the I/O to and from this
file. In general this will degrade performance, but it is
useful in special situations, such as when applications do
their own caching. File I/O is done directly to/from user-
space buffers.
—— 来源:man 2 open(man7.org 在线版原文)
注意它自己写的那句 In general this will degrade performance。O_DIRECT 不是用来加速的,是用来测得准的。加了它,数据必须真的从块设备上来。
我的脚本就靠这个拿冷读的数:
==================================================================
一、这台机器的顺序读带宽
==================================================================
冷读 (O_DIRECT,绕开 page cache) : 109.30 MiB/s
热读 (命中 page cache) : 4.35 GiB/s
热读 / 冷读 : 40.7 倍
先说清楚我这台机器是什么:云上的一台容器,数据盘是共享卷。 109 MiB/s 这个数放在物理 NVMe 上是不正常的,放在共享卷上很正常。你在自己机器上跑,数会完全不同 —— 这正是要你自己跑一遍的原因。
我连着跑了三次,冷读分别是 107.68 / 88.60 / 109.30 MiB/s。抖动接近两成。共享存储就是这样,单次测不准,多跑几次看区间。
慢会不会是我块开小了?
这是我第一反应怀疑的。O_DIRECT 单线程读,队列深度是 1,块开太小确实会被延迟吃掉。所以我扫了一遍块大小:
块大小扫描(确认慢不是因为你块开小了):
128 KiB 97.03 MiB/s
512 KiB 108.27 MiB/s
1024 KiB 112.28 MiB/s
4096 KiB 102.74 MiB/s
16384 KiB 105.03 MiB/s
区间 97.03 MiB/s ~ 112.28 MiB/s,1.2 倍
从 128 KiB 扫到 16 MiB,带宽只在 1.2 倍的区间里晃,没有量级变化。块大小不是原因,这条链路本身就这么快。
这一步别省。你要是不扫,很容易把一个「测法问题」当成「硬件问题」,然后去买一块并不解决问题的盘。
同时也要承认这个数的边界:它是单线程、队列深度 1 测出来的。真实的加载器可能有预读、可能并发读,拿到的会比这个高。所以把它当下界看,不要当上限。
三、拿你自己的带宽算一遍
带宽有了,剩下的就是除法。权重文件多大,取决于参数量和每个参数占几个字节:
参数量 FP16 / BF16 INT8 / Q8_0 Q5_K_M 附近 Q4_K_M 附近
------ ------------ ------------ ------------ -----------
7B 2 分 02 秒 1 分 01 秒 39.7 秒 33.6 秒
14B 4 分 04 秒 2 分 02 秒 1 分 19 秒 1 分 07 秒
32B 9 分 18 秒 4 分 39 秒 3 分 01 秒 2 分 34 秒
70B 20 分 21 秒 10 分 11 秒 6 分 37 秒 5 分 36 秒
235B 1 小时 08 分 34 分 10 秒 22 分 13 秒 18 分 48 秒
671B 3 小时 15 分 1 小时 37 分 1 小时 03 分 53 分 40 秒
这是按 109.30 MiB/s 算的,换成你的带宽,整张表按比例缩放。
看最后一行。671B 的 FP16 权重,在这条链路上光是把它读一遍就要三个多小时。这不是模型的问题,也不是卡的问题,是一个除法的问题。
再看一张横向的,同一个 70B / Q4 量级的模型(权重约 35.86 GiB)在不同介质上:
介质 顺序读 加载耗时
----------------- ------------ ----------
本机实测(冷读) 109.30 MiB/s 5 分 36 秒
机械硬盘 7200rpm 171.66 MiB/s 3 分 34 秒
SATA SSD 524.52 MiB/s 1 分 10 秒
NVMe Gen3 3.26 GiB/s 11.0 秒
NVMe Gen4 6.52 GiB/s 5.5 秒
千兆网 NAS(1GbE) 112.53 MiB/s 5 分 26 秒
万兆网 NAS(10GbE) 1.07 GiB/s 33.5 秒
介质那一列是厂商标称量级,只作横向参照,不是我实测的。但这张表能回答一个很实际的问题:从 5 分 36 秒到 11 秒,中间隔的不是一块更贵的显卡,是一块 NVMe。
还有一行值得多看一眼:千兆网的 NAS,112 MiB/s,跟机械盘一个量级。 很多人把模型放在千兆网的共享存储上,然后奇怪为什么加载这么慢 —— 那根网线就是天花板,盘再快也出不来。
四、「加载完了」可能是假象
这一步最容易骗人。
现在的加载器多数默认用 mmap,把权重文件映射进地址空间,并不立刻读。页是在第一次被访问的时候才真正从盘上来的。所以你会看到一个很奇怪的现象:进度条唰一下就满了,但第一次提问卡了很久。 时间没有消失,它只是从「加载」挪到了「第一次推理」。
llama.cpp 把这几种加载方式做成了一个参数,原文是这样写的:
{"-lm", "--load-mode"}, "MODE",
"model loading mode (default: mmap)\n"
"- none: no special loading mode\n"
"- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)\n"
"- mlock: force system to keep model in RAM rather than swapping or compressing\n"
"- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing\n"
"- dio: use DirectIO if available\n"
—— 来源:llama.cpp 仓库 common/arg.cpp,我抓的是 master 分支当前内容。同一处代码里还有一行弃用提示:--mmap / --no-mmap / --direct-io 这些老参数正在被 --load-mode 取代,两套混用只有最后一个生效。
默认是 mmap。 也就是说,如果你没特意改过,你测到的「加载时间」大概率不是完整的读盘时间。
想看到真实成本,两个办法:要么用 mlock 那档,强制它真的把权重压进内存不许换出;要么直接看 dio,让它走 DirectIO。对照着跑一次,差出来的那段时间就是之前被藏起来的。
五、现象 → 多半是什么 → 怎么确认
把上面几节压成一张排查表。左边是你看到的,中间是我的判断,右边是别信我,自己验的方法。
| 现象 | 多半是什么 | 怎么确认 |
|---|---|---|
| 第一次几分钟,第二次几秒 | page cache 命中 | 跑脚本比冷读/热读,看倍数 |
| 换了个模型又变慢了 | 缓存被新文件挤掉了 | 比一下可用内存和模型总大小 |
| 显卡很新,启动照样慢 | 瓶颈在盘不在卡 | 测顺序读,套第三节的表 |
| 加载「秒完」但第一次回答很卡 | mmap 惰性加载,成本挪到了首次推理 | 换 --load-mode mlock 或 dio 对照 |
| 盘标称很快,实测很慢 | 共享卷 / 虚拟化 / 网络存储 | 先扫块大小排除测法,再看是不是网络盘 |
| 多节点比单节点还慢 | 权重分发走了网络 | 看网卡速率,以及每个节点是各拉一份还是共享一份 |
| 换了 NVMe 但没快多少 | 瓶颈已经不在这一层了 | 重测冷读,若已达标就去查框架初始化和显存拷贝 |
六、权重只是小头
到这儿为止说的都是权重。但如果你做的是知识库、检索、文档问答这类事,权重根本不是占地方的大头。
一个 70B 的 Q4 权重是 35 GiB 左右。而企业真正要喂进去的东西 —— 历年的合同、图纸、工单、会议录音、监控录像 —— 动辄几个 TB 到几十 TB。这些数据要落盘、要建索引、要被反复读。
这就带来两个和上面完全不同的问题:
- 容量的量级不一样。 权重是几十 GiB,语料是几十 TiB,差三个数量级。按权重规划的存储,装不下语料。
- 读法不一样。 权重是顺序读,一次读完;检索是随机读,大量小请求。顺序带宽再高,随机 IOPS 不行照样卡。我这个脚本只量了顺序读,量不了这个。 别拿它的结论去推检索性能。
我见过的返工,多数不是「模型选错了」,是一开始按模型的尺寸规划了存储,跑通之后才发现数据放不下。
七、到了多节点,这件事会被放大
单机的时候,权重读一遍就完事。多节点就不一样了:
- 每个节点各存一份?存储成本乘以节点数,而且更新模型要同步 N 份。
- 放共享存储上,各节点从那儿拉?那么拉取要走网络,前面那张表里「千兆网 112 MiB/s」的行就会重演一次,只是这次要乘以节点数。
于是问题的形状变了。单机的时候你在选一块盘;多节点的时候你在选一个「权重放哪儿、谁负责发给谁」的结构。 这已经不是买块 NVMe 能解决的了。
八、那该找什么样的机器
这一节是判断,不是实测,我把话说在前面。
顺着上面几节的逻辑推下来,如果你确认自己卡在存储和分发这一层,那你要找的不是一台更强的算力机,而是一个把三件事合在一起的角色:
- 容量按语料规划,不是按权重规划 —— 前面算过,两者差三个数量级。
- 它自己就是共享存储 —— 权重和语料放它上面,算力节点从它取,而不是每台各存一份。
- 它和算力节点之间是高速直连 —— 不然第七节那个网络瓶颈会原样搬过来。这里的关键是链路,不是盘。
也就是说,在「一堆算力节点」之外,你还需要一个承担存储与调度的枢纽节点。这类形态现在做得到桌面尺寸,不必是一整个机柜。
我这么说不是因为它更时髦。是因为这三条要求单独买任何一样都满足不了:买盘解决不了分发,买交换机解决不了容量,买卡跟这一层完全无关。
九、什么时候不该在存储上花钱
写完上面一节我得把话说回来,不然这篇就成软文了。下面这些情况,你不该动存储:
- 你的模型能常驻内存。 如果机器内存装得下权重,而且服务是长期开着的,那读盘只发生一次。第一次等 5 分钟,之后几个月都不再付这个成本 —— 这不值得为它换硬件。
- 你的痛点是「吐字慢」不是「等得久」。 回到第一节,这两件事的瓶颈不在一个地方。这种情况下换盘一点用没有。
- 你只跑 7B 以下。 看第三节那张表,7B 的 Q4 在我这条最慢的链路上也就 33.6 秒。33 秒不值得为它做架构改造。
- 你还没量过。 这是最重要的一条。先跑一遍脚本再决定,别因为看了这篇就去买盘。我给的是方法,不是结论 —— 你的瓶颈在哪一层,只有你自己的数说了算。
第 4 条我想多说一句。上面每一张表我都给了脚本、给了口径、说了它测不到什么,就是为了让你能推翻我。要是你跑出来冷读有 3 GiB/s,那这篇文章对你就是没用的,这很正常。
十、脚本
零依赖,标准库跑得动就行。默认在当前目录造一个 2 GiB 的临时文件,跑完自己删。
python3 model_load_budget.py # 默认 2 GiB
python3 model_load_budget.py --size-gb 8 # 文件越大冷读越准
python3 model_load_budget.py --dir /mnt/nvme # 指定测哪块盘
python3 model_load_budget.py --sweep # 扫块大小,排除测法问题
python3 model_load_budget.py --json # 机器可读
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
model_load_budget.py —— 把「本地跑大模型,启动要等多久」这件事算清楚。
它做三件事:
1. 实测你这台机器的顺序读带宽。分两个数:
- 冷读:用 O_DIRECT 绕开 page cache,逼数据真的从盘上来
- 热读:普通读,命中 page cache
这两个数常常差一个数量级,而「第二次快了」正是最容易骗到人的地方。
2. 用实测带宽,算不同规模 / 不同量化的权重文件要多久才能进内存。
3. 给一张按介质档位的速查表,方便你判断该不该换盘。
零依赖,只用标准库。默认在当前目录建一个 2 GiB 临时文件,跑完自己删。
python3 model_load_budget.py # 默认 2 GiB 测试文件
python3 model_load_budget.py --size-gb 8 # 文件越大,冷读越接近真实值
python3 model_load_budget.py --dir /mnt/nvme # 指定要测哪块盘
python3 model_load_budget.py --json # 机器可读输出
注意:脚本量的是「顺序读带宽」,不是随机 IOPS。加载模型权重是典型顺序读,
所以这个数是对的;但如果你的瓶颈在向量库检索,那要量的是另一个东西(见文末)。
"""
import argparse
import json
import mmap
import os
import sys
import time
BLOCK = 1 << 20 # 1 MiB 对齐块,O_DIRECT 要求对齐
ALIGN = 4096
def human(n):
"""字节数转人话。"""
for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
if abs(n) < 1024 or unit == 'TiB':
return '%.2f %s' % (n, unit)
n /= 1024.0
def hms(seconds):
"""秒数转人话,够短就只说秒。"""
if seconds < 60:
return '%.1f 秒' % seconds
m, s = divmod(seconds, 60)
if m < 60:
return '%d 分 %02.0f 秒' % (m, s)
h, m = divmod(m, 60)
return '%d 小时 %02d 分' % (h, m)
def make_file(path, size_bytes):
"""造一个测试文件。写完 fsync,确保真落盘而不是停在页缓存里。"""
buf = os.urandom(BLOCK) # 用随机数据,避免文件系统压缩/去重把测试做废
written = 0
with open(path, 'wb') as f:
while written < size_bytes:
n = min(BLOCK, size_bytes - written)
f.write(buf[:n])
written += n
f.flush()
os.fsync(f.fileno())
return written
def read_direct(path, size_bytes, block=BLOCK):
"""冷读:O_DIRECT 绕过 page cache,数据必须从块设备上来。
注意这是单线程、队列深度 1 的读法。真实的加载器可能有预读或并发,
拿到的数会比这个高。所以这个值是一条**保守下界**,不是盘的极限值。
"""
flags = os.O_RDONLY | getattr(os, 'O_DIRECT', 0)
fd = os.open(path, flags)
# O_DIRECT 要求用户态缓冲区地址对齐,mmap 匿名映射天然页对齐
buf = mmap.mmap(-1, block)
total = 0
t0 = time.perf_counter()
try:
while True:
n = os.preadv(fd, [buf], total) if hasattr(os, 'preadv') else None
if n is None: # 老内核退路
os.lseek(fd, total, os.SEEK_SET)
n = os.readv(fd, [buf])
if n <= 0:
break
total += n
finally:
elapsed = time.perf_counter() - t0
os.close(fd)
buf.close()
return total, elapsed
def read_cached(path):
"""热读:普通读。刚写完/刚读过,大概率命中 page cache。"""
total = 0
t0 = time.perf_counter()
with open(path, 'rb') as f:
while True:
b = f.read(BLOCK)
if not b:
break
total += len(b)
return total, time.perf_counter() - t0
# 参数量 × 每参数字节数 → 权重文件大小
# 说明:这里算的是权重本身,不含 KV Cache、不含框架开销。
QUANTS = [
('FP16 / BF16', 2.0),
('INT8 / Q8_0', 1.0),
('Q5_K_M 附近', 0.65),
('Q4_K_M 附近', 0.55),
]
MODEL_SIZES_B = [7, 14, 32, 70, 235, 671]
# 常见介质的顺序读带宽档位(厂商标称量级,用于横向参照,不是本机实测)
MEDIA = [
('机械硬盘 7200rpm', 180e6),
('SATA SSD', 550e6),
('NVMe Gen3', 3.5e9),
('NVMe Gen4', 7.0e9),
('千兆网 NAS(1GbE)', 118e6),
('万兆网 NAS(10GbE)', 1.15e9),
]
def table(rows, headers):
"""按显示宽度对齐(中文算两格),别用 len()。"""
def w(s):
return sum(2 if ord(c) > 0x2E80 else 1 for c in str(s))
widths = [max(w(h), max((w(r[i]) for r in rows), default=0)) for i, h in enumerate(headers)]
line = ' '.join(h + ' ' * (widths[i] - w(h)) for i, h in enumerate(headers))
out = [line, ' '.join('-' * x for x in widths)]
for r in rows:
out.append(' '.join(str(c) + ' ' * (widths[i] - w(c)) for i, c in enumerate(r)))
return '\n'.join(out)
def main():
ap = argparse.ArgumentParser(description='算清本地跑大模型的存储这一层')
ap.add_argument('--size-gb', type=float, default=2.0, help='测试文件大小 GiB,默认 2')
ap.add_argument('--dir', default='.', help='在哪块盘上测,默认当前目录')
ap.add_argument('--keep', action='store_true', help='跑完保留测试文件')
ap.add_argument('--sweep', action='store_true',
help='扫一遍块大小,确认冷读慢不是你块开小了')
ap.add_argument('--json', action='store_true', help='输出 JSON')
a = ap.parse_args()
size = int(a.size_gb * (1 << 30))
path = os.path.join(a.dir, '_model_load_probe.bin')
if not a.json:
print('\n在 %s 上造一个 %s 的测试文件…' % (os.path.abspath(a.dir), human(size)))
made = make_file(path, size)
try:
try:
cold_bytes, cold_t = read_direct(path, made)
cold_bw = cold_bytes / cold_t
direct_ok = True
except OSError as e:
# 有些文件系统(tmpfs、部分网络盘、overlayfs)不支持 O_DIRECT
cold_bw, direct_ok = None, False
cold_err = '%s: %s' % (type(e).__name__, e)
warm_bytes, warm_t = read_cached(path)
warm_bw = warm_bytes / warm_t
sweep = []
if a.sweep and direct_ok:
for kb in (128, 512, 1024, 4096, 16384):
try:
b, t = read_direct(path, made, block=kb << 10)
sweep.append((kb, b / t))
except OSError:
pass
finally:
if not a.keep and os.path.exists(path):
os.remove(path)
result = {
'dir': os.path.abspath(a.dir),
'file_size_bytes': made,
'cold_read_bytes_per_s': cold_bw,
'warm_read_bytes_per_s': warm_bw,
'o_direct': direct_ok,
}
if a.json:
result['load_seconds'] = {}
base = cold_bw or warm_bw
for p in MODEL_SIZES_B:
for qname, bpp in QUANTS:
result['load_seconds']['%dB/%s' % (p, qname)] = p * 1e9 * bpp / base
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print('\n' + '=' * 66)
print('一、这台机器的顺序读带宽')
print('=' * 66)
if direct_ok:
print(' 冷读 (O_DIRECT,绕开 page cache) : %s/s' % human(cold_bw))
else:
print(' 冷读 : 本文件系统不支持 O_DIRECT(%s)' % cold_err)
print(' 下面一律按热读算,会明显偏乐观,别拿它做采购决策。')
print(' 热读 (命中 page cache) : %s/s' % human(warm_bw))
if direct_ok and cold_bw:
print(' 热读 / 冷读 : %.1f 倍' % (warm_bw / cold_bw))
print('\n 这个倍数就是「第一次慢、第二次快」的全部原因。')
print(' 你重启机器、或者换一个模型把缓存挤掉之后,付的是冷读那个价。')
if sweep:
print('\n 块大小扫描(确认慢不是因为你块开小了):')
for kb, bw in sweep:
print(' %6d KiB %s/s' % (kb, human(bw)))
lo = min(b for _, b in sweep)
hi = max(b for _, b in sweep)
print(' 区间 %s/s ~ %s/s,%.1f 倍' % (human(lo), human(hi), hi / lo))
print(' 没有量级变化 = 瓶颈不在块大小,在这条链路本身。')
base = cold_bw or warm_bw
label = '冷读' if direct_ok else '热读(偏乐观)'
print('\n' + '=' * 66)
print('二、按本机%s带宽,权重从盘进内存要多久' % label)
print('=' * 66)
rows = []
for p in MODEL_SIZES_B:
row = ['%dB' % p]
for qname, bpp in QUANTS:
nbytes = p * 1e9 * bpp
row.append(hms(nbytes / base))
rows.append(row)
print(table(rows, ['参数量'] + [q[0] for q in QUANTS]))
print('\n 只算权重读盘,不含显存拷贝、不含框架初始化、不含 KV Cache。')
print(' 所以这是一条下界:真实启动时间只会比它长。')
print('\n' + '=' * 66)
print('三、换介质能换来什么(厂商标称量级,横向参照用)')
print('=' * 66)
demo = 70, 0.55 # 拿 70B / Q4_K_M 当标尺
nbytes = demo[0] * 1e9 * demo[1]
rows = [['本机实测(%s)' % label, human(base) + '/s', hms(nbytes / base)]]
for name, bw in MEDIA:
rows.append([name, human(bw) + '/s', hms(nbytes / bw)])
print(' 标尺:%dB 模型,%s,权重约 %s\n' % (demo[0], QUANTS[3][0], human(nbytes)))
print(table(rows, ['介质', '顺序读', '加载耗时']))
print('\n' + '=' * 66)
print('四、这个脚本没量的东西')
print('=' * 66)
print(' · 随机读 / IOPS —— 向量库检索、大量小文件是随机读,跟顺序带宽是两回事')
print(' · 网络存储的抖动 —— NAS/共享盘的尾延迟比平均带宽更能决定体感')
print(' · 多节点分发 —— 一份权重要进 N 台机器时,瓶颈会从盘转到网')
print(' · 显存带宽 —— 那决定「每秒吐几个 token」,跟本文的「等多久才开始吐」无关')
print()
if __name__ == '__main__':
sys.exit(main())
十一、这套方法不解决什么
说清楚边界,免得当银弹用:
- 只量顺序读。 向量检索、大量小文件是随机读,跟顺序带宽是两回事,本脚本量不了。
- 只量单线程、队列深度 1。 真实加载器可能并发读、可能预读,实际会比这个快。这是下界不是上限。
- 没量网络存储的抖动。 NAS 和共享盘的尾延迟常常比平均带宽更决定体感,而平均值会把尾巴抹平。
- 没算显存拷贝和框架初始化。 从内存到显存那一段、以及框架自己的启动开销,都不在这条时间线里。所以真实启动时间只会比表里的长。
- 共享卷上的数会抖。 我三次冷读差了两成。单次读数不要当结论,多跑几次看区间。
小结
- 「启动慢」和「吐字慢」是两件事,瓶颈不在一个地方,换显卡只解决后一件。
- 冷读和热读能差一个数量级 —— 我这台机器是 40.7 倍。「第二次快」是缓存,不是问题解决了。
- 想测准就得绕开 page cache,
O_DIRECT就是干这个的,手册原话是它会让性能变差、但适用于「应用自己做缓存」这类场景。 - 测出慢之后先扫块大小,排除是自己测法的问题。我从 128 KiB 扫到 16 MiB 只差 1.2 倍,说明瓶颈是链路本身。
- 加载时间 = 权重大小 ÷ 冷读带宽。这个除法能一路算到 671B,也能反过来告诉你换介质能省多少。
- 默认的 mmap 是惰性的,「加载完了」不等于「读完了」,时间可能被挪到了首次推理。
- 做知识库的话,权重是小头,语料才是大头,而且它是随机读,另一套账。
- 到多节点,问题从「选一块盘」变成「选一个权重放哪儿、谁发给谁的结构」。
最后留一个我确实没算明白的问题:多节点的时候,一份权重到底该放哪儿。
我知道的有三条路,代价各不相同:
一、每个节点各存一份全量权重。 最简单,没有单点,任何一台挂了不影响别人。代价是存储成本乘以节点数,而且每次换模型要同步 N 份,同步期间版本还可能不一致。
二、共享一份,由一个节点统一存放并分发。 省存储、换模型只改一处。代价是多了一个所有人都依赖的角色,它慢或者它挂了是全局的事。
三、放对象存储,节点按需拉取并本地缓存。 弹性最好,加节点不用预先规划容量。代价是首次拉取慢、缓存命中率不好预测,而且你要多运维一套东西。
这三条我都见过有人选,在各自的规模下都是对的。分界点在哪儿,我没有像样的推导 —— 它同时取决于节点数、模型换得勤不勤、以及你能不能接受一个全局依赖。我自己目前是按节点数拍的:几台以内倾向第二条,再多就不确定了。这个「几台」是经验,不是算出来的。
这个分界点我一直想算清楚,但一直没找到能算的方法 —— 找到的资料要么只讲某一条路怎么实现,要么直接给结论不给推导。这一块我目前是空的。
更多推荐


所有评论(0)