【Bug已解决】Single-scale image input support for SAM3 Mask Decoder 解决方案
一、现象长什么样
SAM3 的 Mask Decoder 在官方示例里总是配合"多尺度图像特征"(image encoder 输出的多层特征金字塔)。但当你只想用单尺度图像特征(比如一个已经提好的单层特征图,或想省显存不走多尺度)喂给 Mask Decoder 时,会报这类错:
# 现象 A:特征层数不够,索引越界 IndexError: tuple index out of range File ".../models/sam3/mask_decoder.py", line 88, in forward high_res = image_features[-1] # 期望多尺度,但只给了 1 层 # 现象 B:形状对不上 RuntimeError: The size of tensor a (64) must match the size of tensor b (256) # 单尺度特征被当成某一层去和另一层做逐元素操作,通道数不一致 # 现象 C:字典 vs 列表约定混乱 TypeError: list indices must be integers or slices, not str # 代码里有时按 list 索引(image_features[i]),有时按 dict(image_features["low"])最典型的触发代码:你直接拿 backbone 的最后一层特征(single-scale)传给mask_decoder(image_features=feat, ...),而不是传[feat1, feat2, feat3]这种多尺度列表。
二、背景
SAM 系列(含 SAM3)的 Mask Decoder 设计上是消费多尺度图像嵌入的:image encoder 通常输出若干个不同 stride 的特征图(例如 1/4、1/8、1/16、1/32),Mask Decoder 内部用neck/fpn把高低层特征融合,再做 mask 预测。代码多处直接假设image_features是"长度>=2 的序列",并用image_features[-1]、按固定下标取层。
问题在于:很多实际场景只需要单尺度。比如:
- 下游只关心最终分辨率 mask,不需要高层语义融合;
- 用轻量 backbone 只产出单层特征;
- 做 ablation 想验证单尺度是否够用。
此时 Mask Decoder 没有"单尺度兜底路径",于是上面的索引/形状/类型错误就出现了。
三、根因
根因有三类:
硬性下标假设多尺度。
mask_decoder里写死了low_res = image_features[0]、high_res = image_features[-1],并对不同层做通道对齐。当image_features只有 1 个元素,要么索引越界([-1]其实 OK,但image_features[1]之类越界),要么两个"不同层"其实是同一个张量、通道数相同却被当成需要融合的不同层 → 形状断言失败。缺少
num_scales自适应。 Mask Decoder 的neck(上采样/卷积融合)按固定层数构造,没有根据输入实际层数动态调整。单尺度输入时,应当跳过融合、直接把单层特征送进 mask 预测头。list / dict 约定不统一。 部分实现把多尺度存成 dict(
{"low":..., "high":...}),另一部分按 list 索引。单尺度输入时用户不知道该传[feat]还是{"single": feat},类型错配直接TypeError。
四、最小可运行复现
下面用纯 Python 模拟"Mask Decoder 按固定下标取多尺度、单尺度输入越界/形状错"的逻辑:
from dataclasses import dataclass from typing import List @dataclass class FakeFeat: channels: int scale: int # 下采样倍率,如 4/8/16/32 def mask_decoder_forward_multi_scale(image_features: List[FakeFeat]): # 真实代码假设至少两层,并融合 low(最小 scale) 与 high(最大 scale) low = image_features[0] # scale 最小的(如 4) high = image_features[-1] # scale 最大的(如 32) if low.channels != high.channels: # 需要 1x1 对齐通道 return f"fusion {low.channels}->{high.channels}" return "same channel, concat" # 多尺度(正常) multi = [FakeFeat(256, 4), FakeFeat(256, 8), FakeFeat(256, 16), FakeFeat(256, 32)] print("多尺度:", mask_decoder_forward_multi_scale(multi)) # 单尺度(复现问题) single = [FakeFeat(256, 16)] try: # 这里虽然 image_features[-1] 不越界,但真实逻辑常取 image_features[1] 做中层 mid = single[1] # IndexError print(mid) except IndexError as e: print("复现成功(越界):", e) # 复现形状:单层被当成两层融合,通道不匹配时误判 mixed = [FakeFeat(64, 16), FakeFeat(256, 32)] # 用户把单尺度拆成两份但通道不同 print("单尺度误用:", mask_decoder_forward_multi_scale(mixed))运行后,对单尺度列表取single[1]会IndexError;而把单层复制成两份但通道不同,会触发"通道对齐"逻辑被错误激活,正是现象 A/B 的来源。
五、解决方案(第一层:最小直接修复)
最快的止血:在调用 Mask Decoder 前,把单尺度特征包装成它期望的多尺度结构(复制/上采样成若干层),或在 decoder 入口加一个"单尺度兜底"分支:
import torch import torch.nn.functional as F def prepare_image_features_for_sam3(image_features, expected_scales=4): """第一层修复:把单尺度特征适配成 Mask Decoder 期望的多尺度列表。""" if isinstance(image_features, torch.Tensor): # 只有一个张量 -> 复制成 expected_scales 份(单尺度降级方案) return [image_features for _ in range(expected_scales)] if isinstance(image_features, (list, tuple)): if len(image_features) == 1: return [image_features[0] for _ in range(expected_scales)] return list(image_features) if isinstance(image_features, dict): # dict 约定:按 low/high 取,单尺度时两键指向同一张量 if "single" in image_features: t = image_features["single"] return [t for _ in range(expected_scales)] return [image_features["low"], image_features.get("high", image_features["low"])] raise TypeError(f"不支持的 image_features 类型: {type(image_features)}") # 使用 single_feat = torch.randn(1, 256, 64, 64) # 单尺度 multi = prepare_image_features_for_sam3(single_feat, expected_scales=4) masks = mask_decoder(image_features=multi, sparse_prompt_embeddings=..., dense_prompt_embeddings=...)第一层让用户立刻能用单尺度特征跑通 Mask Decoder,无需改动模型权重。
六、解决方案(第二层:结构性改进)
更彻底的做法是让 Mask Decoder 自身支持num_scales自适应,用MaskDecoderFeatureAdapter在 forward 入口统一归一化输入:
from dataclasses import dataclass from typing import List, Union import torch @dataclass class MaskDecoderFeatureAdapter: """把任意尺度的 image_features 归一化为 decoder 内部统一格式。""" min_scales: int = 1 upsample_single: bool = True def normalize(self, image_features): # 统一成 list[Tensor] if isinstance(image_features, torch.Tensor): feats = [image_features] elif isinstance(image_features, dict): feats = [image_features[k] for k in sorted(image_features.keys())] else: feats = list(image_features) if len(feats) < 2: # 单尺度:通过 1x1 卷积生成一份"高层"特征,避免融合越界 only = feats[0] if self.upsample_single: high = F.interpolate(only, scale_factor=0.5, mode="nearest") # 通道对齐 if high.shape[1] != only.shape[1]: conv = torch.nn.Conv2d(high.shape[1], only.shape[1], 1) high = conv(high) feats = [only, high] return feats def forward_decoder(self, decoder, feats, *args, **kwargs): # decoder 内部按 list 索引,现在 feats 至少 2 层,安全 return decoder(feats, *args, **kwargs) # 使用 adapter = MaskDecoderFeatureAdapter() feats = adapter.normalize(single_feat) # 单尺度 -> [low, high] 至少两层 out = adapter.forward_decoder(mask_decoder, feats, sparse_prompt_embeddings, dense_prompt_embeddings)MaskDecoderFeatureAdapter的语义是:无论输入是单尺度、多尺度、还是 dict,都归一成 decoder 内部安全的list[Tensor](至少 2 层),从而根治索引越界与形状错。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 固化"单尺度输入必须被接受且输出形状正确":
import pytest import torch def test_single_scale_tensor_accepted(): from adapter import MaskDecoderFeatureAdapter adapter = MaskDecoderFeatureAdapter() feat = torch.randn(1, 256, 64, 64) out = adapter.normalize(feat) assert isinstance(out, list) and len(out) >= 2, \ "单尺度张量必须被归一化为至少 2 层,避免 decoder 索引越界" # 两层通道应一致(融合不会形状错) assert out[0].shape[1] == out[1].shape[1] def test_dict_single_scale_accepted(): from adapter import MaskDecoderFeatureAdapter adapter = MaskDecoderFeatureAdapter() feat = torch.randn(1, 256, 64, 64) out = adapter.normalize({"single": feat}) assert len(out) >= 2 def test_multi_scale_passthrough(): from adapter import MaskDecoderFeatureAdapter adapter = MaskDecoderFeatureAdapter() feats = [torch.randn(1, 256, 256, 256), torch.randn(1, 256, 64, 64)] out = adapter.normalize(feats) assert len(out) == 2 # 多尺度不应被改动CI 跑pytest tests/test_sam3_mask_decoder.py,以后只要有人又把 decoder 写死成"只认多尺度 list",测试立刻红灯。
八、排查清单
当 SAM3 Mask Decoder 喂单尺度特征报错,按顺序查:
- 报错是
IndexError→ decoder 按固定下标取层,单尺度层数不够,用prepare_image_features_for_sam3包装。 - 报错是形状不匹配(通道/分辨率)→ 单层被当成两层融合,需要通道对齐或生成高层特征。
- 报错是
TypeError: list indices ... not str→ list/dict 约定混乱,统一成 list 输入。 - 显存允许时,直接用官方多尺度输出最稳;若必须单尺度,确保 adapter 生成至少 2 层且通道一致。
- 改 decoder 内部时,永远不要写死
image_features[1],改成"按实际长度自适应"。
九、小结
"Single-scale image input support for SAM3 Mask Decoder" 的根因是:Mask Decoder 写死了多尺度假设(固定下标、固定层数融合、list/dict 约定不统一),单尺度输入就索引越界或形状错。
- 第一层:调用前用
prepare_image_features_for_sam3把单尺度张量包装成期望的多尺度列表,立即跑通。 - 第二层:用
MaskDecoderFeatureAdapter让 decoder 入口自适应任意尺度,单尺度自动生成高层特征,根治越界与形状错。 - 第三层:pytest 断言"单尺度张量/字典都被接受、输出至少 2 层且通道一致、多尺度原样透传",防止回归。
记住:视觉 decoder 不要写死特征层数;输入归一化层(adapter)应当把任意尺度统一成内部安全格式,而不是让调用方去猜约定。