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

系统设计参考答案(二):长上下文、投机解码、结构化输出、可观测性、多地域

接上一章,这里是另外 5 道系统设计题的参考答案。这几道题更偏"平台":延迟与成本的权衡、CPU 与 GPU 的配合、扩缩容与容灾。同样,每道题的估算都能运行,数字依赖假设,面试时要先把假设说清楚。

6. 百万 token 长上下文服务

为一个 MLA + MoE 模型(DeepSeek-V3 规模)提供最长 100 万 token 的上下文:文档问答、代码库分析。要求首 token 延迟"可接受",decode 速度不明显下降。

估算。

L, ACTIVE, LAYERS = 1_000_000, 37e9, 61                     # 100 万 token;MLA + MoE 模型(激活 370 亿参数、61 层)
print(f"KV:MLA 每 token 70 KB → {L * 70e3 / 1e9:.0f} GB;同尺寸的 GQA 稠密模型(FP8,160 KB)→ {L * 160e3 / 1e9:.0f} GB")
linear = 2 * ACTIVE * L                                     # 激活参数的矩阵乘
dense = L * L / 2 * 81920 * LAYERS                          # 因果注意力:L²/2 对,每对 82K FLOPs(展开的多头注意力)
index = L * L / 2 * 16384 * LAYERS                          # DSA 索引器给所有历史 token 打分(FP8,按两倍吞吐计)
sparse = L * 2048 * 278528 * LAYERS                         # 每个 query 只对 2048 个 token 做吸收后的 MLA
rate = 64 * 989e12 * 0.5                                    # 64 张卡、MFU 50%
print(f"稠密 prefill:线性层 {linear:.1e} + 注意力 {dense:.1e} FLOPs,64 卡约 {(linear + dense) / rate:.0f} s")
print(f"稀疏注意力(DSA):索引器 {index:.1e} + 选中部分 {sparse:.1e} FLOPs,64 卡约 {(linear + index / 2 + sparse) / rate:.0f} s")
print(f"decode 每步读 KV:稠密 {L * 1152 * LAYERS / 1e9:.0f} GB,DSA {(L * 132 + 2048 * 1152) * LAYERS / 1e9:.0f} GB(索引器的键 + 选中的潜向量)")
输出
KV:MLA 每 token 70 KB → 70 GB;同尺寸的 GQA 稠密模型(FP8,160 KB)→ 160 GB
稠密 prefill:线性层 7.4e+16 + 注意力 2.5e+18 FLOPs,64 卡约 81 s
稀疏注意力(DSA):索引器 5.0e+17 + 选中部分 3.5e+16 FLOPs,64 卡约 11 s
decode 每步读 KV:稠密 70 GB,DSA 8 GB(索引器的键 + 选中的潜向量)

百万 token 时,注意力的计算是线性层的 30 多倍:稠密模型即使用 64 张卡,首 token 也要一分多钟;稀疏注意力把它压到十秒左右。decode 每一步要读 70 GB 的 KV(一台 8 卡机的显存带宽约 27 TB/s,就是约 2.6 ms 的下限),稀疏注意力读的少得多。

架构。 长上下文请求走独立的资源池,不与普通请求混在一起(否则一个百万 token 的 prefill 会拖住整个实例);prefill 用上下文并行(CP)把序列切到多台机器上,ring / Ulysses 通信(见分布式训练手册的上下文并行);KV 卸载到内存池、SSD;前缀缓存是最大的杠杆——同一个文档或代码库的第二个问题,prefill 几乎免费;可选的有损手段(KV 淘汰)只用于对精度不敏感的场景。

取舍。 专用资源池利用率低 vs 混部干扰大;CP 的通信开销 vs 单机算不动;首 token 延迟与"先返回一部分结果"(流式处理文档)之间的产品取舍。

追问:用户上传同一份 50 万 token 的文档问了 10 个问题,怎么优化?

参考思路

