@@ -310,6 +310,126 @@ def test_qs8_conv2d_per_channel(self) -> None:
310310 quant_config = get_symmetric_quantization_config (is_per_channel = True ),
311311 )
312312
313+ def test_qs8_conv2d_even_kernel_same_padding (self ) -> None :
314+ # An even-kernel 'same'-padding conv decomposes into
315+ # dequant -> constant_pad_nd -> convolution. The pad and conv are pulled
316+ # into one partition and both delegate (the pad as a quantized
317+ # XNNStaticConstantPad), so neither survives in the top-level graph.
318+ for has_bias in (True , False ):
319+ m = Conv2d (
320+ in_channels = 2 ,
321+ out_channels = 4 ,
322+ kernel_size = (4 , 4 ),
323+ stride = (1 , 1 ),
324+ padding = "same" ,
325+ bias = has_bias ,
326+ )
327+ tester = Tester (m .eval (), m .get_inputs ())
328+ tester .quantize (
329+ Quantize (quantization_config = get_symmetric_quantization_config ())
330+ )
331+ (
332+ tester .export ()
333+ .check_count ({"torch.ops.aten.conv2d" : 1 })
334+ .to_edge_transform_and_lower ()
335+ .check_not (
336+ [
337+ "executorch_exir_dialects_edge__ops_aten_convolution_default" ,
338+ "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default" ,
339+ ]
340+ )
341+ .check_count ({"torch.ops.higher_order.executorch_call_delegate" : 1 })
342+ .to_executorch ()
343+ .serialize ()
344+ .run_method_and_compare_outputs (qtol = 2 )
345+ )
346+
347+ def test_qs8_conv2d_depthwise_even_kernel_same_padding (self ) -> None :
348+ # Depthwise is a standard (non-transposed) 2D conv, so its even-'same' pad
349+ # is delegated alongside the conv too. Assert it still fully delegates and is
350+ # numerically correct.
351+ m = Conv2d (
352+ in_channels = 4 ,
353+ out_channels = 4 ,
354+ groups = 4 ,
355+ kernel_size = (4 , 4 ),
356+ stride = (1 , 1 ),
357+ padding = "same" ,
358+ )
359+ tester = Tester (m .eval (), m .get_inputs ())
360+ tester .quantize (
361+ Quantize (quantization_config = get_symmetric_quantization_config ())
362+ )
363+ (
364+ tester .export ()
365+ .check_count ({"torch.ops.aten.conv2d" : 1 })
366+ .to_edge_transform_and_lower ()
367+ .check_not (
368+ [
369+ "executorch_exir_dialects_edge__ops_aten_convolution_default" ,
370+ "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default" ,
371+ ]
372+ )
373+ .check_count ({"torch.ops.higher_order.executorch_call_delegate" : 1 })
374+ .to_executorch ()
375+ .serialize ()
376+ .run_method_and_compare_outputs (qtol = 2 )
377+ )
378+
379+ class EvenSamePadFlatten (torch .nn .Module ):
380+ # Reproduces the failure where an even-kernel 'same' conv is followed by a
381+ # shape-dependent op. The flatten bakes a fixed view over the conv's padded
382+ # output extent; if the pad's spatial contribution is dropped from the graph
383+ # the conv output shrinks and the view becomes invalid.
384+ def __init__ (self ):
385+ super ().__init__ ()
386+ self .conv = torch .nn .Conv2d (1 , 16 , (2 , 1 ), padding = "same" )
387+
388+ def forward (self , x ):
389+ out = torch .reshape (x , (1 , 1 , 256 , 1 ))
390+ out = self .conv (out )
391+ return torch .flatten (out , 1 )
392+
393+ def get_inputs (self ):
394+ return (torch .randn (1 , 1 , 256 , 1 ),)
395+
396+ def test_fp32_even_kernel_same_padding_flatten (self ) -> None :
397+ m = self .EvenSamePadFlatten ().eval ()
398+ (
399+ Tester (m , m .get_inputs ())
400+ .export ()
401+ .to_edge_transform_and_lower ()
402+ .check_not (
403+ [
404+ "executorch_exir_dialects_edge__ops_aten_convolution_default" ,
405+ "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default" ,
406+ ]
407+ )
408+ .check_count ({"torch.ops.higher_order.executorch_call_delegate" : 1 })
409+ .to_executorch ()
410+ .serialize ()
411+ .run_method_and_compare_outputs ()
412+ )
413+
414+ def test_qs8_even_kernel_same_padding_flatten (self ) -> None :
415+ m = self .EvenSamePadFlatten ().eval ()
416+ (
417+ Tester (m , m .get_inputs ())
418+ .quantize (Quantize (quantization_config = get_symmetric_quantization_config ()))
419+ .export ()
420+ .to_edge_transform_and_lower ()
421+ .check_not (
422+ [
423+ "executorch_exir_dialects_edge__ops_aten_convolution_default" ,
424+ "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default" ,
425+ ]
426+ )
427+ .check_count ({"torch.ops.higher_order.executorch_call_delegate" : 1 })
428+ .to_executorch ()
429+ .serialize ()
430+ .run_method_and_compare_outputs (qtol = 2 )
431+ )
432+
313433 def test_fp32_conv2d_seq (self ) -> None :
314434 for transpose in (True , False ):
315435 self ._test (Conv2dSeq (transpose = transpose ), conv_count = 2 )
0 commit comments