Skip to content

Commit 072748b

Browse files
committed
fix(ci): stabilize DPA4 acceleration coverage
1 parent 618918a commit 072748b

4 files changed

Lines changed: 179 additions & 45 deletions

File tree

deepmd/pt/utils/compile_compat.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -655,13 +655,19 @@ def build_inductor_compile_options(*, inference: bool = False) -> dict[str, Any]
655655
# the loops are parallel. The axes this threshold guards are always
656656
# system sized at run time, so the guard is removed rather than
657657
# retuned.
658-
compile_options["cpp.min_chunk_size"] = 1
659-
# Resolve the thread count at run time instead of baking the freezing
660-
# host's into the generated code. A deployed artifact is routinely
661-
# loaded on a machine with a different core count, and an artifact
662-
# frozen under the DeePMD-kit thread defaults would otherwise pin
663-
# every parallel region to those.
664-
compile_options["cpp.dynamic_threads"] = True
658+
under_lsan = os.environ.get("DP_GEN_UNDER_SANITIZER") == "lsan"
659+
if not under_lsan:
660+
compile_options["cpp.min_chunk_size"] = 1
661+
# Outside sanitizer fixtures, resolve the thread count at run time
662+
# instead of baking the freezing host's into the generated code. A
663+
# deployed artifact is routinely loaded on a machine with a different
664+
# core count, and an artifact frozen under the DeePMD-kit thread
665+
# defaults would otherwise pin every parallel region to those.
666+
compile_options["cpp.dynamic_threads"] = not under_lsan
667+
if under_lsan:
668+
# LeakSanitizer fails on the generated OpenMP force/virial
669+
# reductions, so its memory-safety fixtures use serial codegen.
670+
compile_options["cpp.threads"] = 1
665671
try:
666672
from torch._inductor import config as inductor_config
667673

