跳转至
推理系统手册 求职 下载 notebook

系统设计参考答案(一):MoE 部署、KV 缓存池、网关、多 LoRA、RL rollout

系统设计题一章给出了答题框架和一道完整的例题。这一章和下一章给出冲刺计划里 10 道系统设计题的参考答案,每道题都按"澄清 → 估算 → 架构 → 取舍 → 追问"组织,估算部分都是能运行的脚本。数字依赖假设,面试时要先把假设说出来;更重要的是推理的过程和取舍的理由,而不是和这里的数字一模一样。

1. 600B 级 MoE 模型的在线推理服务

为一个 DeepSeek-V3 规模的 MoE 模型(671B 总参数、37B 激活、MLA)设计在线服务:峰值 500 请求/秒,平均输入 3000 token、输出 800 token,前缀缓存命中约 50%;TTFT P99 < 2 s,TPOT < 50 ms。

澄清。 硬件(H800 / H100,节点间网络)、是否允许 FP8(模型原生 FP8,是)、流量的日内波动、是否有超长上下文。

估算。 用复盘一章从公开数据折算的单卡能力(prefill 每卡每秒约 4000 个真正计算的 token,decode 每卡每秒约 1850 个输出 token):

import math

QPS, ISL, OSL, HIT = 500, 3000, 800, 0.5        # 峰值请求/秒、平均输入与输出 token、前缀缓存命中率
PREFILL_GPU, DECODE_GPU = 4000, 1850             # 每卡每秒真正计算的 prefill token、每卡每秒输出 token(公开系统折算)
P_UNIT, D_UNIT = 32, 144                         # 部署单元:prefill 4 节点、decode 18 节点
p_gpus, d_gpus = QPS * ISL * (1 - HIT) / PREFILL_GPU, QPS * OSL / DECODE_GPU
p_units, d_units = math.ceil(p_gpus * 1.2 / P_UNIT), math.ceil(d_gpus * 1.2 / D_UNIT)   # 留 20% 余量
print(f"prefill 需要 {p_gpus:.0f} 卡 → {p_units} 个单元({p_units * P_UNIT} 卡);decode 需要 {d_gpus:.0f} 卡 → {d_units} 个单元({d_units * D_UNIT} 卡)")
seqs = DECODE_GPU / 20                            # TPOT 50 ms,每个请求每秒 20 个 token
kv = seqs * (ISL + OSL / 2) * 70272 / 1e9        # MLA:每 token 70 KB
print(f"decode 每卡约 {seqs:.0f} 个并发请求,KV {kv:.0f} GB;总计 {(p_units * P_UNIT + d_units * D_UNIT) // 8} 台 8 卡机")
gpus = p_units * P_UNIT + d_units * D_UNIT
print(f"按每卡每小时 $2:每小时 ${gpus * 2:,},每百万输出 token ${gpus * 2 / (QPS * OSL * 3600) * 1e6:.2f}")
输出
prefill 需要 188 卡 → 8 个单元(256 卡);decode 需要 216 卡 → 2 个单元(288 卡)
decode 每卡约 92 个并发请求,KV 22 GB;总计 68 台 8 卡机
按每卡每小时 $2:每小时 $1,088,每百万输出 token $0.76

架构。 PD 分离;prefill 单元 4 节点(注意力 DP、专家 EP32、高吞吐 all-to-all、双 micro-batch 重叠、分层 EPLB),decode 单元 18 节点(EP144、低延迟 all-to-all、每卡 2 个路由专家 + 1 个共享专家、全局 EPLB、MTP);全局调度器做缓存感知路由和 PD 配对;分布式 KV 缓存池(内存 + SSD)承接多轮对话的前缀。细节见大规模 EP 部署与 NVSHMEM 与 DeepEP。

取舍。

  • decode 的部署单元大(144 卡),扩缩容粒度粗:用更小的单元(比如 EP72)换灵活性,代价是每个专家分到的 token 少、效率下降;
  • MTP 在通信受限时可能变慢(MTP 与稀疏注意力),要按负载开关;
  • 流量低谷时 decode 单元利用率低:夜间可以把部分单元切给离线任务或 RL rollout。

追问:一个 decode 节点坏了怎么办?

参考思路

EP 组内一个节点故障,整个 144 卡的单元都无法完成 all-to-all。做法:单元级别的故障隔离——网关把流量切到其他单元,故障单元上正在生成的请求用"已生成的前缀"在别的单元上续写(KV 可以从缓存池取回或重算);备用节点替换后重新加载权重、预热、加入。所以部署时单元数至少要 N+1,这也是 20% 余量的来源之一。

