多模态LLM并行扩展与可扩展计算分配:从负载均衡到资源利用率
2026/8/27 4:07:59 网站建设 项目流程

你负责一个多模态大模型服务的线上部署时,通常会怎么做?把请求切成 batch,分发到多张 GPU 上,然后观察显存和吞吐。最开始一切正常,但很快你就会发现一个很拧巴的现象:图片请求占比升高时,有些卡在视觉编码阶段忙得不行,有些卡却在等解码;视频请求一旦进来,预填充阶段会瞬间吃掉大量算力,后面生成阶段反而很空闲。你以为是并行度不够,于是加卡、拆 batch,结果吞吐并没有线性涨。

ParVL 这个命名,指的不是一个简单的加卡方案,它把 “Parallel Scaling” 和 “Expandable Compute Allocation” 放到一起,更像是在提示一个判断:多模态 LLM 的并行瓶颈,往往不在并发请求的数量,而在计算负载在时间和空间上的分布。只有把“并行”和“分配”放在同一个框架里设计,才可能真正跑出合理的资源利用率。

1. 多模态 LLM 真正难并行的地方,不是模型体积,而是计算负载分布

很多团队在部署多模态大模型时,默认沿用单模态文本模型的服务方式。这个思路本身没有错,但它忽略了一个关键点:多模态输入的计算负载不是均匀的,也不是只在解码阶段发生。模型变大只是让显存压力增加,真正让并行扩展变难的,是请求内部的计算分布完全不可控。

1.1 多模态请求不是等量请求

在多模态场景里,一个请求的“大小”不是一个固定值。文本请求可能是几百个 token,图像请求在视觉编码阶段会产生几十到上千个视觉 token,视频请求的 token 数量会随帧数、分辨率和采样策略成倍增长。这些 token 不是均匀地进入模型,它们要在不同阶段被处理:图像要经过视觉编码器,视频要按帧抽取和编码,音频则要经过对应的模态编码器。无论这些编码器的参数量是大是小,都会在推理流程里占用独立的计算路径。

如果只用 batch size 或并发数来衡量负载,就会漏掉最关键的变量:每个请求的内部计算分布。一个很小的 batch,可能因为含有一个高分辨率图片或一段长视频,而比一个几十条纯文本请求的 batch 消耗更多显存和算力。反过来,一个很大的 batch 如果全是短文本,也可能让 GPU 在解码阶段保持低占用。

1.2 预填充与解码:两条计算曲线完全不同

文本大模型的推理里有明显的 prefill(预填充)和 decode(解码)阶段。prefill 阶段并行度高,适合用高算力快速处理;decode 阶段是逐 token 生成,并行度下降,更依赖访存带宽和调度效率。多模态模型在 prefill 阶段会额外加入视觉 token,因此可能出现一个很长的 prefill,直接拉长单请求首字延迟。

这带来一个很实际的影响:并行策略需要区分阶段。固定地按请求维度并行,很容易让某些卡在 prefill 时满负荷,在 decode 时又闲置。你可以把整个推理过程想象成一条流水线:每个阶段需要的资源不一样,必须允许资源在不同阶段之间伸缩。

1.3 固定并行策略的低效,本质上是负载错配

任何并行策略都有偏重。数据并行偏重提高吞吐,张量并行偏重拆开大模型,序列并行偏重长上下文,流水线并行偏重阶段解耦。多模态模型恰好不是单一形状的负载:它在 prefill 阶段需要大量算力处理视觉 token,在 decode 阶段需要为已生成的文本连续分配计算资源,在跨模态融合阶段又会反复读取视觉特征。如果只用一个并行维度,很容易出现某个维度成为瓶颈,其他维度资源闲置。

另一个容易忽视的点是存储带宽。图像和视频经过编码后会产生大量中间特征,这些特征可能被多次读取和保存。并行扩展如果只增加 GPU 数量,但内存带宽和互联带宽没有同步扩大,计算分配就会在数据搬运处卡住。所以,ParVL 这类方案强调 “可扩展计算分配”,不光是调度 GPU 算力,还要考虑显存、带宽和排队资源。

2. 并行扩展与可扩展计算分配:一个问题的两个面

