4444 DynamicRadialDegreeMixer ,
4545 SO2Convolution ,
4646 SO2Linear ,
47+ _active_triton_level ,
4748)
4849from deepmd .pt_expt .kernels .cuda .dpa4 .so2_conv_train import (
4950 op_available as cuda_value_available ,
5051)
52+ from deepmd .pt_expt .kernels .triton .sezm .grid_pair import (
53+ GRID_PAIR_TRITON_AVAILABLE ,
54+ )
55+ from deepmd .pt_expt .kernels .triton .sezm .segment_softmax import (
56+ SEGMENT_SOFTMAX_TRITON_AVAILABLE ,
57+ )
5158from deepmd .pt_expt .kernels .triton .sezm .so2_block_gemm import (
59+ SO2_BLOCK_GEMM_TRITON_AVAILABLE ,
5260 slices_supported ,
5361)
5462from deepmd .pt_expt .kernels .triton .sezm .so2_value_path import (
@@ -105,12 +113,21 @@ def test_cuda_train_gate_accepts_shared_truthy_values(monkeypatch, value: str) -
105113 assert cuda_train_enabled ()
106114
107115
108- @pytest .mark .parametrize ("triton_train" , [0 , 1 ])
109- def test_triton_train_gate_binds_its_stages (monkeypatch , triton_train : int ) -> None :
110- """``DP_TRITON_TRAIN`` binds the per-stage operators, and only it does."""
116+ @pytest .mark .parametrize (
117+ ("gate_name" , "training" ),
118+ [("DP_TRITON_TRAIN" , True ), ("DP_TRITON_INFER" , False )],
119+ ids = ("training" , "inference" ),
120+ )
121+ @pytest .mark .parametrize ("enabled" , [0 , 1 ])
122+ def test_triton_mode_gate_binds_each_stage (
123+ monkeypatch , gate_name : str , training : bool , enabled : int
124+ ) -> None :
125+ """Each Triton gate binds every supported stage for only its own mode."""
111126 _clear_gates (monkeypatch )
112- monkeypatch .setenv ("DP_TRITON_TRAIN" , str (triton_train ))
113- expected = bool (triton_train ) and SO2_VALUE_PATH_TRITON_AVAILABLE
127+ monkeypatch .setenv (gate_name , str (enabled ))
128+ requested = bool (enabled )
129+ train_level = enabled if training else 0
130+ infer_level = enabled if not training else 0
114131
115132 descriptor = _make_descriptor (2 , [20 ], 4.0 )
116133 convolutions = [
@@ -119,13 +136,19 @@ def test_triton_train_gate_binds_its_stages(monkeypatch, triton_train: int) -> N
119136 assert convolutions
120137
121138 for conv in convolutions :
122- assert conv .triton_train_level == triton_train
123- # The rotations, the segmented softmax and the flash aggregation all
124- # serve this layout; the aggregation is marked training-capable only
125- # by the training gate, which is what the dpmodel dispatch reads.
126- assert (conv ._rotate_to_local_fn is not None ) is expected
127- assert (conv ._segment_softmax_fn is not None ) is expected
128- assert conv ._flash_atten_trains is expected
139+ assert conv .triton_train_level == train_level
140+ assert conv .triton_infer_level == infer_level
141+ # Rotation and flash wrappers retain their eager implementations when
142+ # Triton is unavailable, so their binding follows the mode gates alone.
143+ assert (conv ._rotate_to_local_fn is not None ) is requested
144+ assert (conv ._rotate_back_fn is not None ) is requested
145+ assert (conv ._flash_atten_fn is not None ) is requested
146+ assert conv ._flash_atten_trains is (requested and training )
147+ # Segment softmax has no wrapper-level fallback and binds only when its
148+ # own Triton implementation is importable.
149+ assert (conv ._segment_softmax_fn is not None ) is (
150+ requested and SEGMENT_SOFTMAX_TRITON_AVAILABLE
151+ )
129152 # The rotate-mix front end is bound by a profitability bound on the
130153 # hidden width, which this narrow block sits below.
131154 assert conv .hidden_channels < 128
@@ -138,20 +161,44 @@ def test_triton_train_gate_binds_its_stages(monkeypatch, triton_train: int) -> N
138161 # The fused GEMM additionally needs every |m| block width to align
139162 # to its BN=64 tile, which a narrow block does not satisfy.
140163 aligned = slices_supported (module ._block_diag_slices )
141- assert (module ._block_diag_gemm is not None ) is (expected and aligned )
164+ assert (module ._block_diag_gemm is not None ) is (
165+ requested and SO2_BLOCK_GEMM_TRITON_AVAILABLE and aligned
166+ )
142167 if isinstance (module , DynamicRadialDegreeMixer ):
143- assert (module ._radial_mix_block is not None ) is expected
168+ # The callable contains its eager fallback, so construction binds it
169+ # whenever either mode requests the stage.
170+ assert (module ._radial_mix_block is not None ) is requested
144171 if isinstance (module , GatedActivation ):
145- assert module .triton_train_level == triton_train
172+ assert module .triton_train_level == train_level
173+ assert module .triton_infer_level == infer_level
146174 # The fused activation is bounded by the register footprint of one
147175 # focus stream's degrees.
148176 footprint_ok = module .channels <= 32 or (
149177 module .channels <= 64 and module .lmax <= 3
150178 )
151179 assert (module ._fused_gated_act is not None ) is (
152- expected and footprint_ok and module .layout == "fndc"
180+ requested and footprint_ok and module .layout == "fndc"
153181 )
154182
183+ # A shared binding is only a construction-time capability. Runtime dispatch
184+ # follows the active module mode, so the opposite gate remains disabled.
185+ for mode , active_level in ((training , enabled ), (not training , 0 )):
186+ descriptor .train (mode )
187+ for module in descriptor .modules ():
188+ if isinstance (
189+ module , (SO2Convolution , SO2Linear , DynamicRadialDegreeMixer )
190+ ):
191+ assert _active_triton_level (module ) == active_level
192+ if isinstance (module , SO2Convolution ):
193+ assert module ._rotation_active () is bool (active_level )
194+ if isinstance (module , GatedActivation ):
195+ level = (
196+ module .triton_train_level
197+ if module .training
198+ else module .triton_infer_level
199+ )
200+ assert level == active_level
201+
155202
156203def test_cuda_train_gate_binds_the_value_stream (monkeypatch ) -> None :
157204 """``DP_CUDA_TRAIN`` binds the fused value path without the Triton gate."""
@@ -196,10 +243,13 @@ def test_cuda_triton_train_reuses_packed_wigner_runs(monkeypatch) -> None:
196243 assert conv ._flash_atten_trains
197244
198245
199- def test_grid_pair_train_follows_the_slot_bound (monkeypatch ) -> None :
200- """The grid pair training operator binds only above its measured crossover."""
246+ @pytest .mark .parametrize ("gate_name" , ["DP_TRITON_TRAIN" , "DP_TRITON_INFER" ])
247+ def test_grid_pair_train_follows_its_gate_and_slot_bound (
248+ monkeypatch , gate_name : str
249+ ) -> None :
250+ """Grid-pair training ignores the inference gate and its narrow layouts."""
201251 _clear_gates (monkeypatch )
202- monkeypatch .setenv ("DP_TRITON_TRAIN" , "1" )
252+ monkeypatch .setenv (gate_name , "1" )
203253
204254 descriptor = _make_descriptor (2 , [20 ], 4.0 )
205255 grid_nets = [
@@ -212,7 +262,12 @@ def test_grid_pair_train_follows_the_slot_bound(monkeypatch) -> None:
212262 slots = int (net .projector .to_grid_mat .shape [1 ])
213263 # Below the crossover the dense section is small enough that the
214264 # operator's dispatch costs more than its kernels save.
215- assert (net ._grid_pair_train_fn is not None ) == (slots >= 75 )
265+ expected = (
266+ gate_name == "DP_TRITON_TRAIN"
267+ and GRID_PAIR_TRITON_AVAILABLE
268+ and slots >= 75
269+ )
270+ assert (net ._grid_pair_train_fn is not None ) is expected
216271
217272
218273@pytest .mark .skipif (not torch .cuda .is_available (), reason = "CUDA is required" )
0 commit comments