【Bug已解决】RTDetrLoss crashing with ValueError: cost matrix is infeasible 解决方案
一、现象长什么样
用 RT-DETR(实时 Detection Transformer)训练或推理时,损失计算阶段(具体是匈牙利匹配 / 代价矩阵构建)崩溃:
ValueError: cost matrix is infeasible这个报错来自匹配算法(通常是scipy.optimize.linear_sum_assignment或等价实现)在代价矩阵(cost matrix)上找不到可行匹配时抛出。最迷惑的是:它只在特定 batch 出现——大多数 batch 正常,偶尔几个 batch 炸,而且往往是"某个样本没有 GT 框"或"GT 框坐标异常"的那批。
本质:RT-DETR 在 matching 阶段要把num_queries个预测和num_gt个真实框做最优匹配,构建cost matrix(形状[num_queries, num_gt])。当num_gt == 0(空标注样本)、或 cost matrix 含inf/非法值(如 GT 框坐标越界、IoU 算出 nan)、或num_queries < num_gt时,线性分配无解,抛 "cost matrix is infeasible"。
二、背景
RT-DETR 的 loss 由两部分组成:
- 匹配代价(cost):用分类 logits 的代价 + 框的 L1/IoU 代价,构建
[Q, G]的 cost matrix(Q=num_queries,G=num_gt)。 - 匈牙利匹配:
linear_sum_assignment(cost_matrix)找最优一一对应,再用匹配结果算分类/框回归 loss。
linear_sum_assignment要求矩阵有效且可分配。以下会让它报 "infeasible":
num_gt == 0:cost matrix 是[Q, 0](0 列)。分配算法在"0 个任务"时没有可行解,某些实现直接报 infeasible 而非返回空匹配。- cost matrix 含
inf/nan:IoU 计算在退化框(面积为 0、坐标越界)时得到 inf/nan,污染矩阵,分配无解。 num_queries < num_gt:预测 queries 比 GT 少,无法一一覆盖(严格一对一匹配下无解)。- 矩阵全为 inf 行/列:某预测对所有 GT 代价都是 inf,无可行分配。
下面用可运行代码复现"cost matrix 含 inf 或 0 列导致分配 infeasible"。
三、根因
根因一句话:RT-DETR 匹配的 cost matrix 在num_gt==0、含inf/nan(退化框)、或num_queries<num_gt时,匈牙利分配无可行解,抛ValueError: cost matrix is infeasible。
三个具体失配:
- 空 GT(num_gt=0):cost matrix 0 列,分配无解。
- 退化框导致 inf/nan:IoU 在面积为 0/越界框上算出 inf,污染矩阵。
- queries < gt:预测数少于真实框,严格匹配无解。
四、最小可运行复现
用纯 Python 模拟"cost matrix 含 inf 或 0 列导致分配 infeasible"(用异常模拟linear_sum_assignment行为):
import math from dataclasses import dataclass from typing import List def hungarian(cost: List[List[float]]): """模拟分配:矩阵含 inf 或 0 列则 infeasible。""" if not cost or not cost[0]: raise ValueError("cost matrix is infeasible (0 列/空矩阵)") for row in cost: if all(math.isinf(v) for v in row): raise ValueError("cost matrix is infeasible (全 inf 行)") return "matched" def main(): # 情况1:num_gt=0 -> cost 0 列 try: hungarian([[0.1], [0.2], [0.3]]) # 正常 except ValueError as e: pass # 模拟 0 列(空 GT) try: hungarian([]) # 实际是 [Q, 0],这里用空表象征 except ValueError as e: print("复现到报错(空GT):", e) # 情况2:cost 含 inf(退化框) try: hungarian([[0.1, math.inf], [0.2, math.inf], [0.3, math.inf]]) except ValueError as e: print("复现到报错(inf):", e) if __name__ == "__main__": main()运行会打印复现到报错(空GT): cost matrix is infeasible和复现到报错(inf): ...——正是 RT-DETR 匹配崩溃的两种典型本质。
五、解决方案(第一层:最小直接修复)
最立竿见影的修复:在构建 cost matrix 前,处理好三类退化情况:(1) 空 GT 样本直接跳过匹配(该样本无检测 loss,或给零损失);(2) 过滤/裁剪退化框,避免 IoU 算 inf/nan;(3) 用linear_sum_assignment时把 cost 的 inf 替换成极大值,并允许num_queries >= num_gt的 padding。
import math import torch def safe_cost_matrix(pred_boxes, gt_boxes): """修复:构建 cost matrix 前处理退化情况。""" if gt_boxes.numel() == 0: # 空 GT:返回空匹配,该样本 loss 置 0 return None # 过滤退化 GT(面积<=0 或越界) valid = (gt_boxes[..., 2:] > gt_boxes[..., :2]).all(dim=-1) gt_boxes = gt_boxes[valid] if gt_boxes.numel() == 0: return None # 计算 IoU 代价,替换 inf/nan 为极大惩罚 iou = iou_cost(pred_boxes, gt_boxes) iou = torch.where(torch.isfinite(iou), iou, torch.full_like(iou, 1e6)) return iou def iou_cost(a, b): # 示意:返回 [Q, G] 代价 return torch.rand(a.shape[0], b.shape[0]) def main(): pred = torch.rand(10, 4) empty_gt = torch.zeros(0, 4) cm = safe_cost_matrix(pred, empty_gt) print("空 GT 样本:cost=None,跳过匹配,loss 置 0") if __name__ == "__main__": main()第一层修复让空 GT / 退化框不再触发 infeasible,匹配稳定。
六、解决方案(第二层:结构性改进)
把"匹配前的退化处理"收口成一个MatcherGuard,统一检查空 GT、过滤退化框、把 inf 替换成惩罚值,并保证num_queries >= num_gt(不足则 pad GT 到 queries 数),避免出现 infeasible。
import torch from dataclasses import dataclass from typing import Optional @dataclass class MatcherGuard: num_queries: int = 10 inf_penalty: float = 1e6 def prepare(self, pred_boxes, gt_boxes) -> Optional[torch.Tensor]: if gt_boxes.numel() == 0: return None # 空 GT,跳过 valid = (gt_boxes[:, 2:] > gt_boxes[:, :2]).all(dim=-1) gt_boxes = gt_boxes[valid] if gt_boxes.shape[0] == 0: return None # pad GT 到 num_queries,保证一对一匹配可行 g = gt_boxes.shape[0] if g < self.num_queries: pad = torch.zeros(self.num_queries - g, 4) gt_boxes = torch.cat([gt_boxes, pad], dim=0) cost = torch.rand(self.num_queries, self.num_queries) # 示意 cost = torch.where(torch.isfinite(cost), cost, torch.full_like(cost, self.inf_penalty)) return cost def main(): guard = MatcherGuard(num_queries=10) pred = torch.rand(10, 4) gt = torch.tensor([[10, 10, 20, 20], [5, 5, 3, 3]]) # 第二个退化 cost = guard.prepare(pred, gt) print("匹配 guard 处理后 cost 形状:", None if cost is None else tuple(cost.shape)) if __name__ == "__main__": main()第二层的关键是MatcherGuard把"空 GT→跳过、退化框→过滤、inf→惩罚、GT 数<queries→pad"全部固化,匹配前不可能再出现 infeasible。
七、解决方案(第三层:断言 / CI 守护)
加 pytest 守护:(1) 空 GT 时prepare返回 None(跳过,不崩);(2) cost matrix 不含 inf/nan;(3) GT 数不足时被 pad 到 num_queries。
import torch import pytest class MatcherGuard: def __init__(self, num_queries): self.num_queries = num_queries def prepare(self, gt): if gt.numel() == 0: return None valid = (gt[:, 2:] > gt[:, :2]).all(dim=-1) gt = gt[valid] if gt.shape[0] == 0: return None if gt.shape[0] < self.num_queries: gt = torch.cat([gt, torch.zeros(self.num_queries - gt.shape[0], 4)]) return gt def test_empty_gt_returns_none(): g = MatcherGuard(10) assert g.prepare(torch.zeros(0, 4)) is None def test_degenerate_filtered(): g = MatcherGuard(10) gt = torch.tensor([[10, 10, 20, 20], [5, 5, 3, 3]]) # 第二个退化 out = g.prepare(gt) assert out.shape[0] == 10 # 过滤退化 + pad 到 10 def test_no_inf(): cost = torch.tensor([[0.1, float("inf")]]) cost = torch.where(torch.isfinite(cost), cost, torch.full_like(cost, 1e6)) assert torch.isfinite(cost).all() if __name__ == "__main__": pytest.main([__file__, "-q"])CI 里test_empty_gt_returns_none+test_degenerate_filtered通过,就能保证 RT-DETR 匹配不再因空 GT/退化框崩溃,杜绝 "cost matrix is infeasible" 回归。
八、排查清单
RT-DETR loss 报 "cost matrix is infeasible" 时,按此顺序查:
- 确认是不是特定 batch:若偶发,多半是某样本空 GT 或退化框。
- 检查空 GT 样本:数据里有没有标注为 0 框的样本,cost matrix 变 0 列。
- 检查退化框:GT 框坐标是否
[x1,y1,x2,y2]且x2>x1, y2>y1,面积为 0 或越界会算 inf。 - 第一层修复:空 GT 跳过匹配(loss 置 0),退化框过滤,inf 替换惩罚值。
- 检查 num_queries vs num_gt:确保 queries 足够,必要时 pad GT。
- 用 MatcherGuard 兜底:匹配前统一处理四类退化。
- 加数值回归:固定样本跑匹配,确认不再 infeasible。
九、小结
RT-DETR loss 报 "cost matrix is infeasible",根因不在模型结构,而在匹配阶段的 cost matrix 在num_gt==0(空标注样本)、含inf/nan(退化/越界 GT 框算 IoU 得到)、或num_queries < num_gt时,匈牙利分配无可行解而抛错。它偶发、只在特定 batch 出现,最易误判为随机崩溃。
修复三层:第一层,匹配前处理空 GT(跳过/loss 0)、过滤退化框、inf 替换惩罚值;第二层用MatcherGuard把"空 GT→跳过、退化→过滤、inf→惩罚、GT 不足→pad"固化;第三层用 pytest 断言"空 GT 返回 None、退化被过滤、无 inf"。记住:RT-DETR 匹配前先清场——空 GT 跳过、退化框过滤、inf 换惩罚;cost matrix 干净,分配才不会 infeasible。