2. 以 KV Cache 为中心的多级缓存池

为一个多轮对话 + 智能体平台(70B 模型,16 台 8 卡机)设计 KV 缓存池,目标是降低 TTFT 和 prefill 算力:会话平均上下文 8K,用户两轮之间平均间隔几分钟到几十分钟。

估算。 每层存得下多少会话、读回来要多久、重算要多久:

KV_TOKEN = 160e3                                 # 70B(GQA,FP8 KV)每 token 160 KB
NODES, CTX = 16, 8192                            # 16 台 8 卡机;平均每个会话的上下文
TIERS = [("GPU 显存(每卡留 40 GB 给 KV)", 8 * 40e9, None),     # (层级, 每节点容量, 读回一台机器的带宽)
         ("内存池(每台 1.5 TB,本机走 PCIe、远端走 RDMA)", 1.5e12, 8 * 50e9),
         ("SSD 池(每台 8 块,共 60 TB)", 60e12, 8 * 6e9)]
session = CTX * KV_TOKEN
recompute = 2 * 70.6e9 * CTX / (8 * 989e12 * 0.5)                # 8 卡、MFU 50% 重新 prefill
print(f"一个会话的 KV {session / 1e9:.1f} GB;重算需要 {recompute * 1e3:.0f} ms")
for name, cap, bw in TIERS:
    n = NODES * cap / session
    load = f",读回约 {session / bw * 1e3:.0f} ms" if bw else ""
    print(f"{name}:全集群可存约 {n:,.0f} 个会话{load}")
输出
一个会话的 KV 1.3 GB;重算需要 292 ms
GPU 显存(每卡留 40 GB 给 KV):全集群可存约 3,906 个会话
内存池(每台 1.5 TB,本机走 PCIe、远端走 RDMA):全集群可存约 18,311 个会话,读回约 3 ms
SSD 池(每台 8 块,共 60 TB):全集群可存约 732,422 个会话,读回约 27 ms

显存只放得下几千个活跃会话,间隔几分钟的多轮对话大多会被挤出显存;内存池多存 5 倍,SSD 再多 40 倍,而且从任何一层读回都比重算快 10 倍以上。

架构。

推理实例(GPU KV + 本机 CPU 缓存) ──写穿 / 淘汰写回──▶ 分布式 KV 池(元数据服务 + 各节点内存)──▶ SSD 池
        ▲                                                   │
        └──────── 命中:逐层读回(RDMA / PCIe,与计算重叠)◀──┘
全局键:前缀链式哈希(混入模型版本、LoRA、租户);路由器优先把请求发往已缓存前缀的实例

取舍。 写穿(算完即写,淘汰快、便于共享,写入量大)还是写回(淘汰时写,写入量小);块的粒度(大块元数据少、命中粒度粗);一致性要求低(丢了可以重算),不需要强一致的复制;淘汰策略按"读回节省的时间 × 被再次访问的概率"排序,而不是单纯的 LRU。详见 KV 传输引擎与分布式 KV 存储。

追问:模型升级之后缓存怎么办?

参考思路

KV 依赖权重,模型版本必须混进块键的种子里,新旧版本的块天然不会互相命中;旧版本的块随着淘汰自然消失,或者在切换完成后批量删除。灰度期间两个版本并存,缓存容量要按两份规划,或者给新版本分配独立的配额。

3. 全局调度与缓存感知的推理网关

设计一个推理网关:后端 200 个实例,峰值 5000 请求/秒,要求缓存感知路由、负载均衡、故障转移和灰度发布。

估算。

QPS, INSTANCES, PROMPT, BLOCK = 5000, 200, 4096, 64      # 网关的峰值、后端实例数、平均提示词长度、块大小
KV_TOKENS = 2_000_000                                     # 每个实例能缓存的 token
blocks = PROMPT // BLOCK
per_instance = [blocks * INSTANCES, blocks]              # 方案一:逐个实例查;方案二:全局前缀树,节点上记录实例集合
print(f"每个请求 {blocks} 个块;逐实例查询每秒 {QPS * per_instance[0] / 1e6:.0f}M 次查找,全局前缀树每秒 {QPS * per_instance[1] / 1e3:.0f}K 次")
nodes = INSTANCES * KV_TOKENS // BLOCK
print(f"索引规模:最多 {nodes / 1e6:.2f}M 个块节点,每个约 64 字节 → {nodes * 64 / 2**20:.0f} MiB,单机内存放得下")
print(f"一个实例故障:健康检查每秒一次、连续 3 次失败才摘除,这 3 秒里发往它的约 {QPS / INSTANCES * 3:.0f} 个请求需要重试;"
      f"用被动检测(请求失败立即标记)可以缩短到几百毫秒")
