Skip to content
28 changes: 14 additions & 14 deletions deepmd/pt/optimizer/hybrid_muon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
Routing is controlled by parameter dimensionality, parameter names, and
``muon_mode``:

- Parameters whose final effective name segment contains ``bias``
(case-insensitive), or starts with ``adam_`` (case-insensitive): Adam.
- Parameters whose final effective name segment is ``b``, contains ``bias``,
or starts with ``adam_`` (case-insensitive): Adam.
- Parameters whose final effective name segment starts with ``adamw_``
(case-insensitive): Adam with decoupled weight decay (AdamW-style).
The final effective segment means the last non-numeric segment in the full
Expand Down Expand Up @@ -715,7 +715,7 @@ def get_adam_route(
effective name segment after stripping trailing numeric ParameterList
indices):

1. Contains ``"bias"`` -> ``"adam"`` (no weight decay).
1. Is ``"b"`` or contains ``"bias"`` -> ``"adam"`` (no weight decay).
2. Starts with ``"adam_"`` -> ``"adam"`` (no weight decay).
Typical: norm scales, radial frequencies.
3. Starts with ``"adamw_"`` -> ``"adamw"`` (decoupled weight decay).
Expand All @@ -730,7 +730,7 @@ def get_adam_route(
while leaf_name_idx > 0 and name_segments[leaf_name_idx].isdigit():
leaf_name_idx -= 1
leaf_name = name_segments[leaf_name_idx]
if "bias" in leaf_name:
if leaf_name == "b" or "bias" in leaf_name:
return "adam"
if leaf_name.startswith("adam_"):
return "adam"
Expand Down Expand Up @@ -808,9 +808,9 @@ class HybridMuonOptimizer(Optimizer):

This optimizer applies different update rules based on parameter dimensionality,
parameter names, and ``muon_mode``:
- Parameters with final effective name segment containing ``bias``
(case-insensitive), or starting with ``adam_`` (case-insensitive):
standard Adam update.
- Parameters with final effective name segment equal to ``b``, containing
``bias``, or starting with ``adam_`` (case-insensitive): standard Adam
update.
- Parameters with final effective name segment starting with ``adamw_``
(case-insensitive): Adam with decoupled weight decay (AdamW-style).
- 1D parameters: standard Adam update.
Expand All @@ -826,8 +826,8 @@ class HybridMuonOptimizer(Optimizer):
``(m, n)`` slice.

Naming convention for explicit Adam routing:
- Parameters representing bias terms should include ``bias`` in their
final effective name segment (case-insensitive).
- Parameters representing bias terms should use ``b`` or include ``bias``
in their final effective name segment (case-insensitive).
- Parameters that are not semantic bias but should still use Adam should
use an ``adam_`` prefix in their final effective name segment
(case-insensitive).
Expand Down Expand Up @@ -887,10 +887,10 @@ class HybridMuonOptimizer(Optimizer):
- ``"slice"``: >=3D parameters use per-slice Muon routing on last two dims.
named_parameters : iterable[tuple[str, torch.Tensor]] | None
Optional named parameter iterable used for name-based routing.
Parameters with final effective name segment containing ``bias``
(case-insensitive), or starting with ``adam_`` (case-insensitive),
are forced to Adam (no weight decay). Parameters starting with
``adamw_`` are forced to AdamW-style decoupled decay path.
Parameters with final effective name segment equal to ``b``, containing
``bias``, or starting with ``adam_`` (case-insensitive) are forced to
Adam (no weight decay). Parameters starting with ``adamw_`` are forced
to AdamW-style decoupled decay path.
enable_gram : bool
Enable the compiled Gram Newton-Schulz path for rectangular Muon
matrices. Square matrices continue to use the current standard
Expand Down Expand Up @@ -1502,7 +1502,7 @@ def _build_param_routing(self) -> None:
Classify parameters into Muon, Adam, and AdamW routes (static routing).

Routing logic:
- name-based ``adam_`` prefix or contains ``bias`` → Adam (no decay)
- name-based ``b``, ``bias``, or ``adam_`` route → Adam (no decay)
- name-based ``adamw_`` prefix → AdamW (decoupled weight decay)
- effective shape rank <2 → Adam (no decay)
- non-matrix effective shape for current muon_mode → AdamW (decoupled)
Expand Down
59 changes: 6 additions & 53 deletions deepmd/pt_expt/model/make_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
from deepmd.pt_expt.common import (
torch_module,
)
from deepmd.pt_expt.utils.graph_builder import (
build_neighbor_graph_for_method,
)
from deepmd.pt_expt.utils.graph_csr import (
validate_graph_csr_for_export,
)
Expand Down Expand Up @@ -249,56 +252,6 @@ def __call__(self, coord_flat: torch.Tensor) -> torch.Tensor:
return energy_redu


def _build_graph_for_method(
method: str,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None,
rcut: float,
pair_excl: Any,
with_csr: bool = False,
) -> Any:
"""Build a carry-all ``NeighborGraph`` for the named pt_expt builder.

Single owning site for the graph-builder dispatch shared by
:meth:`_call_common_graph` and the graph Hessian wrapper
(:class:`_WrapperForwardEnergyGraph`), so both build the graph identically.
"""
from deepmd.dpmodel.utils.neighbor_graph import (
build_neighbor_graph,
build_neighbor_graph_ase,
)

if method == "dense":
return build_neighbor_graph(
coord, atype, box, rcut, with_csr=with_csr, pair_excl=pair_excl
)
if method == "ase":
return build_neighbor_graph_ase(
coord, atype, box, rcut, with_csr=with_csr, pair_excl=pair_excl
)
if method == "vesin":
from deepmd.pt_expt.utils.vesin_graph_builder import (
build_neighbor_graph_vesin,
)

return build_neighbor_graph_vesin(
coord, atype, box, rcut, with_csr=with_csr, pair_excl=pair_excl
)
if method == "nv":
from deepmd.pt_expt.utils.nv_graph_builder import (
build_neighbor_graph_nv,
)

return build_neighbor_graph_nv(
coord, atype, box, rcut, with_csr=with_csr, pair_excl=pair_excl
)
raise ValueError(
f"unknown neighbor_graph_method {method!r}; use 'dense', 'ase', "
"'vesin', or 'nv'"
)


class _WrapperForwardEnergyGraph:
"""Graph twin of :class:`_WrapperForwardEnergy` for the Hessian.

Expand Down Expand Up @@ -339,7 +292,7 @@ def __init__(

def __call__(self, coord_flat: torch.Tensor) -> torch.Tensor:
cc = coord_flat.reshape(1, self.nloc, 3)
ng = _build_graph_for_method(
ng = build_neighbor_graph_for_method(
self.method, cc, self.atype, self.box, self.rcut, self.pair_excl
)
atomic_ret = self.model.atomic_model.forward_common_atomic_graph(
Expand Down Expand Up @@ -719,7 +672,7 @@ def _resolve_graph_method(
if "energy" not in self.atomic_output_def().keys():
return None
if self.mixed_types() and self.atomic_model.uses_graph_lower():
return "dense"
return getattr(self, "neighbor_graph_method", "dense")
Comment thread
OutisLi marked this conversation as resolved.
return None

def _call_common_graph(
Expand Down Expand Up @@ -795,7 +748,7 @@ def _call_common_graph(
and bool(getattr(_desc, "geo_compress", False))
)
pair_excl = getattr(self.atomic_model, "pair_excl", None)
ng = _build_graph_for_method(
ng = build_neighbor_graph_for_method(
method, cc, atype, bb, rcut, pair_excl, with_csr=with_csr
)
nf, nloc = atype.shape[:2]
Expand Down
99 changes: 80 additions & 19 deletions deepmd/pt_expt/train/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,20 @@
from deepmd.dpmodel.utils.learning_rate import (
make_learning_rate_schedule,
)
from deepmd.pt.optimizer import (
HybridMuonOptimizer,
)
from deepmd.pt.train.utils import (
resolve_best_checkpoint_dir,
)
from deepmd.pt.train.validation import (
FullValidator,
resolve_full_validation_start_step,
)
from deepmd.pt.utils.compile_compat import (
apply_global_compile_patches,
build_inductor_compile_options,
)
from deepmd.pt.utils.compile_compat import next_safe_prime as _next_safe_prime
from deepmd.pt.utils.compile_compat import rebuild_graph_module as _rebuild_graph_module
from deepmd.pt.utils.compile_compat import (
Expand Down Expand Up @@ -93,6 +100,10 @@

log = logging.getLogger(__name__)

# Apply the shared process-global compiler workarounds before any pt_expt
# training graph reaches Dynamo or Inductor.
apply_global_compile_patches()
Comment thread
OutisLi marked this conversation as resolved.
Outdated

# Buffer names in atomic_model that are per-task (energy/output statistics).
# These live one level above the fitting net and are not reached by
# fitting-net share_params. They are always promoted to FX placeholders
Expand Down Expand Up @@ -572,19 +583,8 @@ def _finalize_compiled_lower(
if not was_training:
model.eval()

# Inductor defaults tuned for second-order-gradient training graphs.
# User-supplied compile_opts override these on a per-key basis.
inductor_options: dict[str, Any] = {
"max_autotune": False,
"shape_padding": True,
"epilogue_fusion": False,
"triton.cudagraphs": False,
"max_fusion_size": 8,
# NOTE: On GPU with PyTorch <=2.11, consider adding
# "triton.mix_order_reduction": False to work around
# pytorch/pytorch#174379, #178080, #179494 under
# data-dependent symbolic shapes.
}
# Keep pt_expt training on the same compiler contract as the PT SeZM path.
inductor_options = build_inductor_compile_options(inference=False)
if extra_options:
inductor_options.update(extra_options)
if compile_opts:
Expand Down Expand Up @@ -1146,8 +1146,8 @@ def _forward_graph(
so no extended->local scatter is needed; only the flat ``(N, *)`` node
keys are unravelled to ``(nf, nloc, *)`` at the I/O boundary.
"""
from deepmd.dpmodel.utils.neighbor_graph import (
build_neighbor_graph,
from deepmd.pt_expt.utils.graph_builder import (
build_neighbor_graph_for_method,
)

_model = self.original_model
Expand Down Expand Up @@ -1198,7 +1198,14 @@ def _forward_graph(
# into edge_mask here so the compiled lower consumes a pre-excluded graph
# (the lower no longer re-applies it), matching the eager path exactly.
pair_excl = getattr(_model.atomic_model, "pair_excl", None)
ng = build_neighbor_graph(coord_3d, atype, box_flat, rcut, pair_excl=pair_excl)
ng = build_neighbor_graph_for_method(
getattr(_model, "neighbor_graph_method", "dense"),
coord_3d,
atype,
box_flat,
rcut,
pair_excl,
)
atype_flat = atype.reshape(nframes * nloc)

# Lazy compile of the GRAPH lower (cached per structure key).
Expand Down Expand Up @@ -1352,6 +1359,7 @@ def __init__(

model_params = config["model"]
training_params = config["training"]
optimizer_params = config.get("optimizer", {})
validating_params = config.get("validating", {}) or {}

# Task normalization --------------------------------------------------
Expand Down Expand Up @@ -1576,22 +1584,48 @@ def _make_sample(
)

# Optimiser -----------------------------------------------------------
opt_type = training_params.get("opt_type", "Adam")
opt_type = optimizer_params.get("type", "Adam")
# LambdaLR multiplies each param group's initial learning rate by the
# lambda value. Warmup schedules legitimately return zero at step 0,
# so use the nonzero schedule base as the denominator and let the
# lambda initialize the optimizer to the requested warmup value.
initial_lr = float(self.lr_schedule.start_lr)
adam_betas = (
float(optimizer_params["adam_beta1"]),
Comment thread
OutisLi marked this conversation as resolved.
float(optimizer_params["adam_beta2"]),
)
weight_decay = float(optimizer_params["weight_decay"])

if opt_type == "Adam":
self.optimizer = torch.optim.Adam(self.wrapper.parameters(), lr=initial_lr)
self.optimizer = torch.optim.Adam(
self.wrapper.parameters(),
lr=initial_lr,
betas=adam_betas,
weight_decay=weight_decay,
)
elif opt_type == "AdamW":
weight_decay = training_params.get("weight_decay", 0.001)
self.optimizer = torch.optim.AdamW(
self.wrapper.parameters(),
lr=initial_lr,
betas=adam_betas,
weight_decay=weight_decay,
)
elif opt_type == "HybridMuon":
Comment thread
OutisLi marked this conversation as resolved.
Outdated
runtime_named_parameters = tuple(self.wrapper.named_parameters())
self.optimizer = HybridMuonOptimizer(
self.wrapper.parameters(),
lr=initial_lr,
momentum=float(optimizer_params["momentum"]),
weight_decay=weight_decay,
adam_betas=adam_betas,
lr_adjust=float(optimizer_params["lr_adjust"]),
lr_adjust_coeff=float(optimizer_params["lr_adjust_coeff"]),
muon_mode=str(optimizer_params["muon_mode"]),
named_parameters=runtime_named_parameters,
enable_gram=bool(optimizer_params["enable_gram"]),
flash_muon=bool(optimizer_params["flash_muon"]),
magma_muon=bool(optimizer_params["magma_muon"]),
)
else:
raise ValueError(f"Unsupported optimizer type: {opt_type}")

Expand Down Expand Up @@ -1793,6 +1827,10 @@ def _make_sample(
last_epoch=self.start_step - 1,
)

self._configure_neighbor_graph_method(
training_params.get("neighbor_graph_method", "auto")
)

# torch.compile -------------------------------------------------------
self.enable_compile = training_params.get("enable_compile", False)
if self.enable_compile:
Expand Down Expand Up @@ -1891,6 +1929,29 @@ def _raise_if_full_validation_unsupported(
# torch.compile helpers
# ------------------------------------------------------------------

def _configure_neighbor_graph_method(self, requested: str) -> None:
"""Resolve and install the training graph builder on eligible models."""
graph_models = [
self.models[model_key]
for model_key in self.model_keys
if model_uses_graph_lower(self.models[model_key])
]
if not graph_models:
if requested != "auto":
raise ValueError(
"training.neighbor_graph_method applies only to "
"graph-eligible energy models"
)
return

from deepmd.pt_expt.utils.graph_builder import (
resolve_neighbor_graph_method,
)

resolved = resolve_neighbor_graph_method(requested, DEVICE)
for model in graph_models:
model.neighbor_graph_method = resolved

def _compile_model(self, compile_opts: dict[str, Any]) -> None:
"""Replace ``self.model`` with a compiled version.

Expand Down
Loading
Loading