|
28 | 28 | PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView, |
29 | 29 | PropagateSlice, |
30 | 30 | SinkOpsCloserToUsePass, |
| 31 | + SplitDequantizedCatPass, |
31 | 32 | ) |
32 | 33 | from executorch.backends.test.graph_builder import GraphBuilder |
33 | 34 | from executorch.exir.dialects._ops import ops as exir_ops |
@@ -1024,3 +1025,246 @@ def test_no_swap_binary_same_shape(self) -> None: |
1024 | 1025 | result = PropagateSlice().call(gm) |
1025 | 1026 |
|
1026 | 1027 | self.assertFalse(result.modified) |
| 1028 | + |
| 1029 | + |
| 1030 | +class TestSplitDequantizedCat(unittest.TestCase): |
| 1031 | + def test_no_dequant_input_noop(self) -> None: |
| 1032 | + """Cat with only float (non-dequant) inputs should not be split.""" |
| 1033 | + builder = GraphBuilder() |
| 1034 | + a = builder.placeholder("a", torch.randn(2, 4)) |
| 1035 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1036 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([a, b], 0)) |
| 1037 | + q = builder.call_operator( |
| 1038 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1039 | + args=(cat, 0.01, 0, -128, 127, torch.int8), |
| 1040 | + ) |
| 1041 | + dq = builder.call_operator( |
| 1042 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1043 | + args=(q, 0.01, 0, -128, 127, torch.int8), |
| 1044 | + ) |
| 1045 | + builder.output([dq]) |
| 1046 | + gm = builder.get_graph_module() |
| 1047 | + |
| 1048 | + result = SplitDequantizedCatPass().call(gm) |
| 1049 | + |
| 1050 | + self.assertFalse(result.modified) |
| 1051 | + self.assertEqual(count_node(gm, exir_ops.edge.aten.cat.default), 1) |
| 1052 | + |
| 1053 | + def test_no_quant_output_noop(self) -> None: |
| 1054 | + """Cat with a dequant input but no quant consumer should not be split.""" |
| 1055 | + builder = GraphBuilder() |
| 1056 | + x_int8 = builder.placeholder( |
| 1057 | + "x_int8", torch.randint(-128, 127, (2, 4), dtype=torch.int8) |
| 1058 | + ) |
| 1059 | + dq = builder.call_operator( |
| 1060 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1061 | + args=(x_int8, 0.01, 0, -128, 127, torch.int8), |
| 1062 | + ) |
| 1063 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1064 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([dq, b], 0)) |
| 1065 | + builder.output([cat]) |
| 1066 | + gm = builder.get_graph_module() |
| 1067 | + |
| 1068 | + result = SplitDequantizedCatPass().call(gm) |
| 1069 | + |
| 1070 | + self.assertFalse(result.modified) |
| 1071 | + self.assertEqual(count_node(gm, exir_ops.edge.aten.cat.default), 1) |
| 1072 | + |
| 1073 | + def test_one_dequant_input_one_quant_output(self) -> None: |
| 1074 | + """Cat with one dequant input and one quant consumer should be split.""" |
| 1075 | + builder = GraphBuilder() |
| 1076 | + x_int8 = builder.placeholder( |
| 1077 | + "x_int8", torch.randint(-128, 127, (2, 4), dtype=torch.int8) |
| 1078 | + ) |
| 1079 | + dq = builder.call_operator( |
| 1080 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1081 | + args=(x_int8, 0.01, 0, -128, 127, torch.int8), |
| 1082 | + ) |
| 1083 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1084 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([dq, b], 0)) |
| 1085 | + sliced = builder.call_operator( |
| 1086 | + exir_ops.edge.aten.slice_copy.Tensor, args=(cat, 0, 0, 2) |
| 1087 | + ) |
| 1088 | + q = builder.call_operator( |
| 1089 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1090 | + args=(cat, 0.02, -5, -128, 127, torch.int8), |
| 1091 | + ) |
| 1092 | + q_dq = builder.call_operator( |
| 1093 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1094 | + args=(q, 0.02, -5, -128, 127, torch.int8), |
| 1095 | + ) |
| 1096 | + builder.output([sliced, q_dq]) |
| 1097 | + gm = builder.get_graph_module() |
| 1098 | + |
| 1099 | + result = SplitDequantizedCatPass().call(gm) |
| 1100 | + |
| 1101 | + self.assertTrue(result.modified) |
| 1102 | + converted = result.graph_module |
| 1103 | + self.assertEqual(count_node(converted, exir_ops.edge.aten.cat.default), 2) |
| 1104 | + |
| 1105 | + # The slice should still be on the original cat, which has no quant consumers. |
| 1106 | + slice_nodes = converted.graph.find_nodes( |
| 1107 | + op="call_function", target=exir_ops.edge.aten.slice_copy.Tensor |
| 1108 | + ) |
| 1109 | + for node in slice_nodes: |
| 1110 | + cat_input = node.args[0] |
| 1111 | + self.assertEqual(cat_input.target, exir_ops.edge.aten.cat.default) |
| 1112 | + quant_users = [ |
| 1113 | + u |
| 1114 | + for u in cat_input.users |
| 1115 | + if u.target |
| 1116 | + == exir_ops.edge.quantized_decomposed.quantize_per_tensor.default |
| 1117 | + ] |
| 1118 | + self.assertEqual(len(quant_users), 0) |
| 1119 | + |
| 1120 | + def test_non_quant_consumers_stay_on_original_cat(self) -> None: |
| 1121 | + """All non-quant consumers should remain on the original cat.""" |
| 1122 | + builder = GraphBuilder() |
| 1123 | + x_int8 = builder.placeholder( |
| 1124 | + "x_int8", torch.randint(-128, 127, (2, 4), dtype=torch.int8) |
| 1125 | + ) |
| 1126 | + dq = builder.call_operator( |
| 1127 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1128 | + args=(x_int8, 0.01, 0, -128, 127, torch.int8), |
| 1129 | + ) |
| 1130 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1131 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([dq, b], 0)) |
| 1132 | + sliced = builder.call_operator( |
| 1133 | + exir_ops.edge.aten.slice_copy.Tensor, args=(cat, 0, 0, 2) |
| 1134 | + ) |
| 1135 | + abs_val = builder.call_operator(exir_ops.edge.aten.abs.default, args=(cat,)) |
| 1136 | + q = builder.call_operator( |
| 1137 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1138 | + args=(cat, 0.02, -5, -128, 127, torch.int8), |
| 1139 | + ) |
| 1140 | + q_dq = builder.call_operator( |
| 1141 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1142 | + args=(q, 0.02, -5, -128, 127, torch.int8), |
| 1143 | + ) |
| 1144 | + builder.output([sliced, abs_val, q_dq]) |
| 1145 | + gm = builder.get_graph_module() |
| 1146 | + |
| 1147 | + result = SplitDequantizedCatPass().call(gm) |
| 1148 | + |
| 1149 | + self.assertTrue(result.modified) |
| 1150 | + converted = result.graph_module |
| 1151 | + self.assertEqual(count_node(converted, exir_ops.edge.aten.cat.default), 2) |
| 1152 | + |
| 1153 | + # Both non-quant consumers (slice and abs) should use the same cat. |
| 1154 | + slice_nodes = converted.graph.find_nodes( |
| 1155 | + op="call_function", target=exir_ops.edge.aten.slice_copy.Tensor |
| 1156 | + ) |
| 1157 | + abs_nodes = converted.graph.find_nodes( |
| 1158 | + op="call_function", target=exir_ops.edge.aten.abs.default |
| 1159 | + ) |
| 1160 | + self.assertEqual(len(slice_nodes), 1) |
| 1161 | + self.assertEqual(len(abs_nodes), 1) |
| 1162 | + self.assertIs(slice_nodes[0].args[0], abs_nodes[0].args[0]) |
| 1163 | + |
| 1164 | + # That shared cat should have no quant consumers. |
| 1165 | + original_cat = slice_nodes[0].args[0] |
| 1166 | + quant_users = [ |
| 1167 | + u |
| 1168 | + for u in original_cat.users |
| 1169 | + if u.target |
| 1170 | + == exir_ops.edge.quantized_decomposed.quantize_per_tensor.default |
| 1171 | + ] |
| 1172 | + self.assertEqual(len(quant_users), 0) |
| 1173 | + |
| 1174 | + def test_two_quant_outputs_same_params_shared_cat(self) -> None: |
| 1175 | + """Two quant consumers with identical params should share one duplicate cat.""" |
| 1176 | + builder = GraphBuilder() |
| 1177 | + x_int8 = builder.placeholder( |
| 1178 | + "x_int8", torch.randint(-128, 127, (2, 4), dtype=torch.int8) |
| 1179 | + ) |
| 1180 | + dq = builder.call_operator( |
| 1181 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1182 | + args=(x_int8, 0.01, 0, -128, 127, torch.int8), |
| 1183 | + ) |
| 1184 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1185 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([dq, b], 0)) |
| 1186 | + sliced = builder.call_operator( |
| 1187 | + exir_ops.edge.aten.slice_copy.Tensor, args=(cat, 0, 0, 2) |
| 1188 | + ) |
| 1189 | + q1 = builder.call_operator( |
| 1190 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1191 | + args=(cat, 0.02, -5, -128, 127, torch.int8), |
| 1192 | + ) |
| 1193 | + q2 = builder.call_operator( |
| 1194 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1195 | + args=(cat, 0.02, -5, -128, 127, torch.int8), |
| 1196 | + ) |
| 1197 | + dq1 = builder.call_operator( |
| 1198 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1199 | + args=(q1, 0.02, -5, -128, 127, torch.int8), |
| 1200 | + ) |
| 1201 | + dq2 = builder.call_operator( |
| 1202 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1203 | + args=(q2, 0.02, -5, -128, 127, torch.int8), |
| 1204 | + ) |
| 1205 | + builder.output([sliced, dq1, dq2]) |
| 1206 | + gm = builder.get_graph_module() |
| 1207 | + |
| 1208 | + result = SplitDequantizedCatPass().call(gm) |
| 1209 | + |
| 1210 | + self.assertTrue(result.modified) |
| 1211 | + converted = result.graph_module |
| 1212 | + # Original cat + one shared duplicate = 2 cats total |
| 1213 | + self.assertEqual(count_node(converted, exir_ops.edge.aten.cat.default), 2) |
| 1214 | + |
| 1215 | + # Both quant nodes should share the same cat input (the duplicate). |
| 1216 | + quant_nodes = converted.graph.find_nodes( |
| 1217 | + op="call_function", |
| 1218 | + target=exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1219 | + ) |
| 1220 | + quant_cat_inputs = {node.args[0] for node in quant_nodes} |
| 1221 | + self.assertEqual(len(quant_cat_inputs), 1) |
| 1222 | + |
| 1223 | + def test_two_quant_outputs_different_params_separate_cats(self) -> None: |
| 1224 | + """Two quant consumers with different params should get separate duplicate cats.""" |
| 1225 | + builder = GraphBuilder() |
| 1226 | + x_int8 = builder.placeholder( |
| 1227 | + "x_int8", torch.randint(-128, 127, (2, 4), dtype=torch.int8) |
| 1228 | + ) |
| 1229 | + dq = builder.call_operator( |
| 1230 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1231 | + args=(x_int8, 0.01, 0, -128, 127, torch.int8), |
| 1232 | + ) |
| 1233 | + b = builder.placeholder("b", torch.randn(2, 4)) |
| 1234 | + cat = builder.call_operator(exir_ops.edge.aten.cat.default, args=([dq, b], 0)) |
| 1235 | + sliced = builder.call_operator( |
| 1236 | + exir_ops.edge.aten.slice_copy.Tensor, args=(cat, 0, 0, 2) |
| 1237 | + ) |
| 1238 | + q1 = builder.call_operator( |
| 1239 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1240 | + args=(cat, 0.02, -5, -128, 127, torch.int8), |
| 1241 | + ) |
| 1242 | + q2 = builder.call_operator( |
| 1243 | + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1244 | + args=(cat, 0.03, 10, -128, 127, torch.int8), |
| 1245 | + ) |
| 1246 | + dq1 = builder.call_operator( |
| 1247 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1248 | + args=(q1, 0.02, -5, -128, 127, torch.int8), |
| 1249 | + ) |
| 1250 | + dq2 = builder.call_operator( |
| 1251 | + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, |
| 1252 | + args=(q2, 0.03, 10, -128, 127, torch.int8), |
| 1253 | + ) |
| 1254 | + builder.output([sliced, dq1, dq2]) |
| 1255 | + gm = builder.get_graph_module() |
| 1256 | + |
| 1257 | + result = SplitDequantizedCatPass().call(gm) |
| 1258 | + |
| 1259 | + self.assertTrue(result.modified) |
| 1260 | + converted = result.graph_module |
| 1261 | + # Original cat + two separate duplicates = 3 cats total |
| 1262 | + self.assertEqual(count_node(converted, exir_ops.edge.aten.cat.default), 3) |
| 1263 | + |
| 1264 | + # Each quant node should have a different cat input. |
| 1265 | + quant_nodes = converted.graph.find_nodes( |
| 1266 | + op="call_function", |
| 1267 | + target=exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, |
| 1268 | + ) |
| 1269 | + quant_cat_inputs = {node.args[0] for node in quant_nodes} |
| 1270 | + self.assertEqual(len(quant_cat_inputs), 2) |
0 commit comments