@@ -4066,6 +4066,165 @@ def forward(self, x: Tensor, index: Tensor, src: Tensor) -> Tensor:
40664066 )
40674067
40684068
4069+ class TestMaskedScatter :
4070+ """Tests for aten.masked_scatter.default → coreai flatten / cumsum-1 /
4071+ gather / where lowering.
4072+ """
4073+
4074+ class _Model (nn .Module ):
4075+ def forward (self , self_ : Tensor , mask : Tensor , src : Tensor ) -> Tensor :
4076+ return torch .masked_scatter (self_ , mask , src )
4077+
4078+ @staticmethod
4079+ def _shared_dynamic_shapes (self_ : Tensor , src : Tensor ) -> dict :
4080+ """Build dynamic_shapes where self_ and mask share dim objects
4081+ (torch.export emits an equality guard when traced shapes match)
4082+ and src has its own independent dim.
4083+ """
4084+ dims = {i : torch .export .Dim (f"d{ i } " , min = 1 ) for i in range (self_ .dim ())}
4085+ return {
4086+ "self_" : dims ,
4087+ "mask" : dims ,
4088+ "src" : {0 : torch .export .Dim ("s0" , min = 1 )}
4089+ if src .dim () == 1
4090+ else _all_dims_dynamic (src , "s" ),
4091+ }
4092+
4093+ @pytest .mark .ir
4094+ def test_lowers_ir_static (self ) -> None :
4095+ """Pin the full operand chain on a static shape."""
4096+ self_ = torch .zeros (2 , 3 )
4097+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4098+ src = torch .arange (1.0 , 7.0 )
4099+ program = torch .export .export (
4100+ self ._Model (), args = (self_ , mask , src )
4101+ ).run_decompositions ()
4102+ coreai_program = TorchConverter ().add_exported_program (program ).to_coreai ()
4103+ filecheck_pattern (
4104+ str (coreai_program ),
4105+ check_file = """
4106+ // CHECK-LABEL: coreai.graph @main
4107+ // CHECK-SAME: %arg0: tensor<2x3xf32>
4108+ // CHECK-SAME: %arg1: tensor<2x3xi1>
4109+ // CHECK-SAME: %arg2: tensor<6xf32>
4110+ // CHECK: %[[SF:.+]] = coreai.reshape %arg0, %{{.+}} : (tensor<2x3xf32>, tensor<1xsi32>) -> tensor<6xf32>
4111+ // CHECK: %[[MF:.+]] = coreai.reshape %arg1, %{{.+}} : (tensor<2x3xi1>, tensor<1xsi32>) -> tensor<6xi1>
4112+ // CHECK: %[[VF:.+]] = coreai.reshape %arg2, %{{.+}} : (tensor<6xf32>, tensor<1xsi32>) -> tensor<6xf32>
4113+ // CHECK: %[[MI:.+]] = coreai.cast %[[MF]] : tensor<6xi1> to tensor<6xsi32>
4114+ // CHECK: %[[CS:.+]] = coreai.scan %[[MI]], %{{.+}}, %{{.+}} combiner = <sum> : (tensor<6xsi32>, tensor<ui32>, tensor<i1>) -> tensor<6xsi32>
4115+ // CHECK: %[[IDX:.+]] = coreai.decomposable.broadcasting_sub %[[CS]], %{{.+}} : (tensor<6xsi32>, tensor<si32>) -> tensor<6xsi32>
4116+ // CHECK: %[[G:.+]] = coreai.gather_along_axis %[[VF]] at %[[IDX]] along %{{.+}} : (tensor<6xf32>, tensor<6xsi32>, tensor<si32>) to tensor<6xf32>
4117+ // CHECK: %[[W:.+]] = coreai.decomposable.broadcasting_where %[[MF]], %[[G]], %[[SF]] : (tensor<6xi1>, tensor<6xf32>, tensor<6xf32>) -> tensor<6xf32>
4118+ // CHECK: %[[OUT:.+]] = coreai.reshape %[[W]], %{{.+}} : (tensor<6xf32>, tensor<2xsi32>) -> tensor<2x3xf32>
4119+ // CHECK: coreai.output %[[OUT]]
4120+ """ ,
4121+ )
4122+
4123+ @pytest .mark .ir
4124+ def test_lowers_ir_dynamic (self ) -> None :
4125+ """Confirm the same op chain appears under dynamic shapes — shapes
4126+ become symbolic, but cast/scan/sub/gather/where/reshape order is
4127+ preserved.
4128+ """
4129+ self_ = torch .zeros (2 , 3 )
4130+ mask = torch .zeros (2 , 3 , dtype = torch .bool )
4131+ src = torch .arange (6.0 )
4132+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src )
4133+ program = torch .export .export (
4134+ self ._Model (), args = (self_ , mask , src ), dynamic_shapes = dynamic_shapes
4135+ ).run_decompositions ()
4136+ coreai_program = TorchConverter ().add_exported_program (program ).to_coreai ()
4137+ filecheck_pattern (
4138+ str (coreai_program ),
4139+ check_file = """
4140+ // CHECK-LABEL: coreai.graph @main
4141+ // CHECK: coreai.reshape
4142+ // CHECK: coreai.reshape
4143+ // CHECK: coreai.reshape
4144+ // CHECK: coreai.cast {{.+}} to tensor<{{.*}}xsi32>
4145+ // CHECK: coreai.scan {{.+}} combiner = <sum>
4146+ // CHECK: coreai.decomposable.broadcasting_sub
4147+ // CHECK: coreai.gather_along_axis
4148+ // CHECK: coreai.decomposable.broadcasting_where
4149+ // CHECK: coreai.reshape
4150+ // CHECK: coreai.output
4151+ """ ,
4152+ )
4153+
4154+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4155+ @pytest .mark .parametrize ("dtype" , [torch .float32 , torch .float16 , torch .int32 ])
4156+ async def test_basic (self , dtype : torch .dtype , dynamic : bool ) -> None :
4157+ """self.shape == mask.shape, src is 1-D of length mask.numel()."""
4158+ self_ = torch .zeros (2 , 3 , dtype = dtype )
4159+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4160+ src = torch .arange (1 , 7 , dtype = dtype )
4161+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4162+ await validate_numerical_output (
4163+ model = self ._Model ().eval (),
4164+ self_ = self_ ,
4165+ mask = mask ,
4166+ src = src ,
4167+ dynamic_shapes = dynamic_shapes ,
4168+ )
4169+
4170+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4171+ async def test_all_false_mask (self , dynamic : bool ) -> None :
4172+ """No positions selected — output must equal self unchanged."""
4173+ self_ = torch .arange (6.0 ).reshape (2 , 3 )
4174+ mask = torch .zeros (2 , 3 , dtype = torch .bool )
4175+ src = torch .full ((6 ,), - 1.0 )
4176+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4177+ await validate_numerical_output (
4178+ model = self ._Model ().eval (),
4179+ self_ = self_ ,
4180+ mask = mask ,
4181+ src = src ,
4182+ dynamic_shapes = dynamic_shapes ,
4183+ )
4184+
4185+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4186+ async def test_all_true_mask (self , dynamic : bool ) -> None :
4187+ """All positions selected — output must equal src reshaped to
4188+ self.shape, with self values fully overwritten.
4189+ """
4190+ self_ = torch .zeros (2 , 3 )
4191+ mask = torch .ones (2 , 3 , dtype = torch .bool )
4192+ src = torch .arange (1.0 , 7.0 )
4193+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4194+ await validate_numerical_output (
4195+ model = self ._Model ().eval (),
4196+ self_ = self_ ,
4197+ mask = mask ,
4198+ src = src ,
4199+ dynamic_shapes = dynamic_shapes ,
4200+ )
4201+
4202+ async def test_src_larger_than_mask_sum (self ) -> None :
4203+ """src may carry more elements than mask.sum(); only the leading
4204+ mask.sum() are consumed (read flat). The rest are unused.
4205+ """
4206+ self_ = torch .zeros (2 , 3 )
4207+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4208+ # 10 elements, only first 3 are needed (mask has 3 True positions)
4209+ src = torch .arange (1.0 , 11.0 )
4210+ await validate_numerical_output (
4211+ model = self ._Model ().eval (), self_ = self_ , mask = mask , src = src
4212+ )
4213+
4214+ async def test_broadcast_mask_lower_rank (self ) -> None :
4215+ """mask is rank-1 (3,) and self is rank-2 (2, 3) — mask is
4216+ right-aligned and broadcasts across the leading dim. Exercises
4217+ the broadcast_to branch in the lowering.
4218+ """
4219+ self_ = torch .zeros (2 , 3 )
4220+ mask = torch .tensor ([True , False , True ])
4221+ # mask broadcasts to (2, 3) with 4 True positions
4222+ src = torch .arange (1.0 , 5.0 )
4223+ await validate_numerical_output (
4224+ model = self ._Model ().eval (), self_ = self_ , mask = mask , src = src
4225+ )
4226+
4227+
40694228@pytest .mark .parametrize ("dynamic" , [False , True ])
40704229@pytest .mark .parametrize (
40714230 "x,dim,index" ,
0 commit comments