你负责一个多模态大模型服务的线上部署时通常会怎么做把请求切成 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 是 ParallelVL 指向多模态视觉语言场景。整套思路强调的不是某一个具体实现而是扩展和分配的组合策略。先想清楚并行扩展再想清楚计算分配最后把两者合并成一个调度问题。2.1 并行扩展的本质是拆分计算单元并行不是简单地让多个请求同时跑而是把推理过程拆成多个可以并发执行的计算单元。数据并行容易理解把不同请求分到不同设备张量并行是把一个大算子切分成多个小算子放到不同 GPU序列并行是把长序列切到多个设备流水线并行是把不同阶段分到不同设备。对多模态 LLM 来说更常见的是组合使用。一个请求从输入到输出可能存在多个计算阶段。你可以让视觉编码在 A 组 GPU 上执行让视觉语言融合在 B 组 GPU 上执行让自回归解码在 C 组 GPU 上执行。这其实就是一种并行扩展。但如果 A、B、C 之间的资源是固定比例那么请求模态一变资源就会失衡。于是需要第二个能力。2.2 可扩展计算分配不是简单动态 batchExpandable 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 这类方案真正有价值的地方不在于某个具体的调度算法而在于它让团队重新思考计算分配这件事把请求按模态、按阶段拆开再让资源跟随负载伸缩。你不需要一步到位可以先用一个最小负载估计和两个资源池跑起来至少先确认瓶颈在哪个阶段。跑通之后再把队列、监控和灰度补上慢慢变成一套能持续优化的系统。