文档部分的 KV 算一次、存进缓存池(内存 + SSD,这里约 35 GB),后续问题只算问题本身的 prefill,TTFT 从几十秒降到亚秒级。要点:提示词的组织让文档在前、问题在后(前缀才能复用);路由把同一个文档的请求发到已有缓存的实例或能快速读回的位置;按文档哈希计费或设置缓存的保留时间。

7. 投机解码服务化

在一个对话服务上启用投机解码(EAGLE 或 MTP),要求在低负载时降低延迟、高负载时不影响吞吐,并能监控效果。

估算。 小 batch(访存受限)时,每步能前进的 token 数和加速比取决于接受率 \(\alpha\) 和草稿数 \(k\)(每个草稿 token 的开销按一步 decode 的 5% 计):

C = 0.05                                                    # 每个草稿 token 的开销(相对一步 decode)
print("接受率   最佳草稿数   每步前进 token   小 batch 时的加速")
for a in (0.6, 0.8, 0.9):
    best = max(range(1, 9), key=lambda k: (1 - a ** (k + 1)) / (1 - a) / (1 + k * C))
    adv = (1 - a ** (best + 1)) / (1 - a)
    print(f"{a:>6}   {best:>10}   {adv:>13.2f}   {adv / (1 + best * C):>14.2f}x")
输出
接受率   最佳草稿数   每步前进 token   小 batch 时的加速
   0.6            4            2.31             1.92x
   0.8            8            4.33             3.09x
   0.9            8            6.13             4.38x

这是上限:batch 变大后验证 \(k+1\) 个 token 不再免费,加速迅速下降,甚至变慢(投机解码进阶)。

架构。 草稿模块(EAGLE 头或 MTP 层)与目标模型部署在同一张卡上、共享 KV 管理;按负载动态调整草稿数:batch 小时用较多的草稿,batch 超过阈值时减少到 1 个或关闭;按业务域(代码、闲聊、翻译)分别统计接受率,接受率低的流量不开投机;监控每步前进的 token 数、接受率分布、TPOT 分位数,以及"开 / 关投机"的对照组。

取舍。 更多的草稿 = 更高的上限但更大的浪费;树形草稿接受率更高但验证更贵;草稿模型需要随目标模型一起更新(MTP 天然同步,独立草稿模型要重新训练)。

追问:接受率突然下降了,可能是什么原因?

参考思路

流量结构变了(比如新上线的业务是代码生成,草稿在这类文本上不准);目标模型更新了但草稿没更新;采样参数变了(高温度下接受率天然低);实现问题(量化或 kernel 变化导致草稿与目标的数值不一致)。按业务域、按模型版本、按温度拆开看接受率,通常很快能定位。

8. 结构化输出与工具调用服务

提供 JSON Schema 约束的输出和函数调用(工具调用),要求约束解码不明显增加延迟,流式输出时能尽早解析出工具调用。

估算。 约束解码每一步都要为每个请求算一个"哪些 token 合法"的掩码(词表十几万),这是 CPU 上的工作:

STEP_MS = 25                                                # 一步 decode 的 GPU 时间
for per_req_us, name in ((200, "朴素实现:每步遍历词表"), (20, "预编译语法(大部分 token 的掩码提前算好)")):
    for batch in (32, 256):
        cpu = batch * per_req_us / 1000
        verdict = "可以藏在 GPU 计算后面" if cpu < STEP_MS else "超过一步的时间,成为瓶颈"
        print(f"{name},batch {batch}:CPU 生成掩码 {cpu:.1f} ms / 步(GPU {STEP_MS} ms)→ {verdict}")
输出
朴素实现:每步遍历词表,batch 32:CPU 生成掩码 6.4 ms / 步(GPU 25 ms)→ 可以藏在 GPU 计算后面
朴素实现:每步遍历词表,batch 256:CPU 生成掩码 51.2 ms / 步(GPU 25 ms)→ 超过一步的时间,成为瓶颈
预编译语法(大部分 token 的掩码提前算好),batch 32:CPU 生成掩码 0.6 ms / 步(GPU 25 ms)→ 可以藏在 GPU 计算后面
预编译语法(大部分 token 的掩码提前算好),batch 256:CPU 生成掩码 5.1 ms / 步(GPU 25 ms)→ 可以藏在 GPU 计算后面

