@@ -369,7 +369,8 @@ def test_channel_structured_pruning_hand(self) -> None:
369369 )
370370 assert torch .equal (model .weight .detach (), expected )
371371
372- def test_channel_structured_conv2d (self ) -> None :
372+ @pytest .mark .parametrize ("axis" , [0 , - 4 ])
373+ def test_channel_structured_conv2d (self , axis : int ) -> None :
373374 """Channel-structured pruning on Conv2d zeros entire output filters."""
374375 torch .manual_seed (42 )
375376 model = nn .Conv2d (3 , 8 , kernel_size = 3 , bias = False )
@@ -379,7 +380,7 @@ def test_channel_structured_conv2d(self) -> None:
379380 op_state_spec = {
380381 "weight" : PruningSpec (
381382 target_sparsity = 0.5 ,
382- pruning_scheme = ChannelStructured (axis = 0 ),
383+ pruning_scheme = ChannelStructured (axis = axis ),
383384 )
384385 }
385386 )
@@ -395,6 +396,72 @@ def test_channel_structured_conv2d(self) -> None:
395396 filt = weight [i ]
396397 assert filt .eq (0 ).all () or filt .ne (0 ).all (), f"Filter { i } is partially pruned"
397398
399+ @pytest .mark .parametrize ("target_sparsity" , [0.5 , 0.75 ])
400+ @pytest .mark .parametrize (
401+ "negative_axis,positive_axis" ,
402+ [(- 1 , 1 ), (- 2 , 0 )],
403+ ids = ["last-dim" , "first-dim" ],
404+ )
405+ def test_channel_structured_negative_axis (
406+ self , negative_axis : int , positive_axis : int , target_sparsity : float
407+ ) -> None :
408+ """A negative axis prunes the same channels as its positive equivalent."""
409+
410+ def prune_along (axis : int ) -> torch .Tensor :
411+ model = nn .Linear (4 , 4 , bias = False )
412+ with torch .no_grad ():
413+ model .weight .copy_ (
414+ torch .tensor (
415+ [
416+ [1.0 , 2.0 , 3.0 , 4.0 ],
417+ [5.0 , 6.0 , 7.0 , 8.0 ],
418+ [9.0 , 10.0 , 11.0 , 12.0 ],
419+ [13.0 , 14.0 , 15.0 , 16.0 ],
420+ ]
421+ )
422+ )
423+
424+ config = MagnitudePrunerConfig (
425+ global_config = ModuleMagnitudePrunerConfig (
426+ op_state_spec = {
427+ "weight" : PruningSpec (
428+ target_sparsity = target_sparsity ,
429+ pruning_scheme = ChannelStructured (axis = axis ),
430+ )
431+ }
432+ )
433+ )
434+ pruner = MagnitudePruner (model , config )
435+ pruner .prepare ((torch .randn (1 , 4 ),))
436+ return model .weight .detach ()
437+
438+ pruned = prune_along (negative_axis )
439+ assert torch .equal (pruned , prune_along (positive_axis ))
440+
441+ # L1 norms increase with index along both axes, so the highest indices survive.
442+ num_keep = 4 - int (4 * target_sparsity )
443+ kept = [i for i in range (4 ) if pruned .select (positive_axis , i ).ne (0 ).any ()]
444+ assert kept == list (range (4 - num_keep , 4 ))
445+
446+ @pytest .mark .parametrize ("axis" , [- 3 , 2 ], ids = ["below-range" , "above-range" ])
447+ def test_channel_structured_axis_out_of_range (self , axis : int ) -> None :
448+ """An axis outside [-ndim, ndim) raises ValueError."""
449+ model = nn .Linear (4 , 4 , bias = False )
450+ config = MagnitudePrunerConfig (
451+ global_config = ModuleMagnitudePrunerConfig (
452+ op_state_spec = {
453+ "weight" : PruningSpec (
454+ target_sparsity = 0.5 ,
455+ pruning_scheme = ChannelStructured (axis = axis ),
456+ )
457+ }
458+ )
459+ )
460+ pruner = MagnitudePruner (model , config )
461+
462+ with pytest .raises (ValueError , match = "Invalid axis" ):
463+ pruner .prepare ((torch .randn (1 , 4 ),))
464+
398465 def test_linear_unstructured_conv2d_channel_structured (self ) -> None :
399466 """Apply unstructured to Linear and channel-structured to Conv2d in same model."""
400467
0 commit comments