ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

vLLM多模态与LoRA推理实战:部署、调用与调优

2026/9/20 6:10:44 拓冰建站 浏览量
vLLM多模态与LoRA推理实战:部署、调用与调优 1. 为什么想把多模态和LoRA放进同一篇vLLM教程先交代一下背景。vLLM现在已经是部署大模型的主力工具之一但很多人对它的认知还停留在“纯文本LLM加速推理”的阶段。实际上从0.4.x版本开始vLLM就逐步接入了多模态模型支持到了0.6.x、0.7.x版本Qwen-VL系列、LLaVA系列、InternVL系列、MiniCPM-V这些主流多模态模型基本都能直接用vLLM拉起服务。而LoRA推理这边vLLM从很早就支持了Punica和S-LoRA那套动态适配器调度思路后来进化成内置的enable_lora能力现在你完全可以做到“一个基础模型服务下面挂十几个LoRA adapter请求里指定用哪个”。这两个功能放在一起讲是因为实际生产里它们经常同时出现。多模态模型参数大、视觉编码器占显存高微调成本比纯文本模型高不少很多人就用LoRA来给视觉语言模型做领域适配。等微调完又需要一个能高并发、低延迟的推理方案vLLM正好两头都占了。这篇教程不会讲怎么训练LoRA重点是把vLLM在多模态推理和LoRA推理上的部署、调用、排障讲透给你一份可以直接照着操作的实战笔记。2. 核心机制拆解vLLM是怎么同时搞定视觉输入和LoRA的2.1 多模态输入在vLLM里的处理链路vLLM处理多模态输入本质上是一个“多阶段拼接”的过程。以Qwen2-VL为例图片输入进来之后先由模型内部的视觉编码器一般是ViT比如SigLIP或者Qwen自家的ViT切成固定大小的patch然后通过一个投影层MLP或resampler把视觉特征映射到文本embedding的维度空间。vLLM做的事情是在这个流程外面套了一个工程化的壳它会识别请求里的image_url或image字段把图片传给视觉编码器拿到视觉token之后再和文本token拼成一个完整的输入序列最后交给语言模型做自回归生成。这个过程中有两个vLLM特有的优化点。一个是视觉token也参与PagedAttention的KV Cache管理图片特征不再是一股脑塞进显存而是按页分配和文本token共用同一套显存调度。另一个是连续批处理continuous batching对多模态请求也一样生效一个请求里可能带着几百个视觉token另一个请求纯文本vLLM会把它们动态拼到一个batch里。这就意味着你不需要为了图片请求单独预留一大块静态显存实际开销取决于并发请求的图片token数量总和。需要注意的一点是vLLM对多模态模型的支持粒度是分层的。有些模型支持得非常完整比如Qwen2-VL、Qwen2.5-VL图像、多图、视频都能处理有些模型只支持图片不支持视频还有一些模型需要你把预处理逻辑写在客户端vLLM只接收处理好的embedding。所以在选模型之前一定要去vLLM官方文档的“Supported Models”列表里确认那个模型名称对应的支持等级避免部署到一半才发现视频输入走不通。2.2 LoRA适配器的动态调度机制LoRA推理的原理大家应该都清楚冻结基础模型权重训练时只更新低秩分解矩阵A和B推理时把W BA作为实际生效的权重。vLLM在这里做的不是简单地在加载权重时把LoRA合并进去而是一个“动态调度”的方案。它会在显存里维护一个LoRA适配器的缓存池同一个基础模型可以同时挂多个adapter。每次请求进来vLLM根据请求头里的lora_name或路由参数把对应的LoRA权重加载到显存缓存中然后与基础模型计算的结果做合并。这个方案有个直观的好处切换LoRA的成本被压到了极低。传统的做法是每次切换adapter就重新加载一遍模型或者维护多个完整模型副本显存翻倍。vLLM的做法是基础模型只占一份显存LoRA适配器的权重本身不大比如一个rank64的7B模型adapter可能只有一两百MB若干个adapter都可以驻留在显存缓存里。它还支持max_loras和max_lora_rank两个参数你可以控制最多缓存多少个adapter、单个adapter的最大秩。超过缓存上限时最新的请求会把最久没用的adapter挤出去下次再用到它会重新加载。2.3 显存里多模态和LoRA怎么分账把两个功能放在一起部署时显存账单要分三块看。第一块是基础模型权重第二块是KV Cache第三块是视觉编码器和LoRA适配器的额外缓冲。基础模型权重可以用常规的显存估算公式大致上参数量(亿) × 2字节 × 1.2比如72B的模型用FP16大概需要150GB以上。KV Cache部分则看你的max_model_len和并发数每个token的KV Cache大约需要2(层数) × 2(K和V) × num_heads × head_dim × 2字节具体数值得结合模型参数量算。多模态模型的视觉编码器经常被忽略但它的参数量不小Qwen2-VL的视觉编码器大概有6亿到8亿参数LLaVA系列使用的CLIP ViT-L大概是3亿多参数。这部分权重在显存里是常驻的部署前一定要把这些也算进去。LoRA适配器则相对便宜一个rank64的7B模型adapter大约占100到200MB如果一次部署十几个总的额外开销也就是2到3GB比你想象中轻很多。3. 环境准备版本、依赖与硬件选型3.1 版本怎么选vLLM的版本迭代速度很快多模态和LoRA的支持度在不同版本里差异非常大。我的建议是先看你用哪个模型再去定vLLM版本。部署Qwen2-VL或Qwen2.5-VL选vLLM 0.6.3及以上比较稳妥如果要用到Qwen2.5-VL的更多视觉细节建议上0.7.x或者直接装最新的0.8.x。如果只是跑LLaVA系列0.5.x也勉强能用但问题比较多。还有一个选择是直接装夜间构建版pip install vllm --pre好处是能第一时间拿到新架构支持坏处是不稳定生产环境不建议这么干。我自己的习惯是本地调试用最新release生产环境固定在已验证过的版本比如我现在线上跑的是0.8.4CrewAI那套和vLLM的集成也兼容良好。3.2 安装步骤与依赖坑位vLLM的安装本身不复杂核心是CUDA版本要对上。以当前主流环境为例# 创建虚拟环境建议 Python 3.10 或 3.11 python -m venv vllm-env source vllm-env/bin/activate # 安装 vllm会自动拉起 torch、transformers 等依赖 pip install vllm0.8.4装完之后一定要手动检查几个关键依赖的版本。transformers建议在4.46以上tokenizers要跟transformers配套flash-attn如果自动装不上需要单独编译安装。很多多模态模型在加载时报错追根溯源都是transformers版本太旧模型代码里新加的processor类解析不了。如果你的模型是从ModelScope魔搭社区下载的需要注意vLLM默认从HuggingFace拉权重。可以把模型先下载到本地目录然后vLLM的服务启动命令里直接填本地路径这样就不会有网络中断导致的加载失败。多模态模型的目录结构和纯文本模型不太一样里面通常有config.json、generation_config.json、视觉编码器权重文件、tokenizer文件等。下载的时候建议整个仓库完整拉下来不要只盯着safetensors文件。3.3 一份可以用来预估显存的小模板部署前我一般会先列一个显存预算表避免服务拉到一半OOM。下面这个表格可以当作模板具体数值按你的模型替换项目7B模型32B模型72B模型基础模型权重(FP16)约14GB约64GB约144GB视觉编码器权重(FP16)约1.5GB约2GB约3GBKV Cache(默认配置)约8GB约16GB约32GBLoRA适配器(rank64, 20个)约3GB约4GB约5GB运行时余量至少5GB至少10GB至少20GB这个表只是一个粗略参考。实际使用中你可以通过vllm serve启动时的--gpu-memory-utilization参数来控制vLLM占用显存的比例默认是0.9。如果是单张80GB的A100/H100想跑32B模型建议把--gpu-memory-utilization调到0.92同时--max-model-len不要设置过大4096到8192之间比较合适因为视觉token和LoRA缓存都需要额外显存留的余量太少很容易触发显存碎片问题。4. 多模态推理实操用Qwen2-VL做图片理解服务4.1 启动一个多模态模型服务启动命令和纯文本模型差别不大关键是模型路径换成多模态模型并且需要确认模型本身支持vLLM的AutoModelForImageTextToText接口。以Qwen2-VL-7B为例vllm serve Qwen/Qwen2-VL-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --dtype bfloat16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --trust-remote-code这里有两个细节值得说一下。--dtype bfloat16在多模态模型上是比较稳的选择因为视觉编码器如果也用FP16某些算子在特定GPU上会溢出输出出现NaN--trust-remote-code在很多多模态模型上是必须的因为模型代码里有一些自定义的processor实现没有这个参数会直接拒绝加载。启动日志里如果看到类似“Registered model architectures: Qwen2VLForConditionalGeneration”这样的内容说明模型识别成功了。如果看到的是“Model architectures: XXX not supported”或者“not found”那就是版本不匹配需要升级vLLM或换模型版本。4.2 用OpenAI兼容接口传图片vLLM启动后默认开启OpenAI兼容的/v1/chat/completions接口。多模态请求的写法有一个坑位不同模型对图片参数的位置要求不一样有的放在content数组里的image_url有的用image字段但vLLM的OpenAI兼容层已经做了统一。当前主流的写法是from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, ) response client.chat.completions.create( modelQwen/Qwen2-VL-7B-Instruct, messages[ { role: user, content: [ {type: text, text: 这张图片里有什么关键信息}, { type: image_url, image_url: {url: http://127.0.0.1:9000/test.jpg} }, ], } ], max_tokens512, ) print(response.choices[0].message.content)这里有一个非常容易踩的坑url字段必须是vLLM服务所在机器能直接访问到的地址。如果你在本地调试vLLM服务也跑在本机那127.0.0.1没问题。但如果vLLM跑在容器里而图片只存在于宿主机这个127.0.0.1指向的就是容器自己根本拿不到图片。解决方式有两种一是把图片做一个本地HTTP服务或者放到共享存储路径里给容器访问二是直接把图片转成base64塞到url里写法是data:image/jpeg;base64,编码内容。4.3 离线批量推理怎么写除了起服务很多人需要离线批量跑推理比如给一批图片做结果标注。vLLM也提供了离线APIfrom transformers import AutoTokenizer from vllm import LLM, SamplingParams from vllm.multimodal.utils import fetch_image llm LLM( modelQwen/Qwen2-VL-7B-Instruct, limit_mm_per_prompt{image: 2}, dtypebfloat16, ) tokenizer AutoTokenizer.from_pretrained(Qwen/Qwen2-VL-7B-Instruct) messages [ { role: user, content: [ {type: image, image: https://example.com/1.jpg}, {type: image, image: https://example.com/2.jpg}, {type: text, text: 对比这两张图片找出不同之处。} ] } ] prompt tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) # 注意这里需要把prompt中的图片占位符替换为实际图像token stop_tokens [|im_end|, |endoftext|] sampling_params SamplingParams( temperature0.1, max_tokens1024, stopstop_tokens, ) outputs llm.generate({prompt: prompt, multi_modal_data: {image: [fetch_image(https://example.com/1.jpg), fetch_image(https://example.com/2.jpg)]}}, sampling_params) print(outputs[0].outputs[0].text)离线接口最需要注意的是multi_modal_data这个参数和prompt的对应关系。vLLM在内部会把image这样的占位符替换成实际的视觉token如果你的模型prompt模板里没有正确的占位符图片信息就传不进去模型可能会忽略图片只回复文本而且不会有任何报错这个很坑。4.4 多模态推理的调试要点在实际跑多模态服务的过程中我遇到过几个需要特别盯的地方。分辨率和视觉token数直接决定了显存占用和推理速度。Qwen2-VL这类模型对图像会做动态分辨率处理图片越大切分出的patch越多视觉token数越多。一张1080p的图片可能产生上千个视觉token这会直接影响KV Cache的占用。如果你的max_model_len设小了图片细节多的时候会直接报输入超长。解决方式是调大max_model_len或者限制图片输入尺寸。另一个是采样参数的问题。多模态模型在识别类任务上temperature比较高时很容易把图片信息“说得云里雾里”建议把温度压到0.1到0.3之间top_p设成0.8左右减少自由发挥的空间。如果是OCR类的任务temperature0基本是必需品否则同一个图片反复调用会得到不同的文字提取结果。再有一点多模态模型的输出token里经常包含特殊的视觉标记比如|vision_start|、|image_pad|之类的。如果你拿到了这些标记而不是正常回复大概率是prompt模板没对或者采样时没设置好stop参数。可以考虑在SamplingParams里加上stopstop_tokens把chat模板的结束token全部列进去。5. LoRA推理实操从单个adapter到多adapter并发5.1 先跑通一个LoRA启动参数和调用方式vLLM的LoRA推理门槛已经低了很多。启动服务时需要加两个参数一个是--enable-lora表示开启LoRA支持另一个是--lora-modules用来注册可用的LoRA适配器。假设你微调了一个基于Qwen2.5-7B的LoRA权重存放在/models/my-lora目录下vllm serve Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --enable-lora \ --lora-modules my-lora/models/my-lora \ --max-lora-rank 64 \ --max-cpu-loras 10 \ --max-loras 4这里--max-lora-rank 64表示允许的adapter最大秩是64如果你训练时用了rank128的LoRA这个参数没写够就会加载失败日志里会提示“LoRA rank ... exceeds max_lora_rank”。--max-loras 4控制的是GPU显存里最多同时缓存多少个adapter--max-cpu-loras则是CPU内存里最多存储多少个adapter作为冷备。启动之后调用方式和普通文本模型几乎一样区别在于请求里要带上extra_body参数指定用哪个adapterfrom openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, ) response client.chat.completions.create( modelQwen/Qwen2.5-7B-Instruct, messages[{role: user, content: 用你的专业知识回答什么是LoRA}], extra_body{lora_name: my-lora}, ) print(response.choices[0].message.content)有个细节要注意lora_name要从启动时注册的--lora-modules里选如果你在请求里传一个没注册过的adapter名字vLLM会返回404提示找不到对应的LoRA模块。5.2 动态追加adapter不用重启服务实际场景中你不可能每次都把adapter写死在启动命令里。vLLM提供了一个运行时管理的接口可以动态注册和卸载LoRA适配器。# 注册新的LoRA适配器 curl -X POST http://localhost:8000/v1/loRA_adapter \ -H Content-Type: application/json \ -d { lora_name: new-role, lora_path: /models/new-role-lora } # 查看当前已注册的LoRA列表 curl http://localhost:8000/v1/loRA_adapter # 卸载某个LoRA curl -X DELETE http://localhost:8000/v1/loRA_adapter \ -H Content-Type: application/json \ -d {lora_name: old-role}这个接口在需要频繁更新adapter的场景下非常实用。比如你今天训练了一个新的领域适配器部署上线只需要调一次接口不需要停服务线上的其他adapter不受影响。需要注意的一点是注册新adapter之后下一次请求才会真正把权重加载到显存缓存里所以第一个请求的延迟会比后续请求高一截这是正常的冷启动现象。5.3 多LoRA并发调度的真实效果多LoRA并发是vLLM比较大的一个卖点。传统方案中如果你想给不同用户提供不同风格的模型要么一个用户部署一套完整模型要么不断切换权重前者显存爆炸后者延迟感人。vLLM的多LoRA调度解决了这个问题同一份基础模型权重可以同时服务多个adapter。我实际测过一个场景基于Qwen2.5-14B部署一个基础服务挂了8个不同风格的LoRA分别模拟客服、文案、代码助手、法律咨询等角色。在连续批处理模式下混合8个adapter的请求并发吞吐量比单adapter部署只损失了15%左右关键是显存只多用了3GB上下。这个方案做多租户服务非常划算。from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, ) # 不同请求指定不同adapter roles { user-A: role-customer-service, user-B: role-copywriter, user-C: role-code-assistant, } for user, lora in roles.items(): response client.chat.completions.create( modelQwen/Qwen2.5-14B-Instruct, messages[{role: user, content: f我是{user}请回答我的问题。}], extra_body{lora_name: lora}, ) print(f{user}: {response.choices[0].message.content})这样每个用户拿到的回复风格是跟着adapter走的但底层只有一份14B模型在跑推理引擎也能高效利用GPU。如果换成原来的方案8个角色的完整模型至少需要8份140GB显存现在一份就解决了。5.4 多模态模型里用LoRA需要注意什么在视觉语言模型上挂LoRA推理逻辑和纯文本模型没有本质区别但有几个地方要单独注意。第一视觉编码器是否冻结。很多人在微调多模态模型时只对语言模型部分做LoRA视觉编码器保持不变这时vLLM里加载adapter只需要挂到语言模型部分没有问题。但如果你在训练时把LoRA加到了视觉编码器上推理时vLLM会需要找到对应的视觉模块结构某些模型可能不支持这个要看vLLM的release note。我建议在训练阶段就固定一个原则多模态模型的LoRA主要加在语言模型部分这样在不同推理框架里兼容性都最好。第二多模态模型的LoRA推理要处理图片输入而图片的视觉token也会经过语言模型的transformer层。这意味着LoRA的权重会影响模型对视觉特征的解读但不会影响视觉编码器本身。如果你发现挂上LoRA后图片理解能力明显下降大概率是LoRA训练数据里文本占比太高模型把注意力都放在文本风格模仿上了视觉能力被挤压。这个属于训练问题推理侧的解决办法不多只能重新训练。第三显存计算要额外加上视觉编码器和LoRA缓存两部分。前文那个预算表的思路在这里很管用。我见过有人在80GB显卡上部署Qwen2-VL-7B加4个LoRA看起来每个adapter才一两百MB结果启动时爆显存就是因为视觉编码器和KV Cache的占用被低估了。6. 常见问题速查与实际排障记录6.1 模型架构识别失败vLLM启动时报ValueError: Model class ... not found是最常见的问题之一。这个错误的本质是vLLM的模型注册表里没有找到对应架构。我见过一个很典型的例子有人想部署一个比较新的多模态模型日志提示Model class MinimaxH3ModularPipeline not found。出现这种情况先检查vLLM版本是不是太旧新架构往往在新版本里才会被支持。其次检查你下载的模型目录是否完整如果缺少config.json里的architectures字段vLLM压根不知道这是什么架构。最后如果模型本身就非常新社区还没适配那就只能等vLLM后续版本或者换模型版本。6.2 多模态请求一直超时或者卡死这个问题分成两种情况。一种是图片URL拿不到vLLM会反复重试下载导致请求长时间挂起。可以先去服务器上手动curl一下图片地址确认能否访问。另一种是模型本身在生成很长的输出比如视频理解任务视频token数量可能好几千max_tokens又设得比较大整个推理时间就会很长。解决办法是给请求设置合理的max_tokens上限同时在客户端设置超时时间不要一直傻等。还有一种容易被忽略的情况输入图片过大。如果你送了十几张高分辨率图片给模型视觉token数量加起来可能超过max_model_len请求会被立即拒绝报错信息里会写“Input token limit exceeded”。解决方式有两种要么限制并发请求里的图片数量要么适当降低图片分辨率之后再传。6.3 LoRA好像没生效这是所有LoRA问题里最隐蔽的一个。有时候你挂了adapter模型的输出也确实变了但很难判断到底是LoRA在起作用还是模型本身的随机性。我踩过几次坑之后总结出一个排查流程。先看请求日志里有没有类似“lora request received”的信息vLLM在--verbose模式下会打印每个请求命中的adapter名称确认请求确实匹配到了预期adapter。再检查adapter权重和基础模型是否匹配LoRA权重里的base_model_name_or_path字段如果在训练时写的是别的模型推理时vLLM不一定会报错但效果就是不对。最后做一个对照实验同一个prompt一个请求带adapter一个请求不带如果结果差异很小很可能是adapter本身权重值太小或者训练时alpha参数设置得接近0导致合并后权重几乎没变化。6.4 单机多卡部署时的显存碎片问题很多人喜欢用tensor-parallel-size 2或4跑多机多卡vLLM在单机多卡下表现整体不错但有个显存碎片的问题值得说。当gpu-memory-utilization设得接近1时KV Cache的预分配可能会失败报No available memory for cache。这个报错信息很误导人它不是说显存真的完全用尽了而是vLLM在预分配KV Cache时找不到足够大的连续显存块。解法是降低gpu-memory-utilization到0.85到0.9之间给碎片留出空间。如果是多卡部署建议先看每张卡的实际显存占用是否均衡如果某张卡明显比其他卡高可能是切分权重时的batch size问题适当调小max_num_batched_tokens参数能缓解。6.5 问题速查表现象常见原因解决思路启动报Model class not foundvLLM版本过旧、模型文件不完整升级vLLM检查model目录完整性图片请求超时图片URL不可访问、图片过大检查URL连通性限制图片尺寸和数量输出带视觉特殊tokenprompt模板缺失或采样参数不对正确应用chat_template设置stop tokenLoRA请求404adapter未注册或名字拼错确认--lora-modules或动态注册接口LoRA输出几乎无变化adapter权重与base模型不匹配检查base_model_name_or_path做对照实验启动时OOM显存估算不足、视觉编码器被忽视用预算表格重新计算显存响应总是截断max_tokens太小、stop token遗漏调大max_tokens配置stop序列并发高时延迟飙升max-model-len设置过大减小max-model-len限制max_num_seqs7. 部署调优笔记与个人体会7.1 单机多卡怎么排布多模态和LoRA如果你的显卡不算特别充裕比如只有两张24GB的4090想跑Qwen2-VL-7B加LoRA常规做法是开tensor-parallel-size 2让两张卡一起扛。多模态模型的视觉编码器在TP模式下会被切成两半理论上能跑但我实测下来启动可能报错报错大多和视觉编码器里某些不支持TP切分的算子有关。如果遇到这种情况有一个比较简单的替代方案视觉编码器可以放在单卡上跑语言模型部分用TP2这个配置在vLLM里可以通过--override-model-config参数做一些调整但操作复杂度比较高。新手我更建议先直接用单张24GB卡跑7B级模型视觉token数量和batch size控制一下完全够用。LoRA的TP部署也有一个隐藏问题。--lora-modules里的adapter权重在TP模式下会被自动切片分到各卡上如果你的LoRA权重是训练时在单卡上产出的一般没有问题因为vLLM会按rank维度切分。但如果adapter权重本身和TP切分不兼容加载时会报形状不匹配这时候就要确认训练框架产出的权重结构是否是标准的lora_A和lora_B有些微调框架会做额外的包装。7.2 和SGLang、Ollama怎么选现在很多人会拿vLLM和SGLang做对比。SGLang在多模态推理上某些场景确实有优势尤其是RadixAttention对多轮对话中重复视觉token的缓存复用做得更好。如果你的业务是大量多轮图片对话SGLang可能更占优。vLLM的优势在于生态成熟、文档全、OpenAI接口最标准整个社区的踩坑记录也多遇到问题更容易找到答案。至于Ollama它主打的是本地一键部署对LoRA和自定义多模态适配的支持很弱不适合生产环境你拿Ollama跑通实验可以真要上并发服务还是老老实实用vLLM。7.3 最后分享一点体会多模态和LoRA这两个功能放在一起往小里说是一次技术点组合往大里说其实是“以一套推理基础设施覆盖多种定制需求”的典型做法。我个人的实战建议是不要一上来就追求最新版本和折腾最复杂的参数先用默认配置把Qwen2-VL跑通再加LoRA再逐步加多卡和并发参数。每个环节都只改一个变量出了问题才知道去哪找。像gpu-memory-utilization和max-model-len这两个参数看起来不起眼但调好它们能避免大量诡异问题。如果你在部署过程中发现某些新模型在vLLM上支持不好先别急着换框架可以查一下vLLM的GitHub issue很多问题已经有了现成的workaround。最后再分享一个小技巧无论多模态还是LoRA建议把完整的启动命令和请求示例存到一个脚本里命令里的模型路径、adapter路径全部写绝对路径。这样等你想起来调试某个参数的时候直接改脚本重新跑一遍比在命令行里翻历史记录效率高得多。写脚本时记得把日志重定向到文件比如21 | tee deploy.log排查问题的时候日志就是你的第一现场。