【Bug已解决】[Bug]: Pixtral model(Migstral-Small-2509) will raise AttributeError NoneType Size on graph m

📅 2026/7/30 19:16:13
【Bug已解决】[Bug]: Pixtral model(Migstral-Small-2509) will raise AttributeError NoneType Size on graph m
【Bug已解决】[Bug] Pixtral model(Migstral-Small-2509) will raise AttributeError NoneType Size on graph mode. Eager mode is ok 解决方案一、现象长什么样用Pixtral具体是Migstral-Small-2509一个多模态模型时开 graph 模式CUDA graph 捕获 /torch.compile图模式加载崩溃AttributeError: NoneType object has no attribute size有时日志里被简写成AttributeError NoneType Size——意思就是代码对None调了.size()。崩溃发生在graph 捕获阶段不是实际推理时File pixtral/vision_encoder.py, in forward num_images pixel_values.size(0) AttributeError: NoneType object has no attribute size几个特征eager 模式关 graph完全正常只有 graph 模式炸。崩在「捕获 CUDA 图」那一步还没真正处理用户图片。模型是多模态带视觉编码器纯文本模型不会踩。报错一定落在「访问图像相关张量的.size()/ 形状」那一行且对象为None。本质graph 模式捕获时用的是「无图占位输入」dummy input只有文本、没有图片于是视觉编码器拿到的pixel_values/image_sizes是None而模型代码没对None做保护直接.size(0)于是 AttributeError。eager 模式每次都是真实请求、有图所以不炸。二、背景要理解这个得知道 CUDA graph 捕获怎么用 dummy input。CUDA graph 为了「录」下整段前向需要先用一批虚拟输入dummy跑一遍把 kernel 调用序列录下来。vLLM 在捕获时为文本模型准备的 dummy 输入是「带 token 但没有图像」的——因为图捕获关心的是「计算图结构」不关心真实数据。对纯文本模型dummy 里所有张量都有形状捕获顺利。但 Pixtral 这种多模态模型前向里有个视觉编码器分支它要读pixel_values图片像素张量来提取图像特征再和文本特征拼起来。问题在于捕获用的 dummy 输入没包含图片所以pixel_values是None。模型代码的视觉分支直接pixel_values.size(0)拿图片数量没判断None。graph 捕获阶段这一行就炸了None没有.size。为什么 eager 模式没事因为 eager 模式每个请求都是真实的用户发图文请求时pixel_values必然有值即使发纯文本请求eager 路径通常会走「没有图就跳过视觉分支」的逻辑至少在运行时上下文里有判断。但 graph 捕获路径为了「录图」强制走完整前向包括视觉分支而 dummy 输入又没图于是撞上None.size()。一句话graph 捕获用无图 dummy 输入触发了模型视觉分支对None张量调.size()而 eager 模式有真实图所以不触发。三、根因根因是模型视觉分支假设「图像输入一定存在」在 graph 捕获无图 dummy场景下拿到None后未做保护就访问.size()三层第一层主因视觉分支对pixel_values判空缺失。代码写的是n pixel_values.size(0)没有if pixel_values is None:的提前返回或占位。当 graph 捕获传入None立刻 AttributeError。正确的多模态前向应该「无图就跳过视觉编码、用零维占位」。第二层graph 捕获的 dummy 输入没给「空图占位」。vLLM 的 dummy input 构造器对多模态模型应该提供一个「0 张图」的合法占位比如pixel_values形状(0, 3, H, W)的空张量而不是None。但 Pixtral 的 dummy 构造没这么做直接留None把问题甩给了模型代码。第三层graph 模式无法「运行时跳过分支」。eager 模式可以「这次没图就不进视觉分支」动态控制流但 CUDA graph 捕获的是静态图——如果捕获时进了视觉分支之后所有请求都得进哪怕没图如果捕获时没图、跳过分支之后有图的请求又走不通。graph 模式要求「捕获时的控制流 推理时的控制流」。于是「无图就跳过」在 graph 模式下行不通必须改成「无图就用零维占位、始终走同一分支」才能既捕获成功、又推理正确。一句话视觉分支对 None 未保护 dummy 输入没给空图占位 graph 要求控制流静态三者叠加导致 graph 捕获崩在None.size()。四、最小可运行复现下面用纯 Python 模拟「视觉分支对 None 调 .size() 在 graph 捕获无图 dummy时崩、eager有图正常」的控制流不需要 GPUclass FakeTensor: def __init__(self, n): self.n n def size(self, dim): return self.n def vision_branch_buggy(pixel_values): # 模型视觉分支假设 pixel_values 一定有值 n pixel_values.size(0) # None 上没有 size - AttributeError return fencoded {n} images def eager_mode(real_images): # eager真实请求有图 return vision_branch_buggy(real_images) def graph_capture_mode(): # graph 捕获用无图 dummypixel_values None dummy None return vision_branch_buggy(dummy) def main(): # eager 正常 try: print(eager:, eager_mode(FakeTensor(2))) except Exception as e: print(eager 异常:, e) # graph 捕获崩 try: graph_capture_mode() except AttributeError as e: print(graph 捕获复现成功:, e) if __name__ __main__: main()跑出来 eager 打印encoded 2 imagesgraph 捕获打印graph 捕获复现成功: NoneType object has no attribute size和线上「eager OK、graph 崩」完全一致。五、解决方案第一层最小直接修复最省事的救火关掉 graph 模式退回 eager避开捕获阶段的 None 访问llm LLM( modelMigstral-Small-2509, enforce_eagerTrue, # 关 CUDA grapheager 模式有真实图、不触发 None.size() )或者如果你必须用 graph 模式临时做法是给 dummy 输入补一个空图占位让pixel_values不是None而是「0 张图的合法空张量」# 构造 graph 捕获用的 dummy 输入时给视觉分支一个零维占位 dummy_pixel_values torch.empty(0, 3, 336, 336) # 0 张图但形状合法 dummy_image_sizes torch.empty(0, 2)这样视觉分支dummy_pixel_values.size(0)返回0不崩且语义正确0 张图 → 不编码任何图像特征。六、解决方案第二层结构性改进第一层是「避开或补占位」第二层是「让模型视觉分支对 None / 空图都安全且控制流在 graph 模式下保持静态」从设计上消灭问题import torch def vision_branch_safe(pixel_values, image_sizes): 视觉分支对 None / 空图都安全且控制流静态始终进同一分支。 # 1) None - 用零张图占位避免 AttributeError if pixel_values is None: pixel_values torch.empty(0, 3, 336, 336) if image_sizes is None: image_sizes torch.empty(0, 2) n pixel_values.size(0) # 2) 0 张图返回零维图像特征占位控制流不变仍走这分支 if n 0: hidden pixel_values.new_zeros(0, 4096) # 与有图时同维度 return hidden # 3) 有图正常编码 features encode_images(pixel_values, image_sizes) return features def encode_images(pixel_values, image_sizes): # 真实视觉编码器前向省略 return pixel_values.new_zeros(pixel_values.size(0), 4096)配套dummy 输入构造器统一提供空图占位而不是 None保证 graph 捕获与实际推理走完全相同的控制流def build_dummy_multimodal_input(has_image: bool): if has_image: return { pixel_values: torch.randn(1, 3, 336, 336), image_sizes: torch.tensor([[336, 336]]), } # 关键无图也给空占位绝不给 None return { pixel_values: torch.empty(0, 3, 336, 336), image_sizes: torch.empty(0, 2), }这样 graph 捕获dummy 无图和实际图文请求有图都进vision_branch_safe的同一个分支控制流一致且都不会因None崩溃。七、解决方案第三层断言 / CI 守护把「视觉分支对 None 安全」「无图返回零维」「dummy 给空占位」固化成测试import torch import pytest def test_vision_branch_none_safe(): out vision_branch_safe(None, None) assert out.shape (0, 4096) # None 不崩返回零维 def test_vision_branch_empty_placeholder(): pv torch.empty(0, 3, 336, 336) out vision_branch_safe(pv, torch.empty(0, 2)) assert out.shape[0] 0 def test_vision_branch_real_images(): pv torch.randn(2, 3, 336, 336) out vision_branch_safe(pv, torch.tensor([[336, 336], [336, 336]])) assert out.shape (2, 4096) def test_dummy_no_image_not_none(): d build_dummy_multimodal_input(has_imageFalse) assert d[pixel_values] is not None assert d[pixel_values].shape (0, 3, 336, 336) def test_graph_capture_control_flow_static(): # 捕获无图与推理有图必须进同一分支、都不崩 cap vision_branch_safe(*map(lambda d: d[1], [(k, v) for k, v in build_dummy_multimodal_input(False).items()].__reversed__())) real vision_branch_safe(torch.randn(1, 3, 336, 336), torch.tensor([[336, 336]])) assert cap.shape[0] 0 and real.shape[0] 1再加一个端到端回归graph 模式加载 Pixtral 纯文本请求不崩def test_pixtral_graph_mode_text_only(): engine make_engine(modelMigstral-Small-2509, enforce_eagerFalse) out engine.generate(请描述这张图, imagesNone) # 捕获用空占位 assert out is not None八、排查清单看报错是不是NoneType object has no attribute size或简写NoneType Size且崩在 graph 捕获阶段 → 坐实本问题。关enforce_eagerTrue试能跑则说明是 graph 捕获的 None 问题。看栈是否落在视觉编码器 / 多模态 projector 的.size()/ 形状访问行。临时救火关 graph 模式或给 dummy 输入补空图占位0 张图的合法空张量。长期修复视觉分支对 None/空图做保护并返回零维占位dummy 构造器永远给空占位而非 None。升级模型实现到合了多模态 graph 安全修复的版本并跑上面的「None 安全」用例。注意 graph 模式要求控制流静态别用「无图就 return」的动态分支要用「无图就用零维占位、始终同分支」。九、小结Pixtral 在 graph 模式报NoneType Size即None.size()不是模型能力问题而是graph 捕获用无图 dummy 输入触发了视觉分支对None图像张量调.size()而 eager 模式有真实图所以不触发。最小修复是关 graph 模式或给 dummy 补空图占位结构性修复是视觉分支对 None/空图做保护并始终走同一静态分支无图返回零维占位、dummy 构造器永远给空占位最后用 pytest 把「None 安全」「无图零维」「控制流静态」锁死。抓住「graph 模式要求捕获与推理控制流一致、且 dummy 输入不能为 None」这条所有多模态模型在 graph 模式下的崩溃都能照此排查。