ParVL 这个名字可以拆成两个部分:Par 是 Parallel,VL 指向多模态视觉语言场景。整套思路强调的不是某一个具体实现,而是扩展和分配的组合策略。先想清楚并行扩展,再想清楚计算分配,最后把两者合并成一个调度问题。

2.1 并行扩展的本质是拆分计算单元

并行不是简单地让多个请求同时跑,而是把推理过程拆成多个可以并发执行的计算单元。数据并行容易理解,把不同请求分到不同设备;张量并行是把一个大算子切分成多个小算子放到不同 GPU;序列并行是把长序列切到多个设备;流水线并行是把不同阶段分到不同设备。对多模态 LLM 来说,更常见的是组合使用。

一个请求从输入到输出,可能存在多个计算阶段。你可以让视觉编码在 A 组 GPU 上执行,让视觉语言融合在 B 组 GPU 上执行,让自回归解码在 C 组 GPU 上执行。这其实就是一种并行扩展。但如果 A、B、C 之间的资源是固定比例,那么请求模态一变,资源就会失衡。于是需要第二个能力。

2.2 可扩展计算分配,不是简单动态 batch

Expandable Compute Allocation 的关键,是让每个计算阶段的资源配额可以随负载动态调整。它比动态 batch 更复杂。动态 batch 解决的是 “一次处理多少请求”,而可扩展计算分配解决的是 “一个请求内部的不同阶段各拿多少计算资源”。

可以把它理解成一个资源调度问题。先是状态感知,知道当前请求里的视觉 token 数量、文本长度、视频帧数;然后是负载预测,估算每个阶段会消耗多少算力和显存;最后是配额调整,把空闲资源按需分配给瓶颈阶段。三者配合,才能避免某一阶段被卡住。

2.3 一个可复用框架:先画像、选维度、做调度、看复盘

这里把方法论收束为一个四步框架:

  • 先画像:统计真实请求里模态的分布、视觉 token 范围、prefill/decode 耗时。
  • 选维度:根据单卡显存和模型并行度,确定张量并行、序列并行、流水线并行或组合。
  • 做调度:设计一个按计算负载分配资源的调度器,而不是按请求数量均匀分发。
  • 看复盘:用固定基线和真实流量回放来验证,确认吞吐和延迟是否真的受益。

这个框架适合大多数多模态 LLM 服务部署场景。后续所有优化,都可以先回到这四步,看是哪一步缺了。如果你发现自己已经在调大量参数,但资源和请求的匹配关系没有建立,那大概率是画像和复盘没做好。

3. 给多模态 LLM 设计并行服务的四条实操步骤

到这里,我们需要回到工程落地。很多人在第一次做多模态并行服务时,容易直接从“加 batch”开始,但这很容易踩坑。更稳妥的做法,是先把流程拆成四步。

3.1 第一步:计算画像,把每种输入算清楚

在开始动调度器之前,先用一个 profiling 脚本,构造几条不同类型的请求。建议至少覆盖短文本、长文本、单张低分辨率图片、单张高分辨率图片、多图、视频片段。对每条请求记录:

  • prefill 阶段耗时和显存峰值。
  • decode 阶段每秒生成 token 数。
  • 总等待时间和总生成时长。
  • 视觉编码器耗时,以及是否具有独立的显存峰值。

这些数据会告诉你,资源瓶颈是在视觉编码、prefill 还是 decode。如果所有请求的 prefill 占比都很低,那视觉资源池就不需要做得太复杂;如果视频请求经常把显存打满,就需要按帧数做切分或动态分配。

3.2 第二步:选择并行维度,不要只加卡

画像之后,你就可以判断哪些并行维度是必要的。

  • 如果模型本身超过单卡显存,首先考虑张量并行。
  • 如果请求里有很长的图像序列或视频,序列并行的价值会变大,因为它能把长视觉序列拆分到多个设备。
  • 如果你希望不同阶段使用不同设备,可以考虑流水线并行,但要接受阶段之间存在排队和通信开销。
  • 如果只是请求数量多且负载均匀,数据并行才是主要收益来源。

不要一开始就把所有并行维度都打开。组合并行的调试复杂度是非线性的,最好先在一个维度上验证收益,再逐步叠加。我见过不少项目,一个张量并行还没调明白,就把四种并行全部开启,最后根本无法判断瓶颈是通信还是计算。

