05 - 进程放置与并行
你手上有 2 张 H200,要跑一个 omni 模型,它有 8 个阶段。怎么摆?
第一版摆法很自然:thinker 最大,独占 0 号卡;talker 和声码器放 1 号卡;几个预处理和编码器阶段放 CPU。跑起来没问题。
然后你看利用率:1 号卡长期只有不到 2%。
原因不难想:talker 要等 thinker 先吐出内容才有活干,而且比它小一个数量级。那张 H200 绝大部分时间在等。
于是你想把 talker 挪到 0 号卡,跟 thinker 挤一挤。
马上撞上一个麻烦:两个阶段都在启动时申请显存,而它们是先后加载的。先加载的看到「还剩 90 GB」就按九成去要,等第二个加载时已经不够了。这不是参数配错,是「还剩多少」这句话在多阶段场景下本身有歧义。
一、四种放置形态
阶段、进程、GPU 三者不是一一对应 的。同一个阶段可能占满一张卡,也可能几个阶段挤在一个进程里共用一张卡;一个阶段还可能横跨四张卡。
实际会用到的组合就下面四种,各自解决的问题不同:
四种形态是正交的,实际部署常常是②和③的组合。逐项对照:
| ① 单进程 | ② 按阶段拆 | ③ 同卡共置 | ④ 张量并行 | |
|---|---|---|---|---|
| 怎么配 | 所有阶段 process 同名 | 每阶段一个 process 名 | 不同 process,同一个 gpu | gpu 写成列表 + tp_size |
| 买到什么 | 边全走同进程直传,零传输开销 | 按段独立扩缩,声码器与引擎重叠 | 利用率低的阶段不再独占一张卡 | 单阶段能装下更大的模型 |
| 代价 | CPU 预处理会堵住调度循环 | 多一个 CUDA 上下文,进程内缓存重复 | 显存预算必须显式声明,算错就 OOM | 每层之后要 all-reduce |
| 什么时候用 | 本地调试、单请求 延迟优先 | 生产默认,多数模型的出厂配置 | 某个阶段明显吃不满一张卡时 | 只给大骨干用,小模块别上 |
③ 那一列的动机很具体:talker 曾长期在一张 H200 上跑不到 2% 利用率。④ 的反面同样具体:小模块上张量并行,每层的 all-reduce 通信开销会盖过并行收益。
二、放置的校验规则
上一节四种形态摆在那,问题是:怎么告诉框架你要哪一种?
答案是只靠一个字段 —— StageConfig.process。同名即同进程,不同名即拆开。没有第二个地方能影响进程拓扑,它是唯一真相来源。
模型的配置类给默认值,配置文件或带点号的 CLI 参数覆盖它,跟其他阶段字段没有区别。但合法的组合是有限的,框架在启动时会校验:
# 把声码器单独放进一个进程
sgl-omni serve --model-path MODEL --vocoder.process vocoder
# 重复同一个进程名即共置:下面这条复现了 Higgs-TTS 内置配置的拓扑
sgl-omni serve --model-path bosonai/higgs-tts-3-4b \
--preprocessing.process tts_frontend \
--audio_encoder.process tts_frontend
# tts_frontend : preprocessing, audio_encoder
# pipeline : tts_engine
# vocoder : vocoder
启动前由放置与拓扑规划器校验四条:
- 每个非 TP 阶段都必须声明
process;TP 阶段每个 rank 自动派生一个进程。 - 一个进程组可以跨若干 CPU 阶段,但至多只能占一张 GPU。
- 多个进程组共享一张 GPU 时,每个涉及的 GPU 阶段都必须声明
gpu_memory_fraction,且每卡总和要装进placement.max_total_gpu_memory_fraction_per_gpu。校验会点名说出哪些阶段漏了。 - TP rank 的进程名不能与其他进程组撞车。
有一类边过不了进程边界,而且不会在配置校验时报错。 某些阶段之间靠进程内注册表交换状态 —— MOSS-TTS 用进程内队列把预处理好的请求交给 AR 引擎,Qwen3-TTS 把准备好的请求放在进程内模块状态里。把这种边拆开,配置校验会通过,到真正服务时才失败。出厂配置把这些阶段放在同一个进程里,别拆。
反例是 Ming-Omni-TTS:它把预处理字段放在 StagePayload.data 里,参考编码器的 spk_emb 和 prompt_latent 用 typed_tensor 线格式序列化,所以它的两条边都能跨进程。能不能跨进程,取决于这条边上的状态是不是可序列化的载荷 —— 这是接新模型时要主动设计的,不是自然就有的。