source/tests/pt/model/test_descriptor_sezm_train_paths.py

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
DynamicRadialDegreeMixer,
4747
SO2Convolution,
4848
SO2Linear,
49+
active_triton_level,
4950
)
5051
from deepmd.pt.utils import (
5152
env,
@@ -54,7 +55,14 @@
5455
from deepmd.pt_expt.kernels.cuda.dpa4.so2_conv_train import (
5556
op_available as cuda_value_available,
5657
)
58+
from deepmd.pt_expt.kernels.triton.sezm.grid_pair import (
59+
GRID_PAIR_TRITON_AVAILABLE,
60+
)
61+
from deepmd.pt_expt.kernels.triton.sezm.segment_softmax import (
62+
SEGMENT_SOFTMAX_TRITON_AVAILABLE,
63+
)
5764
from deepmd.pt_expt.kernels.triton.sezm.so2_block_gemm import (
65+
SO2_BLOCK_GEMM_TRITON_AVAILABLE,
5866
slices_supported,
5967
)
6068
from deepmd.pt_expt.kernels.triton.sezm.so2_value_path import (
@@ -106,12 +114,21 @@ def _clear_gates(monkeypatch) -> None:
106114
monkeypatch.setenv(name, "0")
107115

108116

109-
@pytest.mark.parametrize("triton_train", [0, 1])
110-
def test_triton_train_gate_binds_its_stages(monkeypatch, triton_train: int) -> None:
111-
"""``DP_TRITON_TRAIN`` binds the per-stage operators, and only it does."""
117+
@pytest.mark.parametrize(
118+
("gate_name", "training"),
119+
[("DP_TRITON_TRAIN", True), ("DP_TRITON_INFER", False)],
120+
ids=("training", "inference"),
121+
)
122+
@pytest.mark.parametrize("enabled", [0, 1])
123+
def test_triton_mode_gate_binds_each_stage(
124+
monkeypatch, gate_name: str, training: bool, enabled: int
125+
) -> None:
126+
"""Each Triton gate binds every supported stage for only its own mode."""
112127
_clear_gates(monkeypatch)
113-
monkeypatch.setenv("DP_TRITON_TRAIN", str(triton_train))
114-
expected = bool(triton_train) and SO2_VALUE_PATH_TRITON_AVAILABLE
128+
monkeypatch.setenv(gate_name, str(enabled))
129+
requested = bool(enabled)
130+
train_level = enabled if training else 0
131+
infer_level = enabled if not training else 0
115132

116133
descriptor = _make_descriptor(2, [20], 4.0)
117134
convolutions = [
@@ -120,10 +137,19 @@ def test_triton_train_gate_binds_its_stages(monkeypatch, triton_train: int) -> N
120137
assert convolutions
121138

122139
for conv in convolutions:
123-
assert conv.triton_train_level == triton_train
124-
assert (conv._rotate_to_local_fn is not None) is expected
125-
assert (conv._segment_softmax_fn is not None) is expected
126-
assert conv._flash_atten_trains is expected
140+
assert conv.triton_train_level == train_level
141+
assert conv.triton_infer_level == infer_level
142+
# Rotation and flash wrappers retain their eager implementations when
143+
# Triton is unavailable, so their binding follows the mode gates alone.
144+
assert (conv._rotate_to_local_fn is not None) is requested
145+
assert (conv._rotate_back_fn is not None) is requested
146+
assert (conv._flash_atten_fn is not None) is requested
147+
assert conv._flash_atten_trains is (requested and training)
148+
# Segment softmax has no wrapper-level fallback and binds only when its
149+
# own Triton implementation is importable.
150+
assert (conv._segment_softmax_fn is not None) is (
151+
requested and SEGMENT_SOFTMAX_TRITON_AVAILABLE
152+
)
127153
# The rotate-mix front end is bound by a profitability bound on the
128154
# hidden width, which this narrow block sits below.
129155
assert conv.hidden_channels < 128
@@ -136,18 +162,40 @@ def test_triton_train_gate_binds_its_stages(monkeypatch, triton_train: int) -> N
136162
# The fused GEMM additionally needs every |m| block width to align
137163
# to its BN=64 tile, which a narrow block does not satisfy.
138164
aligned = slices_supported(module._block_diag_slices)
139-
assert (module._block_diag_gemm is not None) is (expected and aligned)
165+
assert (module._block_diag_gemm is not None) is (
166+
requested and SO2_BLOCK_GEMM_TRITON_AVAILABLE and aligned
167+
)
140168
if isinstance(module, DynamicRadialDegreeMixer):
141-
assert (module._radial_mix_block is not None) is expected
169+
# The callable contains its eager fallback, so construction binds it
170+
# whenever either mode requests the stage.
171+
assert (module._radial_mix_block is not None) is requested
142172
if isinstance(module, GatedActivation):
143-
assert module.triton_train_level == triton_train
173+
assert module.triton_train_level == train_level
174+
assert module.triton_infer_level == infer_level
144175
footprint_ok = module.channels <= 32 or (
145176
module.channels <= 64 and module.lmax <= 3
146177
)
147178
assert (module._fused_gated_act is not None) is (
148-
expected and footprint_ok and module.layout == "fndc"
179+
requested and footprint_ok and module.layout == "fndc"
149180
)
150181

182+
# A shared binding is only a construction-time capability. Runtime dispatch
183+
# follows the active module mode, so the opposite gate remains disabled.
184+
for mode, active_level in ((training, enabled), (not training, 0)):
185+
descriptor.train(mode)
186+
for module in descriptor.modules():
187+
if isinstance(
188+
module, (SO2Convolution, SO2Linear, DynamicRadialDegreeMixer)
189+
):
190+
assert active_triton_level(module) == active_level
191+
if isinstance(module, GatedActivation):
192+
level = (
193+
module.triton_train_level
194+
if module.training
195+
else module.triton_infer_level
196+
)
197+
assert level == active_level
198+
151199

152200
def test_cuda_train_gate_binds_the_value_stream(monkeypatch) -> None:
153201
"""``DP_CUDA_TRAIN`` binds the fused value path without the Triton gate."""
@@ -192,10 +240,13 @@ def test_cuda_triton_train_reuses_packed_wigner_runs(monkeypatch) -> None:
192240
assert conv._flash_atten_trains
193241

194242

195-
def test_grid_pair_train_follows_the_slot_bound(monkeypatch) -> None:
196-
"""The grid pair training operator binds only above its measured crossover."""
243+
@pytest.mark.parametrize("gate_name", ["DP_TRITON_TRAIN", "DP_TRITON_INFER"])
244+
def test_grid_pair_train_follows_its_gate_and_slot_bound(
245+
monkeypatch, gate_name: str
246+
) -> None:
247+
"""Grid-pair training ignores the inference gate and its narrow layouts."""
197248
_clear_gates(monkeypatch)
198-
monkeypatch.setenv("DP_TRITON_TRAIN", "1")
249+
monkeypatch.setenv(gate_name, "1")
199250

200251
descriptor = _make_descriptor(2, [20], 4.0)
201252
grid_nets = [
@@ -206,7 +257,12 @@ def test_grid_pair_train_follows_the_slot_bound(monkeypatch) -> None:
206257
assert grid_nets
207258
for net in grid_nets:
208259
slots = int(net.projector.to_grid_mat.shape[1])
209-
assert (net._grid_pair_train_fn is not None) == (slots >= 75)
260+
expected = (
261+
gate_name == "DP_TRITON_TRAIN"
262+
and GRID_PAIR_TRITON_AVAILABLE
263+
and slots >= 75
264+
)
265+
assert (net._grid_pair_train_fn is not None) is expected
210266

211267

212268
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required")

source/tests/pt/test_compile_compat.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ def test_missing_accessors_fall_through_best_effort(self) -> None:
3232

3333
def test_fusion_size_defaults_to_eight(monkeypatch) -> None:
3434
monkeypatch.delenv("DP_FUSION_SIZE", raising=False)
35+
monkeypatch.delenv("DP_GEN_UNDER_SANITIZER", raising=False)
3536

3637
assert build_inductor_compile_options()["max_fusion_size"] == 8
37-
assert build_inductor_compile_options(inference=True)["max_fusion_size"] == 8
38+
inference_options = build_inductor_compile_options(inference=True)
39+
assert inference_options["max_fusion_size"] == 8
40+
assert inference_options["cpp.min_chunk_size"] == 1
41+
assert inference_options["cpp.dynamic_threads"] is True
3842

3943

4044
def test_fusion_size_environment_is_shared(monkeypatch) -> None:
@@ -44,6 +48,19 @@ def test_fusion_size_environment_is_shared(monkeypatch) -> None:
4448
assert build_inductor_compile_options(inference=True)["max_fusion_size"] == 16
4549

4650

51+
def test_lsan_inference_uses_serial_codegen(monkeypatch) -> None:
52+
monkeypatch.setenv("DP_GEN_UNDER_SANITIZER", "lsan")
53+
54+
training_options = build_inductor_compile_options()
55+
inference_options = build_inductor_compile_options(inference=True)
56+
57+
assert "cpp.threads" not in training_options
58+
assert "cpp.dynamic_threads" not in training_options
59+
assert "cpp.min_chunk_size" not in inference_options
60+
assert inference_options["cpp.dynamic_threads"] is False
61+
assert inference_options["cpp.threads"] == 1
62+
63+
4764
@pytest.mark.parametrize("value", ["0", "-1", "fast"])
4865
def test_fusion_size_rejects_invalid_values(monkeypatch, value: str) -> None:
4966
monkeypatch.setenv("DP_FUSION_SIZE", value)

source/tests/pt_expt/descriptor/test_dpa4_train_paths.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,19 @@
4444
DynamicRadialDegreeMixer,
4545
SO2Convolution,
4646
SO2Linear,
47+
_active_triton_level,
4748
)
4849
from 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+
)
5158
from deepmd.pt_expt.kernels.triton.sezm.so2_block_gemm import (
59+
SO2_BLOCK_GEMM_TRITON_AVAILABLE,
5260
slices_supported,
5361
)
5462
from 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

156203
def 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

Comments
 (0)