3.3 第三步:用调度器实现可扩展计算分配

一个朴素但有效的实现方式,是在请求进入推理引擎之前加一个轻量调度层。调度层先估算请求的计算负载,再决定把它送到哪类 worker,或者是否需要拆分。这里给出一个示例结构,不是一个可以直接上生产的实现。

def estimate_compute_load(request): text_tokens = len(request.text) // 4 # 大约估计,实际以 tokenizer 为准 vision_tokens = 0 if request.image: vision_tokens += estimate_vision_tokens(request.image) if request.video: vision_tokens += frame_count(request.video) * tokens_per_frame prefill_weight = text_tokens + vision_tokens * vision_factor decode_weight = estimate_output_len(request) return {"prefill": prefill_weight, "decode": decode_weight} def schedule_request(request, pools): load = estimate_compute_load(request) if load["prefill"] > THRESHOLD: return pools.prefill_pool.submit(request) return pools.general_pool.submit(request)

这段代码的意义不是完成任务,而是表达一个思路:调度器不感知模型细节,只感知计算负载的大致分布。实际生产里,你还需要处理队列水位、超时、重试、优先级和显存碎片。这里最关键的是建立“按负载分配”而不是“按请求个数分配”的直觉。

3.4 第四步:和固定基线对比,验证是否真的有用

任何优化都要有基线。建议先用一个固定 batch 的并行服务做基线,记录三个指标:

  • 吞吐:每分钟完成请求数或生成 token 数。
  • 延迟:不同请求类型的 P50/P95 首字延迟和总延迟。
  • 资源利用率:GPU 利用率、显存峰值和平均占用。

然后,把动态计算分配调度跑起来,用同一批请求回放。不要只看平均指标,要看不同模态下的分位数。一个方案如果只是把文本请求的延迟提高了,但图片或视频请求的延迟显著下降,在某些业务里仍然是值得的,关键是要用业务目标衡量。

注意:对比时不要用不同请求集合,否则调度和基线看到的负载分布不一样,结论没有意义。

4. 为什么单次跑通不等于能稳定批量使用

在很多实验里,动态分配看起来不错,但一进入批量生产,问题就接踵而来。原因不是原理错了,而是工程链路还没补齐。

4.1 连续批处理和 prefill/decode 解耦,是并行扩展的地基

很多多模态并行方案在单次请求上表现不错,一进批量环境就崩,根源是绝大多数推理引擎默认把 prefill 和 decode 放在一个 batch 里,视觉 token 又导致 prefill 时间变长,后面的 decode 请求长期得不到调度。连续批处理(continuous batching)允许引擎在请求完成时立即插入新请求,而不是等整个 batch 完成;prefill/decode 解耦则进一步把 prefill 任务和 decode 任务分开调度。没有这两项基础能力,单纯做并行扩展会非常吃力。

4.2 可扩展计算分配的副作用

动态分配资源不是免费的。它引入了几类新开销:

  • 调度开销:每次请求都要计算负载、选择 worker,队列本身也可能成为瓶颈。
  • 排队波动:当视觉请求突然增多时,视觉资源池扩容需要时间,新请求可能在队列里等待。
  • 显存碎片:频繁动态创建和释放 worker,会让显存碎片率上升,甚至导致 OOM。
  • 长尾效应:资源分配策略若偏向均衡,可能让一个高负载请求拖长全局队列。

这些副作用决定了:不是所有系统都应该上动态计算分配。如果请求量小、模态单一,固定分配反而更稳定。

4.3 三个最容易踩的坑

第一,把并行维度当参数乱调。张量并行、流水线并行、序列并行各有通信开销,叠加在一起未必有收益。实际上,多模态场景里最常见的问题不是并行维度不够,而是 prefill 阶段过长和视觉 token 过多。

第二,直接全量切到动态调度。动态分配需要先在小流量下做对比,观察队列和显存碎片。如果没有监控和回滚能力,全量上线的风险很高。

第三,忽略日志和监控。可扩展计算分配如果只做调度,不做观测,你就无法知道资源具体分配给了谁。至少要记录每个请求的负载估计、调度决策、实际耗时,这样才能复盘瓶颈在哪。