架构。 语法引擎(XGrammar 等)预编译 JSON Schema,把"与上下文无关"的 token 掩码提前算好,运行时只处理少量依赖栈状态的 token;掩码计算与 GPU 前向重叠(在 GPU 算这一步时,CPU 为下一步准备掩码),多线程并行处理不同请求;语法状态随请求保存、支持回滚(配合投机解码);工具调用的流式解析器按模型的调用格式增量解析,一旦识别出完整的函数名和参数就提前触发;超时和格式错误的重试策略在网关层实现。详见结构化输出与工具调用。

取舍。 约束越强越安全,但可能让模型"被迫"输出不自然的内容(质量下降);有的场景用"事后校验 + 重试"比约束解码更便宜;复杂的 Schema 编译很慢,要缓存编译结果。

追问:约束解码会不会影响模型的输出质量?

参考思路

会有影响:掩码改变了采样分布,模型想输出的 token 被禁止时只能选次优的,严格的格式(比如必须先输出某个字段)可能打断模型的"思路"。常见的缓解:推理模型先自由地思考,只在最终答案部分施加约束;Schema 设计得宽松一些;对比有无约束时的任务指标,而不只看格式正确率。

9. 推理可观测性与自动扩缩容

为一个多模型的推理平台设计指标体系和自动扩缩容:流量有明显的日内波动,偶尔有突发;要求满足 SLO 的同时降低成本。

估算。

import math

WEIGHTS = 140e9                                             # 70B 的 BF16 权重
for name, bw in (("对象存储(1 GB/s)", 1e9), ("本地 NVMe(7 GB/s)", 7e9), ("从已运行的实例经 RDMA 拉取(8 × 50 GB/s)", 400e9)):
    print(f"冷启动加载权重:{name} {WEIGHTS / bw:.1f} s")
print("另外还有:进程启动与 CUDA 初始化约 10~20 s,CUDA Graph 录制与 JIT 编译约 10~60 s(可以缓存)")
QPS, LAT, PER_INST = 200, 12, 16.8                          # 到达率、平均时延(s)、单实例满足 SLO 的最大速率
need = QPS / PER_INST
growth = 0.30 * 2 / 5                                       # 流量 5 分钟涨 30%,冷启动 2 分钟内会再涨 12%
print(f"Little 定律:平均并发 {QPS * LAT} 个请求;需要 {need:.1f} 个实例;冷启动的 2 分钟里流量还会涨 {growth:.0%},"
      f"至少预留 {math.ceil(need * growth)} 个热备实例")
输出
冷启动加载权重:对象存储(1 GB/s) 140.0 s
冷启动加载权重:本地 NVMe(7 GB/s) 20.0 s
冷启动加载权重:从已运行的实例经 RDMA 拉取(8 × 50 GB/s) 0.3 s
另外还有:进程启动与 CUDA 初始化约 10~20 s,CUDA Graph 录制与 JIT 编译约 10~60 s(可以缓存)
Little 定律:平均并发 2400 个请求;需要 11.9 个实例;冷启动的 2 分钟里流量还会涨 12%,至少预留 2 个热备实例

指标体系。 三层:用户体验(TTFT、TPOT / ITL 的 P50 / P99,goodput = 满足 SLO 的请求比例,错误率);引擎状态(排队长度、运行中的请求数、KV 使用率、抢占次数、前缀缓存命中率、每步的 batch 大小);资源与成本(GPU 显存带宽和 SM 利用率、每百万 token 成本)。按模型、租户、请求长度分桶统计——长请求和短请求混在一起的平均值没有意义。

扩缩容。 触发信号用排队长度、KV 使用率和 TTFT 的趋势,不用 GPU 利用率(decode 时利用率天然不高);按历史的日内曲线提前扩容,突发时依靠热备;缩容要慢(等请求跑完、避免抖动);冷启动优化:权重放本地 NVMe 或从已运行的实例用 RDMA 拉取、缓存 CUDA Graph 和编译结果。