输出
每个请求 64 个块;逐实例查询每秒 64M 次查找,全局前缀树每秒 320K 次
索引规模:最多 6.25M 个块节点,每个约 64 字节 → 381 MiB,单机内存放得下
一个实例故障:健康检查每秒一次、连续 3 次失败才摘除,这 3 秒里发往它的约 75 个请求需要重试;用被动检测(请求失败立即标记)可以缩短到几百毫秒

架构。 无状态的网关进程多副本部署(前面是四层负载均衡),共享同一份路由索引(由实例发布的 KV 事件驱动更新,或每个网关各自维护近似索引);路由代价 = 排队时间 + 未命中部分的 prefill 时间,过载时提前拒绝(全局调度);主动 + 被动健康检查、熔断、带幂等键的重试;灰度按权重分流,并按模型版本隔离缓存索引。

取舍。 索引放在网关内存里(快、多副本之间不一致)还是独立服务(一致、多一跳);会话亲和与负载均衡的权重;流式请求中途失败时,是报错让客户端重试,还是由网关用已生成的内容续写(体验好,但要保证采样的连续性)。

追问:网关成为瓶颈怎么办?

参考思路

网关只做路由和转发,按上面的估算单核每秒可以处理数万次路由决策,瓶颈通常在 SSE 连接数和 TLS。水平扩展网关副本即可;索引用"实例发布事件 → 每个网关订阅"的方式保持近似一致,偶尔路由错了只多算一些 prefill。热点前缀(比如同一个系统提示词)要限制单个实例承接的比例,避免缓存感知把流量压到一个实例上。

4. 多租户 LoRA 服务

一个 8B 基座模型服务上千个 LoRA 适配器(秩 16),不同租户的请求混合到达;要求加载快、互不干扰、按租户计费。

估算。

LAYERS, D, KV, FF, R = 32, 4096, 1024, 14336, 16          # 8B 级基座(GQA)、LoRA 秩 16,作用在全部 7 个投影上
shapes = [(D, D), (D, KV), (D, KV), (D, D), (D, FF), (D, FF), (FF, D)]   # q k v o gate up down
params = LAYERS * R * sum(i + o for i, o in shapes)
size = params * 2                                          # bf16
print(f"一个适配器 {params / 1e6:.1f}M 参数、{size / 2**20:.0f} MiB;1000 个共 {1000 * size / 2**30:.0f} GiB")
print(f"GPU 上留 8 GiB 给适配器:可常驻 {8 * 2**30 // size} 个;从 CPU 内存经 PCIe(25 GB/s)换入一个 {size / 25e9 * 1e3:.1f} ms")
base = 2 * 8.0e9
print(f"每 token 的计算:基座 {base / 1e9:.0f} GFLOPs,LoRA 额外 {2 * params / 1e9:.2f} GFLOPs({2 * params / base:.1%})")
for distinct in (1, 16, 64):                               # 一个 batch 里有多少个不同的适配器
    print(f"batch 256 个请求用到 {distinct} 个适配器:LoRA 部分要读 {distinct * size / 2**30:.2f} GiB 权重,"
          f"约是读一遍基座(16 GB)的 {distinct * size / 16e9:.0%}")
输出
一个适配器 41.9M 参数、80 MiB;1000 个共 78 GiB
GPU 上留 8 GiB 给适配器:可常驻 102 个;从 CPU 内存经 PCIe(25 GB/s)换入一个 3.4 ms
每 token 的计算:基座 16 GFLOPs,LoRA 额外 0.08 GFLOPs(0.5%)
batch 256 个请求用到 1 个适配器:LoRA 部分要读 0.08 GiB 权重,约是读一遍基座(16 GB)的 1%
batch 256 个请求用到 16 个适配器:LoRA 部分要读 1.25 GiB 权重,约是读一遍基座(16 GB)的 8%
batch 256 个请求用到 64 个适配器:LoRA 部分要读 5.00 GiB 权重,约是读一遍基座(16 GB)的 34%

LoRA 的计算几乎可以忽略,代价在权重读取:decode 受访存限制,一个 batch 里不同的适配器越多,要读的 LoRA 权重越多。

