|
1 | 1 | # SPDX-License-Identifier: LGPL-3.0-or-later |
| 2 | +import contextlib |
2 | 3 | import ctypes |
3 | 4 | import json |
4 | 5 | import logging |
| 6 | +import os |
| 7 | +from collections.abc import ( |
| 8 | + Iterator, |
| 9 | +) |
5 | 10 | from typing import ( |
6 | 11 | Any, |
7 | 12 | ) |
@@ -807,6 +812,56 @@ def _serialize_from_file_pt2(model_file: str) -> dict: |
807 | 812 | return model_dict |
808 | 813 |
|
809 | 814 |
|
| 815 | +@contextlib.contextmanager |
| 816 | +def _cuda_infer_at_least_2() -> Iterator[None]: |
| 817 | + """Pin ``DP_CUDA_INFER`` to at least 2 for the duration of a trace. |
| 818 | +
|
| 819 | + Level 2 bakes the end-to-end fused operator, which stays opaque through |
| 820 | + ``torch.export``; the level-1 autograd lower is decomposed to aten (losing |
| 821 | + the hand-tuned kernel), and level 0 selects the untraceable reference |
| 822 | + tabulation. Level 2 degrades to level 1 internally when the fused operator |
| 823 | + is unavailable or ineligible, so it is a safe floor for any graph export. |
| 824 | + """ |
| 825 | + from deepmd.kernels.utils import ( |
| 826 | + cuda_infer_level, |
| 827 | + ) |
| 828 | + |
| 829 | + saved = os.environ.get("DP_CUDA_INFER") |
| 830 | + if cuda_infer_level() < 2: |
| 831 | + os.environ["DP_CUDA_INFER"] = "2" |
| 832 | + try: |
| 833 | + yield |
| 834 | + finally: |
| 835 | + if saved is None: |
| 836 | + os.environ.pop("DP_CUDA_INFER", None) |
| 837 | + else: |
| 838 | + os.environ["DP_CUDA_INFER"] = saved |
| 839 | + |
| 840 | + |
| 841 | +def _resolve_lower_kind(model_file: str, data: dict, lower_kind: str) -> str: |
| 842 | + """Resolve ``lower_kind="auto"`` to a concrete lower-forward schema. |
| 843 | +
|
| 844 | + ``"auto"`` selects the graph lower for a graph-lower-eligible model exported |
| 845 | + to ``.pt2`` -- the deploy-optimal path, whose fused mega operator the graph |
| 846 | + export bakes -- and the dense nlist lower for everything else (a ``.pte`` |
| 847 | + target, a spin model, or a descriptor without a graph lower). An explicit |
| 848 | + ``"nlist"`` / ``"graph"`` is returned unchanged. |
| 849 | + """ |
| 850 | + if lower_kind != "auto": |
| 851 | + return lower_kind |
| 852 | + if not model_file.endswith(".pt2") or data["model"].get("type") == "spin_ener": |
| 853 | + return "nlist" |
| 854 | + from deepmd.pt_expt.model.model import ( |
| 855 | + BaseModel, |
| 856 | + ) |
| 857 | + from deepmd.pt_expt.train.training import ( |
| 858 | + _model_uses_graph_lower, |
| 859 | + ) |
| 860 | + |
| 861 | + model = BaseModel.deserialize(data["model"]) |
| 862 | + return "graph" if _model_uses_graph_lower(model) else "nlist" |
| 863 | + |
| 864 | + |
810 | 865 | def deserialize_to_file( |
811 | 866 | model_file: str, |
812 | 867 | data: dict, |
@@ -835,25 +890,42 @@ def deserialize_to_file( |
835 | 890 | tracing the uncompressed model (make_fx cannot trace custom ops). |
836 | 891 | do_atomic_virial : bool |
837 | 892 | If True, export with per-atom virial correction (3 extra backward |
838 | | - passes, ~2.5x slower). Default False for best performance. |
| 893 | + passes, ~2.5x slower). Default False for best performance. Forced True |
| 894 | + for a graph lower, whose LAMMPS Kokkos consumer always reads it. |
839 | 895 | lower_kind : str |
840 | 896 | Which lower-forward schema the compiled AOTI graph consumes: |
841 | 897 | ``"nlist"`` (default) traces the dense quartet |
842 | 898 | (``extended_coord``/``extended_atype``/``nlist``/``mapping``); |
843 | 899 | ``"graph"`` traces the NeighborGraph schema |
844 | 900 | (``atype``/``n_node``/``edge_index``/``edge_vec``/``edge_mask``) with a |
845 | 901 | DYNAMIC edge axis ``E`` (``Dim("nedge", min=2)``), so the artifact |
846 | | - accepts any system size. The selected schema is recorded as |
847 | | - ``lower_input_kind`` in ``metadata.json``. |
| 902 | + accepts any system size. ``"auto"`` (used by ``convert-backend``) |
| 903 | + resolves to ``"graph"`` for a graph-eligible ``.pt2`` and ``"nlist"`` |
| 904 | + otherwise (see :func:`_resolve_lower_kind`). A graph lower always bakes |
| 905 | + the fused mega operator (``DP_CUDA_INFER >= 2``) and the per-atom virial. |
| 906 | + The selected schema is recorded as ``lower_input_kind`` in |
| 907 | + ``metadata.json``. |
848 | 908 | """ |
849 | | - if model_file.endswith(".pt2"): |
850 | | - _deserialize_to_file_pt2( |
851 | | - model_file, data, model_json_override, do_atomic_virial, lower_kind |
852 | | - ) |
| 909 | + lower_kind = _resolve_lower_kind(model_file, data, lower_kind) |
| 910 | + # A graph lower deploys the fused mega operator: the trace must run at |
| 911 | + # DP_CUDA_INFER >= 2 (level 1 decomposes to aten under torch.export), and the |
| 912 | + # per-atom virial is mandatory for its LAMMPS Kokkos consumer. Enforcing both |
| 913 | + # here gives every producer -- freeze, compress, convert-backend -- the same |
| 914 | + # deployable artifact. |
| 915 | + if lower_kind == "graph": |
| 916 | + do_atomic_virial = True |
| 917 | + ctx: contextlib.AbstractContextManager = _cuda_infer_at_least_2() |
853 | 918 | else: |
854 | | - _deserialize_to_file_pte( |
855 | | - model_file, data, model_json_override, do_atomic_virial, lower_kind |
856 | | - ) |
| 919 | + ctx = contextlib.nullcontext() |
| 920 | + with ctx: |
| 921 | + if model_file.endswith(".pt2"): |
| 922 | + _deserialize_to_file_pt2( |
| 923 | + model_file, data, model_json_override, do_atomic_virial, lower_kind |
| 924 | + ) |
| 925 | + else: |
| 926 | + _deserialize_to_file_pte( |
| 927 | + model_file, data, model_json_override, do_atomic_virial, lower_kind |
| 928 | + ) |
857 | 929 |
|
858 | 930 |
|
859 | 931 | def _trace_and_export( |
|
0 commit comments