5. 多模态并行服务性能异常时的排查链路

性能变差时,不要急着改调度器。先按固定顺序排查,能省下很多时间。

5.1 先看现象,别急着拆调度器

性能异常的现象通常是这几类:GPU 利用率低、延迟高、显存 OOM、请求卡住不返回、吞吐不随并发提升。现象不同,排查路径也不同。如果 GPU 利用率低,优先看是否请求不够多,或者 prefill/decode 阶段被锁死;如果延迟高,优先看队列排队,再看单请求耗时;如果 OOM,优先看显存分配和碎片,而不是单纯加卡。不要一上来就觉得是调度器有问题,可能只是输入数据变了。

5.2 从输入、环境、参数、工具边界逐层排除

可以把排查过程整理成一个顺序:

排查层需要确认的内容常见问题
输入请求里图片分辨率、视频帧数、文本长度、多图数量视觉 token 爆炸导致 prefill 变长
环境GPU 型号、显存、CUDA 版本、依赖库版本、CPU 内存带宽版本升级导致算子行为变化
参数batch size、max_num_seqs、prefill chunk size、动态池上限参数设置过小或过大
调度器队列水位、负载估计误差、调度频率、优先级策略估计不准导致资源分配失衡
工具边界推理引擎是否支持 prefill/decode 解耦、视觉编码器是否独立某些功能并不支持,需要换方案

这个表格不是完整清单,但可以帮你建立一个顺序:先确认输入变化,再查环境,最后才怀疑调度器。以我的经验,大部分性能问题来自输入和参数,少部分是环境变动,真正需要重写调度器的情况很少。

5.3 验证恢复的方式

修改任何一项后,都要返回去看三个指标是否回到正常范围:GPU 利用率、请求 P95 延迟、显存峰值。最好保留一个小流量测试环境,每次只改一个变量。恢复验证的重点不是“不报错了”,而是“同样的请求耗时和资源占用是否稳定”。如果只是偶发变好,还要继续观察。

6. 适用边界:ParVL 这类思路不是所有场景的银弹

讨论到这里,必须把边界说清楚。再好的调度思路,放到不匹配的场景里,也只会变成额外负担。

6.1 适合什么场景

ParVL 所代表的思路,适合那些“模态混合明显、负载波动大、资源利用率低”的多模态服务。比如会议纪要生成工具,输入既有音频又有文档,还有用户对话文本;或者图像批量审核系统,视频和图片的比例不固定。当不同模态请求对算力要求差异很大时,可扩展计算分配才能体现价值。

6.2 不适合什么场景

如果业务场景非常稳定,比如后端只接收固定分辨率的图片识别请求,每个请求的计算负载都差不多,那么固定 batch 并行就足够。动态分配反而会引入队列和调度开销。另外,如果对延迟极其敏感,比如实时交互助手,调度器每层判断都会增加毫秒级开销,这时更应该把资源预留做大,而不是动态伸缩。

还有一个边界:如果推理引擎本身不支持连续批处理或 prefill/decode 解耦,那 ParVL 这类方案实施成本会非常高,不如先改造引擎。不要在一个不兼容的环境里硬套动态调度,最后只会得到一套难以维护的自研系统。

6.3 长期使用还需要补哪些能力

要长期稳定使用可扩展计算分配,至少还需要三块工程化能力:监控与日志、回滚与灰度、容量规划。监控要能区分每个并行阶段的资源利用率;日志要能追溯到每次调度的决策原因;容量规划要根据不同模态比例提前预留资源池,而不是每次都靠实时扩容。没有这些,方案可能只在实验环境表现得不错,进入生产后就会被各种偶发问题消耗。

回到最开始的问题:多模态大模型的并行扩展,为什么加了卡还是不理想?因为并行扩展只是把工作拆开了,没有回答谁在什么时候该获得多少资源。ParVL 这类方案真正有价值的地方,不在于某个具体的调度算法,而在于它让团队重新思考计算分配这件事:把请求按模态、按阶段拆开,再让资源跟随负载伸缩。你不需要一步到位,可以先用一个最小负载估计和两个资源池跑起来,至少先确认瓶颈在哪个阶段。跑通之后,再把队列、监控和灰度补上,慢慢变成一套能持续优化的系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询