forked from deepmodeling/deepmd-kit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdpa1.py
More file actions
2462 lines (2288 loc) · 94 KB
/
Copy pathdpa1.py
File metadata and controls
2462 lines (2288 loc) · 94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-License-Identifier: LGPL-3.0-or-later
import math
import warnings
from collections.abc import (
Callable,
)
from typing import (
Any,
NoReturn,
Optional,
Union,
)
import array_api_compat
import numpy as np
from deepmd.dpmodel import (
DEFAULT_PRECISION,
PRECISION_DICT,
NativeOP,
)
from deepmd.dpmodel.array_api import (
Array,
xp_take_along_axis,
xp_take_first_n,
)
from deepmd.dpmodel.common import (
cast_precision,
get_xp_precision,
to_numpy_array,
to_numpy_dtype,
)
from deepmd.dpmodel.utils import (
EmbeddingNet,
EnvMat,
NetworkCollection,
PairExcludeMask,
tabulate_fusion,
)
from deepmd.dpmodel.utils.env_mat_stat import (
EnvMatStatSe,
)
from deepmd.dpmodel.utils.network import (
LayerNorm,
NativeLayer,
)
from deepmd.dpmodel.utils.safe_gradient import (
safe_for_vector_norm,
)
from deepmd.dpmodel.utils.seed import (
child_seed,
)
from deepmd.dpmodel.utils.type_embed import (
TypeEmbedNet,
)
from deepmd.dpmodel.utils.update_sel import (
UpdateSel,
)
from deepmd.utils.data_system import (
DeepmdDataSystem,
)
from deepmd.utils.env_mat_stat import (
StatItem,
)
from deepmd.utils.finetune import (
get_index_between_two_maps,
map_pair_exclude_types,
)
from deepmd.utils.path import (
DPPath,
)
from deepmd.utils.tabulate_math import (
DPTabulate,
)
from deepmd.utils.version import (
check_version_compatibility,
)
from .base_descriptor import (
BaseDescriptor,
)
from .descriptor import (
DescriptorBlock,
extend_descrpt_stat,
)
def np_softmax(x: Array, axis: int = -1) -> Array:
xp = array_api_compat.array_namespace(x)
# x = xp.nan_to_num(x) # to avoid value warning
x = xp.where(xp.isnan(x), xp.zeros_like(x), x)
e_x = xp.exp(x - xp.max(x, axis=axis, keepdims=True))
return e_x / xp.sum(e_x, axis=axis, keepdims=True)
def np_normalize(x: Array, axis: int = -1) -> Array:
xp = array_api_compat.array_namespace(x)
return x / xp.linalg.vector_norm(x, axis=axis, keepdims=True)
@BaseDescriptor.register("se_atten")
@BaseDescriptor.register("dpa1")
class DescrptDPA1(NativeOP, BaseDescriptor):
r"""Attention-based descriptor which is proposed in the pretrainable DPA-1[1] model.
This descriptor, :math:`\mathcal{D}^i \in \mathbb{R}^{M \times M_{<}}`, is given by
.. math::
\mathcal{D}^i = \frac{1}{N_c^2}(\hat{\mathcal{G}}^i)^T \mathcal{R}^i (\mathcal{R}^i)^T \hat{\mathcal{G}}^i_<,
where :math:`\hat{\mathcal{G}}^i` represents the embedding matrix:math:`\mathcal{G}^i`
after additional self-attention mechanism and :math:`\mathcal{R}^i` is defined by the full case in the se_e2_a descriptor.
Note that we obtain :math:`\mathcal{G}^i` using the type embedding method by default in this descriptor.
To perform the self-attention mechanism, the queries :math:`\mathcal{Q}^{i,l} \in \mathbb{R}^{N_c\times d_k}`,
keys :math:`\mathcal{K}^{i,l} \in \mathbb{R}^{N_c\times d_k}`,
and values :math:`\mathcal{V}^{i,l} \in \mathbb{R}^{N_c\times d_v}` are first obtained:
.. math::
\left(\mathcal{Q}^{i,l}\right)_{j}=Q_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right),
.. math::
\left(\mathcal{K}^{i,l}\right)_{j}=K_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right),
.. math::
\left(\mathcal{V}^{i,l}\right)_{j}=V_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right),
where :math:`Q_{l}`, :math:`K_{l}`, :math:`V_{l}` represent three trainable linear transformations
that output the queries and keys of dimension :math:`d_k` and values of dimension :math:`d_v`, and :math:`l`
is the index of the attention layer.
The input embedding matrix to the attention layers, denoted by :math:`\mathcal{G}^{i,0}`,
is chosen as the two-body embedding matrix.
Then the scaled dot-product attention method is adopted:
.. math::
A(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}, \mathcal{V}^{i,l}, \mathcal{R}^{i,l})=\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right)\mathcal{V}^{i,l},
where :math:`\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right) \in \mathbb{R}^{N_c\times N_c}` is attention weights.
In the original attention method,
one typically has :math:`\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}\right)=\mathrm{softmax}\left(\frac{\mathcal{Q}^{i,l} (\mathcal{K}^{i,l})^{T}}{\sqrt{d_{k}}}\right)`,
with :math:`\sqrt{d_{k}}` being the normalization temperature.
This is slightly modified to incorporate the angular information:
.. math::
\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right) = \mathrm{softmax}\left(\frac{\mathcal{Q}^{i,l} (\mathcal{K}^{i,l})^{T}}{\sqrt{d_{k}}}\right) \odot \hat{\mathcal{R}}^{i}(\hat{\mathcal{R}}^{i})^{T},
where :math:`\hat{\mathcal{R}}^{i} \in \mathbb{R}^{N_c\times 3}` denotes normalized relative coordinates,
:math:`\hat{\mathcal{R}}^{i}_{j} = \frac{\boldsymbol{r}_{ij}}{\lVert \boldsymbol{r}_{ij} \lVert}`
and :math:`\odot` means element-wise multiplication.
Then layer normalization is added in a residual way to finally obtain the self-attention local embedding matrix
:math:`\hat{\mathcal{G}}^{i} = \mathcal{G}^{i,L_a}` after :math:`L_a` attention layers:[^1]
.. math::
\mathcal{G}^{i,l} = \mathcal{G}^{i,l-1} + \mathrm{LayerNorm}(A(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}, \mathcal{V}^{i,l}, \mathcal{R}^{i,l})).
Parameters
----------
rcut: float
The cut-off radius :math:`r_c`
rcut_smth: float
From where the environment matrix should be smoothed :math:`r_s`
sel : list[int], int
list[int]: sel[i] specifies the maxmum number of type i atoms in the cut-off radius
int: the total maxmum number of atoms in the cut-off radius
ntypes : int
Number of element types
neuron : list[int]
Number of neurons in each hidden layers of the embedding net :math:`\mathcal{N}`
axis_neuron: int
Number of the axis neuron :math:`M_2` (number of columns of the sub-matrix of the embedding matrix)
tebd_dim: int
Dimension of the type embedding
tebd_input_mode: str
The input mode of the type embedding. Supported modes are ["concat", "strip"].
- "concat": Concatenate the type embedding with the smoothed radial information as the union input for the embedding network.
- "strip": Use a separated embedding network for the type embedding and combine the output with the radial embedding network output.
resnet_dt: bool
Time-step `dt` in the resnet construction:
y = x + dt * \phi (Wx + b)
trainable: bool
If the weights of this descriptors are trainable.
trainable_ln: bool
Whether to use trainable shift and scale weights in layer normalization.
ln_eps: float, Optional
The epsilon value for layer normalization.
type_one_side: bool
If 'False', type embeddings of both neighbor and central atoms are considered.
If 'True', only type embeddings of neighbor atoms are considered.
Default is 'False'.
attn: int
Hidden dimension of the attention vectors
attn_layer: int
Number of attention layers
attn_dotr: bool
If dot the angular gate to the attention weights
attn_mask: bool
(Only support False to keep consistent with other backend references.)
(Not used in this version. True option is not implemented.)
If mask the diagonal of attention weights
exclude_types : list[list[int]]
The excluded pairs of types which have no interaction with each other.
For example, `[[0, 1]]` means no interaction between type 0 and type 1.
env_protection: float
Protection parameter to prevent division by zero errors during environment matrix calculations.
set_davg_zero: bool
Set the shift of embedding net input to zero.
activation_function: str
The activation function in the embedding net. Supported options are |ACTIVATION_FN|
precision: str
The precision of the embedding net parameters. Supported options are |PRECISION|
scaling_factor: float
The scaling factor of normalization in calculations of attention weights.
If `temperature` is None, the scaling of attention weights is (N_dim * scaling_factor)**0.5
normalize: bool
Whether to normalize the hidden vectors in attention weights calculation.
temperature: float
If not None, the scaling of attention weights is `temperature` itself.
smooth_type_embedding: bool
Whether to use smooth process in attention weights calculation.
concat_output_tebd: bool
Whether to concat type embedding at the output of the descriptor.
stripped_type_embedding: bool, Optional
(Deprecated, kept only for compatibility.)
Whether to strip the type embedding into a separate embedding network.
Setting this parameter to `True` is equivalent to setting `tebd_input_mode` to 'strip'.
Setting it to `False` is equivalent to setting `tebd_input_mode` to 'concat'.
The default value is `None`, which means the `tebd_input_mode` setting will be used instead.
use_econf_tebd: bool, Optional
Whether to use electronic configuration type embedding.
use_tebd_bias : bool, Optional
Whether to use bias in the type embedding layer.
type_map: list[str], Optional
A list of strings. Give the name to each type of atoms.
spin
(Only support None to keep consistent with other backend references.)
(Not used in this version. Not-none option is not implemented.)
The old implementation of deepspin.
Limitations
-----------
The currently implementation will not support the following deprecated features
1. spin is not None
2. attn_mask == True
References
----------
.. [1] Duo Zhang, Hangrui Bi, Fu-Zhi Dai, Wanrun Jiang, Linfeng Zhang, and Han Wang. 2022.
DPA-1: Pretraining of Attention-based Deep Potential Model for Molecular Simulation.
arXiv preprint arXiv:2208.08236.
"""
_update_sel_cls = UpdateSel
def __init__(
self,
rcut: float,
rcut_smth: float,
sel: list[int] | int,
ntypes: int,
neuron: list[int] = [25, 50, 100],
axis_neuron: int = 8,
tebd_dim: int = 8,
tebd_input_mode: str = "concat",
resnet_dt: bool = False,
trainable: bool = True,
type_one_side: bool = False,
attn: int = 128,
attn_layer: int = 2,
attn_dotr: bool = True,
attn_mask: bool = False,
exclude_types: list[tuple[int, int]] = [],
env_protection: float = 0.0,
set_davg_zero: bool = False,
activation_function: str = "tanh",
precision: str = DEFAULT_PRECISION,
scaling_factor: float = 1.0,
normalize: bool = True,
temperature: float | None = None,
trainable_ln: bool = True,
ln_eps: float | None = 1e-5,
smooth_type_embedding: bool = True,
concat_output_tebd: bool = True,
spin: None = None,
stripped_type_embedding: bool | None = None,
use_econf_tebd: bool = False,
use_tebd_bias: bool = False,
type_map: list[str] | None = None,
# consistent with argcheck, not used though
seed: int | list[int] | None = None,
) -> None:
## seed, uniform_seed, not included.
# Ensure compatibility with the deprecated stripped_type_embedding option.
if stripped_type_embedding is not None:
# Use the user-set stripped_type_embedding parameter first
tebd_input_mode = "strip" if stripped_type_embedding else "concat"
if spin is not None:
raise NotImplementedError("old implementation of spin is not supported.")
if attn_mask:
raise NotImplementedError(
"old implementation of attn_mask is not supported."
)
# to keep consistent with default value in this backends
if ln_eps is None:
ln_eps = 1e-5
self.se_atten = DescrptBlockSeAtten(
rcut,
rcut_smth,
sel,
ntypes,
neuron=neuron,
axis_neuron=axis_neuron,
tebd_dim=tebd_dim,
tebd_input_mode=tebd_input_mode,
set_davg_zero=set_davg_zero,
attn=attn,
attn_layer=attn_layer,
attn_dotr=attn_dotr,
attn_mask=False,
activation_function=activation_function,
precision=precision,
resnet_dt=resnet_dt,
scaling_factor=scaling_factor,
normalize=normalize,
temperature=temperature,
smooth=smooth_type_embedding,
type_one_side=type_one_side,
exclude_types=exclude_types,
env_protection=env_protection,
trainable_ln=trainable_ln,
ln_eps=ln_eps,
seed=child_seed(seed, 0),
trainable=trainable,
)
self.use_econf_tebd = use_econf_tebd
self.use_tebd_bias = use_tebd_bias
self.type_map = type_map
self.type_embedding = TypeEmbedNet(
ntypes=ntypes,
neuron=[tebd_dim],
padding=True,
activation_function="Linear",
precision=precision,
use_econf_tebd=use_econf_tebd,
use_tebd_bias=use_tebd_bias,
type_map=type_map,
seed=child_seed(seed, 1),
trainable=trainable,
)
self.tebd_dim = tebd_dim
self.concat_output_tebd = concat_output_tebd
self.trainable = trainable
self.precision = precision
self.tebd_compress = False
self.geo_compress = False
self.compress = False
# When set, force the legacy dense lower even if the config would
# otherwise be graph-lower eligible (see ``disable_graph_lower``).
self._graph_lower_disabled = False
def get_rcut(self) -> float:
"""Returns the cut-off radius."""
return self.se_atten.get_rcut()
def get_rcut_smth(self) -> float:
"""Returns the radius where the neighbor information starts to smoothly decay to 0."""
return self.se_atten.get_rcut_smth()
def get_nsel(self) -> int:
"""Returns the number of selected atoms in the cut-off radius."""
return self.se_atten.get_nsel()
def get_sel(self) -> list[int]:
"""Returns the number of selected atoms for each type."""
return self.se_atten.get_sel()
def get_ntypes(self) -> int:
"""Returns the number of element types."""
return self.se_atten.get_ntypes()
def get_type_map(self) -> list[str]:
"""Get the name to each type of atoms."""
return self.type_map
def get_dim_out(self) -> int:
"""Returns the output dimension."""
ret = self.se_atten.get_dim_out()
if self.concat_output_tebd:
ret += self.tebd_dim
return ret
def get_dim_emb(self) -> int:
return self.se_atten.dim_emb
def mixed_types(self) -> bool:
"""If true, the descriptor
1. assumes total number of atoms aligned across frames;
2. requires a neighbor list that does not distinguish different atomic types.
If false, the descriptor
1. assumes total number of atoms of each atom type aligned across frames;
2. requires a neighbor list that distinguishes different atomic types.
"""
return self.se_atten.mixed_types()
def has_message_passing(self) -> bool:
"""Returns whether the descriptor has message passing."""
return self.se_atten.has_message_passing()
def has_message_passing_across_ranks(self) -> bool:
"""Returns whether per-layer node embeddings need MPI ghost exchange.
DPA1 (se_atten) is single-layer and does not exchange features
across ranks; same as the base se_e2_a path.
"""
return False
def need_sorted_nlist_for_lower(self) -> bool:
"""Returns whether the descriptor needs sorted nlist when using `forward_lower`."""
return self.se_atten.need_sorted_nlist_for_lower()
def get_env_protection(self) -> float:
"""Returns the protection of building environment matrix."""
return self.se_atten.get_env_protection()
def get_numb_attn_layer(self) -> int:
"""Returns the number of se_atten attention layers."""
return self.se_atten.attn_layer
def uses_graph_lower(self) -> bool:
"""Returns whether this descriptor supports the graph-native lower.
The graph-native lower (``call_graph``) covers the factorizable path
AND transformer attention (``attn_layer >= 0``, NeighborGraph PR-D)
with concat type-embedding. ``exclude_types`` is fully supported via
:func:`~deepmd.dpmodel.utils.neighbor_graph.apply_pair_exclusion`.
The only remaining ineligible config is ``tebd_input_mode == "strip"``,
which falls back to the legacy dense path.
Eligibility does NOT imply numerical interchangeability with the
dense route for every config: with ``smooth_type_embedding=True``
the carry-all graph attention is sel-independent by design and
differs from the dense lower by up to ~1e-4 (see the Notes of
:meth:`call_graph`).
"""
if self._graph_lower_disabled:
return False
return self.se_atten.tebd_input_mode == "concat"
def disable_graph_lower(self) -> None:
"""Force the legacy dense lower for this descriptor.
This is an explicit opt-out knob used by contexts where the
graph-native lower is unsupported or undesirable (e.g. spin models,
whose carry-all routing diverges on sel-binding spin systems and
whose ``.pt2``/``.pte`` export trips a torch-inductor scatter/
atomic_add CPU codegen assertion). After calling this,
:meth:`uses_graph_lower` returns ``False`` regardless of the
descriptor configuration. The flag is not serialized; it is
re-derived structurally at spin-model construction/deserialization.
"""
self._graph_lower_disabled = True
def share_params(
self, base_class: "DescrptDPA1", shared_level: int, resume: bool = False
) -> NoReturn:
"""
Share the parameters of self to the base_class with shared_level during multitask training.
If not start from checkpoint (resume is False),
some separated parameters (e.g. mean and stddev) will be re-calculated across different classes.
"""
raise NotImplementedError
@property
def dim_out(self) -> int:
return self.get_dim_out()
@property
def dim_emb(self) -> int:
return self.get_dim_emb()
def compute_input_stats(
self,
merged: Callable[[], list[dict]] | list[dict],
path: DPPath | None = None,
) -> None:
"""
Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data.
Parameters
----------
merged : Union[Callable[[], list[dict]], list[dict]]
- list[dict]: A list of data samples from various data systems.
Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor`
originating from the `i`-th data system.
- Callable[[], list[dict]]: A lazy function that returns data samples in the above format
only when needed. Since the sampling process can be slow and memory-intensive,
the lazy function helps by only sampling once.
path : Optional[DPPath]
The path to the stat file.
"""
return self.se_atten.compute_input_stats(merged, path)
def set_stat_mean_and_stddev(
self,
mean: Array,
stddev: Array,
) -> None:
"""Update mean and stddev for descriptor."""
self.se_atten.mean = mean
self.se_atten.stddev = stddev
def get_stat_mean_and_stddev(self) -> tuple[Array, Array]:
"""Get mean and stddev for descriptor."""
return self.se_atten.mean, self.se_atten.stddev
def change_type_map(
self,
type_map: list[str],
model_with_new_type_stat: Optional["DescrptDPA1"] = None,
) -> None:
"""Change the type related params to new ones, according to `type_map` and the original one in the model.
If there are new types in `type_map`, statistics will be updated accordingly to `model_with_new_type_stat` for these new types.
"""
assert self.type_map is not None, (
"'type_map' must be defined when performing type changing!"
)
remap_index, has_new_type = get_index_between_two_maps(self.type_map, type_map)
obj = self.se_atten
obj.ntypes = len(type_map)
self.type_map = type_map
self.type_embedding.change_type_map(type_map=type_map)
obj.reinit_exclude(map_pair_exclude_types(obj.exclude_types, remap_index))
if has_new_type:
# the avg and std of new types need to be updated
extend_descrpt_stat(
obj,
type_map,
des_with_stat=model_with_new_type_stat.se_atten
if model_with_new_type_stat is not None
else None,
)
obj["davg"] = obj["davg"][remap_index]
obj["dstd"] = obj["dstd"][remap_index]
@cast_precision
def call(
self,
coord_ext: Array,
atype_ext: Array,
nlist: Array,
mapping: Array | None = None,
fparam: Array | None = None,
comm_dict: dict | None = None,
charge_spin: Array | None = None,
) -> Array:
"""Compute the descriptor.
Parameters
----------
coord_ext
The extended coordinates of atoms. shape: nf x (nallx3)
atype_ext
The extended aotm types. shape: nf x nall
nlist
The neighbor list. shape: nf x nloc x nnei
mapping
The index mapping from extended to local region. not used by this descriptor.
Returns
-------
descriptor
The descriptor. shape: nf x nloc x (ng x axis_neuron)
gr
The rotationally equivariant and permutationally invariant single particle
representation. shape: nf x nloc x ng x 3
g2
The rotationally invariant pair-partical representation.
this descriptor returns None
h2
The rotationally equivariant pair-partical representation.
this descriptor returns None
sw
The smooth switch function.
"""
xp = array_api_compat.array_namespace(coord_ext, atype_ext, nlist)
nloc = nlist.shape[1]
nall = xp.reshape(coord_ext, (nlist.shape[0], -1)).shape[1] // 3
# graph-eligible configs route through the graph-native adapter (decision
# #14: graph = single math source, dense call = thin adapter). Ineligible
# configs (strip tebd) and the ghost case with no mapping fall back to
# the legacy dense body. The graph needs `mapping` to fold ghosts to
# local owners; without it only nall == nloc is valid.
if self.uses_graph_lower() and (mapping is not None or nall == nloc):
return self._call_graph_adapter(coord_ext, atype_ext, nlist, mapping)
else:
return self._call_dense(coord_ext, atype_ext, nlist)
def _call_graph_adapter(
self,
coord_ext: Array,
atype_ext: Array,
nlist: Array,
mapping: Array | None,
) -> Array:
"""Regime-1 dense->graph adapter (the eligible ``call`` path).
Builds a NeighborGraph from the dense quartet with the SHAPE-STATIC
converter (``compact=False``, so this is jit/export-traceable -- no
``nonzero``), runs :meth:`call_graph`, and reconstructs the dense-shaped
``sw``. Preserves the dense 5-tuple ABI exactly; masked invalid edges
contribute zero in ``call_graph``'s ``segment_sum`` so the output is
identical to the legacy dense body.
Parameters
----------
coord_ext
The extended coordinates of atoms. shape: nf x (nall x 3)
atype_ext
The extended atom types. shape: nf x nall
nlist
The neighbor list. shape: nf x nloc x nnei
mapping
The index mapping from extended to local region. shape: nf x nall.
``None`` is allowed only when nall == nloc (identity mapping).
Returns
-------
descriptor
The descriptor. shape: nf x nloc x (ng x axis_neuron)
gr
The rotationally equivariant single-particle representation.
shape: nf x nloc x ng x 3
g2
``None`` for this descriptor.
h2
``None`` for this descriptor.
sw
The smooth switch function. shape: nf x nloc x nnei x 1
"""
from deepmd.dpmodel.utils.neighbor_graph import (
graph_from_dense_quartet,
)
xp = array_api_compat.array_namespace(coord_ext, atype_ext, nlist)
nf, nloc, nnei = nlist.shape
# shape-static graph + flat local center types from the dense quartet
# (shared with the input-stat graph path, see graph_from_dense_quartet).
graph, atype_local = graph_from_dense_quartet(
coord_ext, atype_ext, nlist, mapping
)
grrg_flat, rot_mat_flat = self.call_graph(
graph,
atype_local,
type_embedding=self.type_embedding.call(),
# the adapter graph is shape-static center-major (compact=False):
# keep the attention pair enumeration nonzero-free (traceable)
static_nnei=nnei,
)
# call_graph returns flat (N, ...) node axis; reshape to (nf, nloc, ...)
# for the dense 5-tuple ABI -- this reshape is LOCAL to the adapter shim.
grrg = xp.reshape(grrg_flat, (nf, nloc, *grrg_flat.shape[1:]))
rot_mat = xp.reshape(rot_mat_flat, (nf, nloc, *rot_mat_flat.shape[1:]))
# reconstruct the dense-shaped sw the dense way (env_mat switch masked
# where nlist == -1 OR the neighbor pair is type-excluded, matching
# DescrptBlockSeAtten.call which erases excluded nlist entries to -1
# before computing sw). A dense-layout artifact tied to neighbor slots,
# which the graph does not carry.
_, _, sw = self.se_atten.env_mat.call(
coord_ext,
atype_ext,
nlist,
self.se_atten.mean[...],
self.se_atten.stddev[...],
)
nlist_mask = (nlist != -1)[:, :, :, None]
sw = xp.where(nlist_mask, sw, xp.zeros_like(sw))
if self.se_atten.exclude_types:
# additionally mask excluded type-pairs (mirrors the block's nlist
# erasure: excluded entries become -1 there, so sw is 0 for them).
exc_mask = self.se_atten.emask.build_type_exclude_mask(nlist, atype_ext)
exc_mask = xp.astype(exc_mask[:, :, :, None], sw.dtype)
sw = sw * exc_mask
sw = xp.reshape(sw, (nf, nloc, nnei, 1))
return grrg, rot_mat, None, None, sw
def _call_dense(
self,
coord_ext: Array,
atype_ext: Array,
nlist: Array,
) -> Array:
"""Legacy dense descriptor body (the ineligible ``call`` path:
strip tebd or the no-mapping ghost case).
Parameters
----------
coord_ext
The extended coordinates of atoms. shape: nf x (nall x 3)
atype_ext
The extended atom types. shape: nf x nall
nlist
The neighbor list. shape: nf x nloc x nnei
Returns
-------
descriptor
The descriptor. shape: nf x nloc x (ng x axis_neuron)
gr
The rotationally equivariant single-particle representation.
shape: nf x nloc x ng x 3
g2
``None`` for this descriptor.
h2
``None`` for this descriptor.
sw
The smooth switch function. shape: nf x nloc x nnei x 1
"""
xp = array_api_compat.array_namespace(coord_ext, atype_ext, nlist)
nf, nloc = nlist.shape[:2]
nall = xp.reshape(coord_ext, (nf, -1)).shape[1] // 3
type_embedding = self.type_embedding.call()
# nf x nall x tebd_dim
atype_embd_ext = xp.reshape(
xp.take(type_embedding, xp.reshape(atype_ext, (-1,)), axis=0),
(nf, nall, self.tebd_dim),
)
# nfnl x tebd_dim
atype_embd = xp_take_first_n(atype_embd_ext, 1, nloc)
grrg, g2, h2, rot_mat, sw = self.se_atten(
nlist,
coord_ext,
atype_ext,
atype_embd_ext,
mapping=None,
type_embedding=type_embedding,
)
# nf x nloc x (ng x ng1 + tebd_dim)
if self.concat_output_tebd:
grrg = xp.concat(
[grrg, xp.reshape(atype_embd, (nf, nloc, self.tebd_dim))], axis=-1
)
return grrg, rot_mat, None, None, sw
def call_graph(
self,
graph: Any,
atype: Array,
type_embedding: Array | None = None,
static_nnei: int | None = None,
) -> tuple[Array, Array]:
"""Descriptor-level graph-native forward.
Wraps the block kernel
:meth:`DescrptBlockSeAtten.call_graph`, adds the descriptor-level
``concat_output_tebd`` step, and returns the outputs on the flat ``(N,
...)`` node axis (ragged-native; no rectangular ``(nf, nloc)``
reshape).
This method is graph-native: it takes no dense quartet inputs and does
not produce the dense ``sw`` (that lives in the dense :meth:`call`
adapter, which has the ``nlist``/``coord_ext`` needed to build it).
Notes
-----
**Smooth attention is intentionally sel-independent on the graph
path.** For ``smooth_type_embedding=True`` the legacy dense attention
keeps the sel-padding slots in its softmax DENOMINATOR (phantom
``exp(-attnw_shift)`` terms), which makes dense output depend on the
``sel`` setting by up to ~1e-4 even for identical physical neighbors.
A carry-all graph has no padding slots, so its softmax runs over the
real neighbor pairs only: cleaner, sel-independent semantics that
deliberately DIFFER from the dense route for smooth models. The two
routes agree bit-tight only for ``smooth_type_embedding=False`` (at
non-binding ``sel``), or when this kernel is realized on a dense
layout via ``static_nnei`` (the dense :meth:`call` adapter), which
reproduces the phantom terms for exact backward compatibility.
Parameters
----------
graph
A :class:`~deepmd.dpmodel.utils.neighbor_graph.NeighborGraph`.
atype
(N,) flat LOCAL atom types where ``N = sum(n_node)``.
type_embedding
(ntypes_with_padding, tebd_dim) type-embedding table.
Returns
-------
grrg : Array
(N, ng * axis_neuron [+ tebd_dim]) descriptor, flat node axis.
rot_mat : Array
(N, ng, 3) equivariant single-particle representation, flat node
axis.
"""
import dataclasses
xp = array_api_compat.array_namespace(graph.edge_vec)
dev = array_api_compat.device(graph.edge_vec)
# manual @cast_precision: the decorator casts array ARGUMENTS, but the
# graph's only float input (edge_vec) is inside the NeighborGraph
# dataclass, invisible to it. Cast edge_vec down to the descriptor
# precision on entry and the outputs back to the caller's dtype on
# exit (differentiable: grad still flows to the caller's edge_vec leaf).
in_dtype = graph.edge_vec.dtype
prec = get_xp_precision(xp, self.precision)
if in_dtype != prec:
graph = dataclasses.replace(graph, edge_vec=xp.astype(graph.edge_vec, prec))
grrg, rot_mat = self.se_atten.call_graph(
graph, atype, type_embedding=type_embedding, static_nnei=static_nnei
)
# FLAT node axis (N, ...): no (nf, nloc) reshape -- ragged-native, spec.
if self.concat_output_tebd:
# Use type_embedding directly (mirrors the dense path's
# ``xp.take(type_embedding, ...)``): ``xp.asarray(..., device=dev)``
# DETACHES under torch, silently severing the type-embedding weight
# gradient so the tebd net never trains; type_embedding already lives
# on the model device, so the device cast was redundant anyway.
atype_local = xp.asarray(atype, device=dev)
atype_embd = xp.take(type_embedding, atype_local, axis=0) # (N, tebd_dim)
grrg = xp.concat([grrg, atype_embd], axis=-1)
if in_dtype != prec:
grrg = xp.astype(grrg, in_dtype)
rot_mat = xp.astype(rot_mat, in_dtype)
return grrg, rot_mat
def enable_compression(
self,
min_nbor_dist: float,
table_extrapolate: float = 5,
table_stride_1: float = 0.01,
table_stride_2: float = 0.1,
check_frequency: int = -1,
) -> None:
"""Enable descriptor compression.
For DPA-1, compression is available for stripped type embeddings. The
type embedding branch is always precomputed; the radial embedding table
is enabled only when there is no attention layer, matching the PT/TF
compression semantics.
"""
if self.compress:
raise ValueError("Compression is already enabled.")
if self.se_atten.tebd_input_mode != "strip":
raise RuntimeError("Type embedding compression only works in strip mode")
if self.se_atten.resnet_dt:
raise RuntimeError(
"Model compression error: descriptor resnet_dt must be false!"
)
for tt in self.se_atten.exclude_types:
if (tt[0] not in range(self.se_atten.ntypes)) or (
tt[1] not in range(self.se_atten.ntypes)
):
raise RuntimeError(
"exclude types"
+ str(tt)
+ " must within the number of atomic types "
+ str(self.se_atten.ntypes)
+ "!"
)
if (
self.se_atten.ntypes * self.se_atten.ntypes
- len(self.se_atten.exclude_types)
== 0
):
raise RuntimeError(
"Empty embedding-nets are not supported in model compression!"
)
self.se_atten.type_embedding_compression(self.type_embedding)
self.type_embd_data = self.se_atten.type_embd_data
self.tebd_compress = True
self.compress = True
if self.se_atten.attn_layer == 0:
table = DPTabulate(
self,
self.se_atten.neuron,
self.se_atten.type_one_side,
self.se_atten.exclude_types,
self.se_atten.activation_function,
)
table_config = [
table_extrapolate,
table_stride_1,
table_stride_2,
check_frequency,
]
lower, upper = table.build(
min_nbor_dist, table_extrapolate, table_stride_1, table_stride_2
)
self.se_atten.enable_compression(
table.data,
table_config,
lower,
upper,
)
self.compress_data = self.se_atten.compress_data
self.compress_info = self.se_atten.compress_info
self.geo_compress = True
else:
self.geo_compress = False
warnings.warn(
"Attention layer is not 0, only type embedding is compressed. "
"Geometric part is not compressed.",
UserWarning,
stacklevel=2,
)
def serialize(self) -> dict:
"""Serialize the descriptor to dict."""
obj = self.se_atten
data = {
"@class": "Descriptor",
"type": "dpa1",
"@version": 3 if self.compress else 2,
"rcut": obj.rcut,
"rcut_smth": obj.rcut_smth,
"sel": obj.sel,
"ntypes": obj.ntypes,
"neuron": obj.neuron,
"axis_neuron": obj.axis_neuron,
"tebd_dim": obj.tebd_dim,
"tebd_input_mode": obj.tebd_input_mode,
"set_davg_zero": obj.set_davg_zero,
"attn": obj.attn,
"attn_layer": obj.attn_layer,
"attn_dotr": obj.attn_dotr,
"attn_mask": False,
"activation_function": obj.activation_function,
"resnet_dt": obj.resnet_dt,
"scaling_factor": obj.scaling_factor,
"normalize": obj.normalize,
"temperature": obj.temperature,
"trainable_ln": obj.trainable_ln,
"ln_eps": obj.ln_eps,
"smooth_type_embedding": obj.smooth,
"type_one_side": obj.type_one_side,
"concat_output_tebd": self.concat_output_tebd,
"use_econf_tebd": self.use_econf_tebd,
"use_tebd_bias": self.use_tebd_bias,
"type_map": self.type_map,
# make deterministic
"precision": np.dtype(PRECISION_DICT[obj.precision]).name,
"embeddings": obj.embeddings.serialize(),
"attention_layers": obj.dpa1_attention.serialize(),
"env_mat": obj.env_mat.serialize(),
"type_embedding": self.type_embedding.serialize(),
"exclude_types": obj.exclude_types,
"env_protection": obj.env_protection,
"@variables": {
"davg": to_numpy_array(obj["davg"]),
"dstd": to_numpy_array(obj["dstd"]),
},
## to be updated when the options are supported.
"trainable": self.trainable,
"spin": None,
}
if obj.tebd_input_mode in ["strip"]:
data.update({"embeddings_strip": obj.embeddings_strip.serialize()})
if self.compress:
type_embd_data = (
self.type_embd_data
if hasattr(self, "type_embd_data")
else obj.type_embd_data
)
compress_dict: dict = {
"@variables": {
"type_embd_data": to_numpy_array(type_embd_data),
},
"geo_compress": self.geo_compress,
}
if self.geo_compress:
compress_data = (
self.compress_data
if hasattr(self, "compress_data")
else obj.compress_data
)
compress_info = (
self.compress_info
if hasattr(self, "compress_info")
else obj.compress_info
)
compress_dict["@variables"]["compress_data"] = [
to_numpy_array(d) for d in compress_data
]
compress_dict["@variables"]["compress_info"] = [
to_numpy_array(i) for i in compress_info
]
data["compress"] = compress_dict
return data
@classmethod
def deserialize(cls, data: dict) -> "DescrptDPA1":
"""Deserialize from dict."""
data = data.copy()