@@ -4322,6 +4322,165 @@ def forward(self, x: Tensor, index: Tensor, src: Tensor) -> Tensor:
43224322 )
43234323
43244324
4325+ class TestMaskedScatter :
4326+ """Tests for aten.masked_scatter.default → coreai flatten / cumsum-1 /
4327+ gather / where lowering.
4328+ """
4329+
4330+ class _Model (nn .Module ):
4331+ def forward (self , self_ : Tensor , mask : Tensor , src : Tensor ) -> Tensor :
4332+ return torch .masked_scatter (self_ , mask , src )
4333+
4334+ @staticmethod
4335+ def _shared_dynamic_shapes (self_ : Tensor , src : Tensor ) -> dict :
4336+ """Build dynamic_shapes where self_ and mask share dim objects
4337+ (torch.export emits an equality guard when traced shapes match)
4338+ and src has its own independent dim.
4339+ """
4340+ dims = {i : torch .export .Dim (f"d{ i } " , min = 1 ) for i in range (self_ .dim ())}
4341+ return {
4342+ "self_" : dims ,
4343+ "mask" : dims ,
4344+ "src" : {0 : torch .export .Dim ("s0" , min = 1 )}
4345+ if src .dim () == 1
4346+ else _all_dims_dynamic (src , "s" ),
4347+ }
4348+
4349+ @pytest .mark .ir
4350+ def test_lowers_ir_static (self ) -> None :
4351+ """Pin the full operand chain on a static shape."""
4352+ self_ = torch .zeros (2 , 3 )
4353+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4354+ src = torch .arange (1.0 , 7.0 )
4355+ program = torch .export .export (
4356+ self ._Model (), args = (self_ , mask , src )
4357+ ).run_decompositions ()
4358+ coreai_program = TorchConverter ().add_exported_program (program ).to_coreai ()
4359+ filecheck_pattern (
4360+ str (coreai_program ),
4361+ check_file = """
4362+ // CHECK-LABEL: coreai.graph @main
4363+ // CHECK-SAME: %arg0: tensor<2x3xf32>
4364+ // CHECK-SAME: %arg1: tensor<2x3xi1>
4365+ // CHECK-SAME: %arg2: tensor<6xf32>
4366+ // CHECK: %[[SF:.+]] = coreai.reshape %arg0, %{{.+}} : (tensor<2x3xf32>, tensor<1xsi32>) -> tensor<6xf32>
4367+ // CHECK: %[[MF:.+]] = coreai.reshape %arg1, %{{.+}} : (tensor<2x3xi1>, tensor<1xsi32>) -> tensor<6xi1>
4368+ // CHECK: %[[VF:.+]] = coreai.reshape %arg2, %{{.+}} : (tensor<6xf32>, tensor<1xsi32>) -> tensor<6xf32>
4369+ // CHECK: %[[MI:.+]] = coreai.cast %[[MF]] : tensor<6xi1> to tensor<6xsi32>
4370+ // CHECK: %[[CS:.+]] = coreai.scan %[[MI]], %{{.+}}, %{{.+}} combiner = <sum> : (tensor<6xsi32>, tensor<ui32>, tensor<i1>) -> tensor<6xsi32>
4371+ // CHECK: %[[IDX:.+]] = coreai.decomposable.broadcasting_sub %[[CS]], %{{.+}} : (tensor<6xsi32>, tensor<si32>) -> tensor<6xsi32>
4372+ // CHECK: %[[G:.+]] = coreai.gather_along_axis %[[VF]] at %[[IDX]] along %{{.+}} : (tensor<6xf32>, tensor<6xsi32>, tensor<si32>) to tensor<6xf32>
4373+ // CHECK: %[[W:.+]] = coreai.decomposable.broadcasting_where %[[MF]], %[[G]], %[[SF]] : (tensor<6xi1>, tensor<6xf32>, tensor<6xf32>) -> tensor<6xf32>
4374+ // CHECK: %[[OUT:.+]] = coreai.reshape %[[W]], %{{.+}} : (tensor<6xf32>, tensor<2xsi32>) -> tensor<2x3xf32>
4375+ // CHECK: coreai.output %[[OUT]]
4376+ """ ,
4377+ )
4378+
4379+ @pytest .mark .ir
4380+ def test_lowers_ir_dynamic (self ) -> None :
4381+ """Confirm the same op chain appears under dynamic shapes — shapes
4382+ become symbolic, but cast/scan/sub/gather/where/reshape order is
4383+ preserved.
4384+ """
4385+ self_ = torch .zeros (2 , 3 )
4386+ mask = torch .zeros (2 , 3 , dtype = torch .bool )
4387+ src = torch .arange (6.0 )
4388+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src )
4389+ program = torch .export .export (
4390+ self ._Model (), args = (self_ , mask , src ), dynamic_shapes = dynamic_shapes
4391+ ).run_decompositions ()
4392+ coreai_program = TorchConverter ().add_exported_program (program ).to_coreai ()
4393+ filecheck_pattern (
4394+ str (coreai_program ),
4395+ check_file = """
4396+ // CHECK-LABEL: coreai.graph @main
4397+ // CHECK: coreai.reshape
4398+ // CHECK: coreai.reshape
4399+ // CHECK: coreai.reshape
4400+ // CHECK: coreai.cast {{.+}} to tensor<{{.*}}xsi32>
4401+ // CHECK: coreai.scan {{.+}} combiner = <sum>
4402+ // CHECK: coreai.decomposable.broadcasting_sub
4403+ // CHECK: coreai.gather_along_axis
4404+ // CHECK: coreai.decomposable.broadcasting_where
4405+ // CHECK: coreai.reshape
4406+ // CHECK: coreai.output
4407+ """ ,
4408+ )
4409+
4410+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4411+ @pytest .mark .parametrize ("dtype" , [torch .float32 , torch .float16 , torch .int32 ])
4412+ async def test_basic (self , dtype : torch .dtype , dynamic : bool ) -> None :
4413+ """self.shape == mask.shape, src is 1-D of length mask.numel()."""
4414+ self_ = torch .zeros (2 , 3 , dtype = dtype )
4415+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4416+ src = torch .arange (1 , 7 , dtype = dtype )
4417+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4418+ await validate_numerical_output (
4419+ model = self ._Model ().eval (),
4420+ self_ = self_ ,
4421+ mask = mask ,
4422+ src = src ,
4423+ dynamic_shapes = dynamic_shapes ,
4424+ )
4425+
4426+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4427+ async def test_all_false_mask (self , dynamic : bool ) -> None :
4428+ """No positions selected — output must equal self unchanged."""
4429+ self_ = torch .arange (6.0 ).reshape (2 , 3 )
4430+ mask = torch .zeros (2 , 3 , dtype = torch .bool )
4431+ src = torch .full ((6 ,), - 1.0 )
4432+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4433+ await validate_numerical_output (
4434+ model = self ._Model ().eval (),
4435+ self_ = self_ ,
4436+ mask = mask ,
4437+ src = src ,
4438+ dynamic_shapes = dynamic_shapes ,
4439+ )
4440+
4441+ @pytest .mark .parametrize ("dynamic" , [False , True ])
4442+ async def test_all_true_mask (self , dynamic : bool ) -> None :
4443+ """All positions selected — output must equal src reshaped to
4444+ self.shape, with self values fully overwritten.
4445+ """
4446+ self_ = torch .zeros (2 , 3 )
4447+ mask = torch .ones (2 , 3 , dtype = torch .bool )
4448+ src = torch .arange (1.0 , 7.0 )
4449+ dynamic_shapes = self ._shared_dynamic_shapes (self_ , src ) if dynamic else None
4450+ await validate_numerical_output (
4451+ model = self ._Model ().eval (),
4452+ self_ = self_ ,
4453+ mask = mask ,
4454+ src = src ,
4455+ dynamic_shapes = dynamic_shapes ,
4456+ )
4457+
4458+ async def test_src_larger_than_mask_sum (self ) -> None :
4459+ """src may carry more elements than mask.sum(); only the leading
4460+ mask.sum() are consumed (read flat). The rest are unused.
4461+ """
4462+ self_ = torch .zeros (2 , 3 )
4463+ mask = torch .tensor ([[True , False , True ], [False , True , False ]])
4464+ # 10 elements, only first 3 are needed (mask has 3 True positions)
4465+ src = torch .arange (1.0 , 11.0 )
4466+ await validate_numerical_output (
4467+ model = self ._Model ().eval (), self_ = self_ , mask = mask , src = src
4468+ )
4469+
4470+ async def test_broadcast_mask_lower_rank (self ) -> None :
4471+ """mask is rank-1 (3,) and self is rank-2 (2, 3) — mask is
4472+ right-aligned and broadcasts across the leading dim. Exercises
4473+ the broadcast_to branch in the lowering.
4474+ """
4475+ self_ = torch .zeros (2 , 3 )
4476+ mask = torch .tensor ([True , False , True ])
4477+ # mask broadcasts to (2, 3) with 4 True positions
4478+ src = torch .arange (1.0 , 5.0 )
4479+ await validate_numerical_output (
4480+ model = self ._Model ().eval (), self_ = self_ , mask = mask , src = src
4481+ )
4482+
4483+
43254484@pytest .mark .parametrize ("dynamic" , [False , True ])
43264485@pytest .mark .parametrize (
43274486 "x,dim,index" ,
0 commit comments