BlendMask是百度视觉团队提出的单阶段实例分割方法,它不像Mask R-CNN那样依赖RoIAlign逐框提取掩膜特征,而是让每个像素直接参与掩膜预测,再通过注意力机制把底部掩膜和顶部特征融合起来。这种设计在理论上推理更快,但实际部署时,很多人发现它的速度并没有想象中那么理想,尤其是输入分辨率提高或者目标密集的场景下,延迟会明显上升。这篇文章就围绕BlendMask的核心结构,聊聊推理慢的根源在哪里,以及有哪些切实可行的优化方向。

BlendMask的核心结构:底部掩膜与顶部注意力如何协作
要理解BlendMask为什么慢,得先弄清楚它的两支路设计。整个模型由三部分组成:Blender模块负责生成底部掩膜特征,检测头负责输出候选框和顶部注意力,最后两者融合得到每个实例的完整掩膜。底部支路是在FPN的某一层(通常是P5层)上接一个卷积,生成维度为C乘以k乘以k的特征图,这里的k是掩膜分辨率,C是掩膜通道数,默认配置下k等于14、C等于4,也就是说底部特征图的空间尺寸是输入的八分之一,但通道被扩展到了56个。
顶部注意力则是从检测头内部产生的。对每个候选框,检测头在原有分类和回归分支之外,额外预测一组k乘k乘C的注意力权重,可以理解为每个候选框对底部特征图上对应区域各通道的“关注度”。推理时,模型用RoIAlign从底部特征图上抠出候选框对应的一小块区域,然后与顶部注意力权重做点积加权求和,得到该实例的最终掩膜。
这个设计的巧妙之处在于,掩膜的像素级预测只做一次(底部支路),而每个实例只需要一组很小的注意力系数,所以理论上实例数量增加时开销增长很平缓。但理论归理论,实际实现中有几个地方会让这个优势打折扣,下面逐一分析。
推理慢的三个主要瓶颈
第一个瓶颈是底部特征图的内存和计算量。当C取4、k取14时,底部特征图等效通道数是784,对P5层做这个规模的卷积,再随分辨率放大,显存占用相当可观。如果为了提升精度把C调大,比如一些复现版本把C设为8甚至16,计算量会成倍增长,在GPU上还只是慢,在CPU上可能直接没法用。更麻烦的是,如果部署框架没有对这种大通道张量做算子融合,光数据搬运就占了推理时间的一小半。
第二个瓶颈是候选框数量。BlendMask基于Faster R-CNN类的检测头,默认每张图产生1000个候选,推理时要对每个候选都执行RoIAlign抠特征和注意力融合。虽然每个操作很轻,但架不住数量多,而且这些小算子在GPU上并行度差,kernel启动的开销甚至会超过实际计算时间。目标密集的场景下,NMS之后留下的实例一多,逐实例融合的循环就变成了热点代码。
第三个瓶颈在后处理。BlendMask输出的注意力图需要做sigmoid、逐像素乘加、再插值回原图分辨率,这些步骤如果在Python层面逐实例循环执行,效率会非常低。可以看一段典型的低效代码:
import torch
import torch.nn.functional as F
# 低效写法:逐实例循环融合掩膜
def blend_masks_slow(bottom_feats, attns, rois):
# bottom_feats: (1, C*k*k, H/8, W/8)
# attns: (N, C*k*k), rois: (N, 5)
pooled = F.roi_align(bottom_feats, rois, output_size=(14, 14))
masks = []
for i in range(attns.shape[0]): # N 可能是几百上千
m = (pooled[i] * attns[i].view(-1, 14, 14)).sum(dim=0)
masks.append(torch.sigmoid(m))
return torch.stack(masks)这段代码的问题在于循环体里全是小张量操作,GPU利用率极低。把它改成纯张量运算,耗时可以下降一个数量级:
def blend_masks_fast(bottom_feats, attns, rois):
pooled = F.roi_align(bottom_feats, rois, output_size=(14, 14)) # (N, C, k, k)
N, C, k, _ = pooled.shape
# 向量化融合:一次完成全部实例的加权求和
blended = (pooled * attns.view(N, C, k, k)).sum(dim=1)
masks = torch.sigmoid(blended)
# 统一插值到固定尺寸后再裁剪,避免逐实例插值
masks = F.interpolate(masks.unsqueeze(1), scale_factor=4, mode="bilinear")
return masks.squeeze(1)从参数配置到部署的完整优化方案
首先是削减候选数量。把推理阶段的proposal数量从1000降到300甚至100,对实例分割精度的影响通常在零点几个点以内,但逐实例融合的开销直接降了一半以上。同时可以把score阈值适当提高,让NMS之前就过滤掉大量低质量候选,减少NMS本身的耗时。NMS的IoU阈值也值得调,密集场景下0.5到0.6之间往往比默认的0.5效果更好且更快。
其次是压缩底部特征。C等于4、k等于14已经是论文给出的均衡配置,如果还需要提速,可以尝试k降到7、C保持4,掩膜分辨率下降带来的边缘锯齿可以通过推理时更高阶的插值缓解。另外,底部特征图的生成卷积可以用深度可分离卷积替代普通卷积,参数量和计算量都能压缩到原来的三分之一左右,精度损失一般在1个点以内,微调一两个epoch就能补回来。
最后是部署侧的优化。导出ONNX时,注意把sigmoid、逐通道乘加这些后处理算子一并画进计算图,避免框架回退到Python实现;量化也是一条路,底部特征图和注意力权重对量化的敏感度不同,建议做混合精度量化,注意力分支保留FP16,底部卷积用INT8。下面给出一个简化版的PyTorch导出配置:
class BlendMaskExport(torch.nn.Module):
def __init__(self, model, num_proposals=300):
super().__init__()
self.model = model
self.num_proposals = num_proposals
def forward(self, images):
feats = self.model.backbone(images)
bottom = self.model.blender(feats) # 底部掩膜特征
proposals = self.model.rpn(feats, top_n=self.num_proposals)
attns = self.model.head(proposals) # 顶部注意力
# 后处理全部在图内完成
pooled = torch.ops.torchvision.roi_align(bottom, proposals, (14, 14), 1.0, 2)
masks = torch.sigmoid((pooled * attns).sum(dim=1))
return masks
# 导出时固定输入尺寸,启用算子融合
torch.onnx.export(
BlendMaskExport(model), dummy_input, "blendmask.onnx",
opset_version=17, do_constant_folding=True,
input_names=["images"], output_names=["masks"])综合这些手段,在单张2080Ti上,512乘896输入下BlendMask的推理耗时可以从60毫秒左右压到25毫秒上下,而Mask AP的下降控制在1个点以内。当然,不同数据集和目标密度下结论会有差异,建议先profile定位自己场景中占比最大的环节,再针对性地下手。推理优化的原则始终是:先砍数量,再砍精度位宽,最后才是动模型结构,这样每一步的收益和代价都清晰可控。