追问:P99 TTFT 突然升高,怎么排查?

参考思路

先分清是"排队"还是"算得慢":看排队长度和 prefill 本身的耗时。排队长 → 流量突增或实例减少(故障、扩容失败)、路由不均(某个实例被缓存感知压成热点);prefill 慢 → 长请求变多(按长度分桶看)、前缀缓存命中率下降(模型发布、路由变更)、抢占变多(KV 不够)。再对照变更记录(发布、配置、流量来源)。指标按实例、模型、长度拆开,是定位的前提。

10. 跨地域多集群推理平台

在多个地域部署同一个 700B 模型的推理服务:用户就近接入,任意一个地域故障时服务不中断,并尽量降低成本。

估算。

MODEL, REGIONS, LINK = 700e9, 12, 10e9 / 8                  # FP8 的 700B 模型;12 个地域;跨地域专线 10 Gb/s
print(f"逐个地域从中心拷贝:{MODEL / LINK * REGIONS / 3600:.1f} 小时;分块接力分发(收到一块就转发给下一个地域,按两倍余量)约 {MODEL / LINK / 3600 * 2:.1f} 小时")
for n in (3, 4, 6):                                         # 任意一个地域故障时,剩下的要接住它的流量
    print(f"{n} 个地域均摊流量:每个地域平时最多跑到 {1 - 1 / n:.0%} 的容量,才能在一个地域故障时不过载")
输出
逐个地域从中心拷贝:1.9 小时;分块接力分发(收到一块就转发给下一个地域,按两倍余量)约 0.3 小时
3 个地域均摊流量:每个地域平时最多跑到 67% 的容量,才能在一个地域故障时不过载
4 个地域均摊流量:每个地域平时最多跑到 75% 的容量,才能在一个地域故障时不过载
6 个地域均摊流量:每个地域平时最多跑到 83% 的容量,才能在一个地域故障时不过载

架构。 全局流量调度(DNS / Anycast + 全局负载均衡)按延迟和各地域的剩余容量分配;每个地域是一个完整的集群(网关、推理实例、KV 缓存池),KV 缓存不跨地域共享(跨地域读回比重算还慢);模型分发用分块接力或 P2P,新版本先在一个地域灰度;多活容灾,每个地域的容量按"N−1 仍能承载全部流量"规划(地域越多,冗余比例越低);成本上利用地域之间的时差转移可延迟的离线任务、使用竞价实例承接离线批处理。

取舍。 地域越多延迟越低、冗余比例越低,但运维和模型分发越复杂;会话粘在一个地域(缓存命中高)vs 按负载跨地域调度(均衡但缓存失效);数据合规可能要求某些用户的请求不能出境,这会限制调度的自由度。

追问:一个地域整体故障,正在进行的长对话怎么办?

参考思路

流式请求会中断,由客户端或全局网关重试到其他地域;对话历史保存在跨地域复制的会话存储里(文本,不是 KV),新地域重新 prefill 这个会话(缓存未命中,TTFT 会变长)。为避免雪崩,全局调度要限速地转移流量,并在目标地域触发扩容;非交互的离线任务先暂停,把容量让给在线流量。

小结

  • 长上下文:百万 token 时注意力计算是线性层的数十倍,靠稀疏注意力、上下文并行、专用资源池和前缀缓存;KV 容量另靠卸载。
  • 投机解码:接受率 0.8 时小 batch 上限约 3 倍,按负载动态调整草稿数,按业务域监控接受率。
  • 结构化输出:掩码计算在 CPU 上,要预编译语法并与 GPU 前向重叠,否则大 batch 时成为瓶颈。
  • 可观测性与扩缩容:按体验、引擎、资源三层建指标,按排队与 KV 使用率扩缩容,冷启动靠本地缓存与热备。
  • 多地域:N 个地域均摊时每个地域最多跑到 (N−1)/N 的容量;KV 不跨地域共享,模型分块接力分发。

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