架构。 基座权重共享;适配器三级存储(GPU 常驻约 100 个热门的、CPU 内存放全部、对象存储做持久化),按 LRU 换入换出;批处理时同一个 batch 混合多个适配器,用分段 GEMM(Punica 的 BGMV / SGMV,S-LoRA 的统一分页内存)一次算完;路由器按适配器做亲和,让同一个适配器的请求集中到少数实例,降低每个 batch 里不同适配器的数量;按租户限流、计费(按 token 计,LoRA 的额外开销很小)。

取舍。 每个实例能同时服务的适配器数与 batch 效率之间的权衡;适配器的秩越大越贵(线性增长);前缀缓存的键必须混入适配器 ID(LoRA 改变了 K、V)。

追问:一个冷门适配器的第一个请求延迟很高怎么办?

参考思路

换入一个适配器只要几毫秒(上面的估算),真正慢的是"从对象存储下载"(几百毫秒到秒级)和"所在实例的 batch 已满需要排队"。做法:所有适配器预先放在每台机器的内存或本地盘上;冷门适配器路由到负载低的实例;首个请求可以先在一个专门的"冷启动"实例上服务,同时后台把适配器预热到亲和实例。

5. RL rollout 系统

为推理模型的 RL 训练设计 rollout 系统:32B 策略模型,每步 128 个问题 × 16 个回答、平均输出 8K token,训练一步约 5 分钟;要求 rollout 不拖慢训练。

估算。

PROMPTS, G, AVG_OUT, TRAIN_S = 128, 16, 8000, 300         # 每步 128 个问题 × 16 个回答,平均输出 8K token;训练一步 5 分钟
TPUT = 2500                                                # 32B 策略模型,每张 H100 在大 batch 下每秒生成的 token
tokens = PROMPTS * G * AVG_OUT
for name, util in (("同步(长尾,利用率约 30%)", 0.3), ("全异步(利用率约 90%)", 0.9)):
    gpus = tokens / (TPUT * util * TRAIN_S)                # 让 rollout 跟上训练的节奏
    print(f"{name}:每步 {tokens / 1e6:.1f}M 个输出 token,需要约 {gpus:.0f} 张卡做 rollout")
w = 32e9 * 1                                               # FP8 权重
print(f"权重同步:{w / 1e9:.0f} GB,按 8 卡并行 + 实例间接力约 {w / (8 * 50e9):.2f} s,远小于一步的 {TRAIN_S} s")
输出
同步(长尾,利用率约 30%):每步 16.4M 个输出 token,需要约 73 张卡做 rollout
全异步(利用率约 90%):每步 16.4M 个输出 token,需要约 24 张卡做 rollout
权重同步:32 GB,按 8 卡并行 + 实例间接力约 0.08 s,远小于一步的 300 s

架构。 训练与推理分离部署(或小规模时共置);推理池用连续批处理持续生成,全异步或一步异步流水;打分服务(规则、沙箱、奖励模型)独立扩缩容;权重同步按切分映射点对点发送;推理端返回逐 token 的 logprob 和(MoE 的)路由结果。细节见 RL 推理系统。

取舍。 同步简单、无偏但浪费 2/3 的推理算力;全异步高效,但陈旧度集中在长回答上,要做重要性修正、按长度组批、限制陈旧度;共置省卡但要切换显存。

追问:打分服务很慢(比如代码题要跑测试)怎么办?

参考思路

打分和生成流水起来:一条回答生成完立刻送去打分,不等整批;打分服务按 CPU / 沙箱资源独立扩缩容,并设超时(超时算失败或丢弃并记录);对同一问题的 16 个回答并行打分。打分的耗时也会拉长"样本的陈旧度",全异步系统里要把它算进陈旧度的上限。

小结

  • 每道题先澄清会改变设计的假设,再用"需求 ÷ 单卡(单实例)能力 × 余量"做容量估算,并检查显存、带宽和延迟的约束。
  • 600B MoE:PD 分离 + prefill 小 EP / decode 大 EP,约 70 台 8 卡机服务 500 请求/秒;KV 缓存池:显存只放得下几千个会话,内存池和 SSD 扩大 5~200 倍,读回都比重算快 10 倍以上。
  • 网关:全局前缀树让路由查找从每秒 6400 万次降到 32 万次;多 LoRA:代价在权重读取而非计算,要按适配器亲和路由;RL rollout:全异步让推理卡数减到三分之一。

发现错误或有更好的写法?在 GitHub 上编辑此页提交问题