From 8f554d94b0ad88a509cf4ebbc68ed17cd63882f2 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:16:38 +0800 Subject: [PATCH 01/22] feat(pt dpmodel): add lmdb dataloader --- deepmd/dpmodel/utils/__init__.py | 10 + deepmd/dpmodel/utils/lmdb_data.py | 788 ++++++++++++++++++++++ deepmd/entrypoints/test.py | 168 +++-- deepmd/pt/entrypoints/main.py | 80 ++- deepmd/pt/train/training.py | 100 ++- deepmd/pt/utils/lmdb_dataset.py | 229 +++++++ examples/lmdb_data/input_lmdb.json | 65 ++ source/tests/consistent/test_lmdb_data.py | 509 ++++++++++++++ source/tests/pt/test_lmdb_dataloader.py | 464 +++++++++++++ 9 files changed, 2317 insertions(+), 96 deletions(-) create mode 100644 deepmd/dpmodel/utils/lmdb_data.py create mode 100644 deepmd/pt/utils/lmdb_dataset.py create mode 100644 examples/lmdb_data/input_lmdb.json create mode 100644 source/tests/consistent/test_lmdb_data.py create mode 100644 source/tests/pt/test_lmdb_dataloader.py diff --git a/deepmd/dpmodel/utils/__init__.py b/deepmd/dpmodel/utils/__init__.py index cd6eb696c9..93a9ea520c 100644 --- a/deepmd/dpmodel/utils/__init__.py +++ b/deepmd/dpmodel/utils/__init__.py @@ -6,6 +6,12 @@ AtomExcludeMask, PairExcludeMask, ) +from .lmdb_data import ( + LmdbDataReader, + LmdbTestData, + SameNlocBatchSampler, + is_lmdb, +) from .network import ( EmbeddingNet, FittingNet, @@ -47,10 +53,13 @@ "EmbeddingNet", "EnvMat", "FittingNet", + "LmdbDataReader", + "LmdbTestData", "NativeLayer", "NativeNet", "NetworkCollection", "PairExcludeMask", + "SameNlocBatchSampler", "aggregate", "build_multiple_neighbor_list", "build_neighbor_list", @@ -59,6 +68,7 @@ "get_graph_index", "get_multiple_nlist_key", "inter2phys", + "is_lmdb", "load_dp_model", "make_embedding_network", "make_fitting_network", diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py new file mode 100644 index 0000000000..327e260326 --- /dev/null +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -0,0 +1,788 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Framework-agnostic LMDB data utilities for DeePMD-kit. + +All code here is pure Python/NumPy/lmdb/msgpack — no framework dependency. +Backend-specific wrappers (PyTorch Dataset, JAX, etc.) import from here. +""" + +import logging +from collections.abc import ( + Iterator, +) +from pathlib import ( + Path, +) +from typing import ( + Any, +) + +import lmdb +import msgpack +import numpy as np + +from deepmd.utils.data import ( + DataRequirementItem, +) + +log = logging.getLogger(__name__) + +# LMDB key → DeePMD convention +_KEY_REMAP = { + "coords": "coord", + "cells": "box", + "energies": "energy", + "forces": "force", + "atom_types": "atype", +} + + +def _open_lmdb(path: str) -> lmdb.Environment: + """Open LMDB environment readonly.""" + return lmdb.open(path, readonly=True, lock=False, readahead=False, meminit=False) + + +def _read_metadata(txn: lmdb.Transaction) -> dict: + """Read and decode __metadata__ from LMDB transaction.""" + raw = txn.get(b"__metadata__") + if raw is None: + raise ValueError("LMDB file missing __metadata__ key") + return msgpack.unpackb(raw, raw=False) + + +def _decode_array(obj: dict) -> np.ndarray: + """Reconstruct ndarray from msgpack-encoded dict with {type, shape, data}. + + Handles both string keys ("type", "data") and byte keys (b"type", b"data"). + """ + dtype_key = "type" if "type" in obj else b"type" + data_key = "data" if "data" in obj else b"data" + shape_key = "shape" if "shape" in obj else b"shape" + dtype = np.dtype(obj[dtype_key]) + data = obj[data_key] + if shape_key in obj: + shape = tuple(obj[shape_key]) + else: + shape = (len(data) // dtype.itemsize,) + return np.frombuffer(data, dtype=dtype).reshape(shape).copy() + + +def _is_encoded_array(val: Any) -> bool: + """Check if a value is a msgpack-encoded ndarray dict.""" + if not isinstance(val, dict): + return False + return ("data" in val and "type" in val) or (b"data" in val and b"type" in val) + + +def _decode_value(val: Any) -> Any: + """Decode a value: encoded array -> ndarray, list of encoded -> list of ndarray, else pass through.""" + if _is_encoded_array(val): + return _decode_array(val) + elif isinstance(val, list) and len(val) > 0 and _is_encoded_array(val[0]): + return [_decode_array(item) for item in val] + return val + + +def _decode_frame(raw_bytes: bytes) -> dict[str, Any]: + """Decode a msgpack-serialized frame into a dict of numpy arrays / scalars.""" + frame = msgpack.unpackb(raw_bytes, raw=False) + result = {} + for key, val in frame.items(): + result[key] = _decode_value(val) + return result + + +def _remap_keys(frame: dict[str, Any]) -> dict[str, Any]: + """Remap LMDB key names to DeePMD convention, pass through unknown keys.""" + out = {} + for k, v in frame.items(): + out[_KEY_REMAP.get(k, k)] = v + return out + + +def is_lmdb(systems: Any) -> bool: + """Check if systems points to an LMDB dataset.""" + if not isinstance(systems, str): + return False + return systems.endswith(".lmdb") or Path(systems, "data.mdb").exists() + + +def _parse_metadata(meta: dict) -> tuple[int, str, list[int]]: + """Parse LMDB metadata into (nframes, frame_fmt, natoms_per_type). + + Handles system_info as list or dict, and natoms as plain ints or encoded arrays. + """ + nframes = meta["nframes"] + frame_fmt = meta.get("frame_idx_fmt", "012d") + raw_sys_info = meta.get("system_info", {}) + + if isinstance(raw_sys_info, list): + sys_info = raw_sys_info[0] if raw_sys_info else {} + else: + sys_info = raw_sys_info + + raw_natoms = sys_info.get("natoms", []) + natoms_per_type = [] + for item in raw_natoms: + if _is_encoded_array(item): + natoms_per_type.append(int(_decode_array(item).item())) + else: + natoms_per_type.append(int(item)) + + return nframes, frame_fmt, natoms_per_type + + +def _scan_frame_nlocs( + env: lmdb.Environment, nframes: int, frame_fmt: str, fallback_natoms: int +) -> list[int]: + """Scan all frames to get per-frame atom count. + + Reads only the atom_types shape from msgpack without decoding array data. + """ + nlocs = [] + with env.begin() as txn: + for i in range(nframes): + key = format(i, frame_fmt).encode() + raw = txn.get(key) + if raw is not None: + frame_raw = msgpack.unpackb(raw, raw=False) + atype_raw = frame_raw.get("atom_types") + if isinstance(atype_raw, dict): + shape = atype_raw.get("shape") or atype_raw.get(b"shape") + if shape: + nlocs.append(int(shape[0])) + continue + nlocs.append(fallback_natoms) + return nlocs + + +def _compute_batch_size(nloc: int, rule: int) -> int: + """Compute batch_size for a given nloc using the auto rule.""" + bsi = rule // max(nloc, 1) + if bsi * nloc < rule: + bsi += 1 + return max(bsi, 1) + + +class LmdbDataReader: + """Framework-agnostic LMDB dataset reader. + + Reads LMDB frames and returns dicts of numpy arrays. + Backend-specific Dataset classes (PyTorch, JAX, etc.) wrap this. + + Datasets are typically mixed-nloc (frames with different atom counts). + The ``mixed_batch`` flag controls batching strategy: + + - ``mixed_batch=False`` (default, old format): each batch contains only + frames with the same nloc. A ``SameNlocBatchSampler`` groups frames + by nloc and yields same-nloc batches. Auto batch_size is computed + per-nloc-group. + - ``mixed_batch=True`` (new format): frames with different nloc can + coexist in one batch (requires padding + mask in collate_fn). + Currently raises ``NotImplementedError`` at collation time. + + Parameters + ---------- + lmdb_path : str + Path to the LMDB directory. + type_map : list[str] + Global type map from model config. + batch_size : int or str + Batch size. Supports int, "auto", "auto:N". + mixed_batch : bool + If True, allow different nloc in the same batch (future). + If False (default), enforce same-nloc-per-batch. + """ + + def __init__( + self, + lmdb_path: str, + type_map: list[str], + batch_size: int | str = "auto", + mixed_batch: bool = False, + ) -> None: + self.lmdb_path = str(lmdb_path) + self._type_map = type_map + self._env = _open_lmdb(self.lmdb_path) + self.mixed_batch = mixed_batch + + with self._env.begin() as txn: + meta = _read_metadata(txn) + + self.nframes, self._frame_fmt, self._natoms_per_type = _parse_metadata(meta) + self._natoms = sum(self._natoms_per_type) + self._ntypes = len(type_map) + + # Persistent read-only transaction for __getitem__ (avoids per-read overhead). + # Safe because we use num_workers=0 in DataLoader. + self._txn = self._env.begin() + + # Scan per-frame nloc only when needed for same-nloc batching. + # For mixed_batch=True, skip the scan entirely (future: padding handles it). + if not mixed_batch: + # Fast path: use pre-computed frame_nlocs from metadata if available. + # Falls back to scanning each frame's atom_types shape (~10 us/frame). + meta_nlocs = meta.get("frame_nlocs") + if meta_nlocs is not None: + self._frame_nlocs = [int(n) for n in meta_nlocs] + else: + self._frame_nlocs = _scan_frame_nlocs( + self._env, self.nframes, self._frame_fmt, self._natoms + ) + self._nloc_groups: dict[int, list[int]] = {} + for idx, nloc in enumerate(self._frame_nlocs): + self._nloc_groups.setdefault(nloc, []).append(idx) + else: + self._frame_nlocs = [] + self._nloc_groups = {} + + # Parse batch_size spec + self._auto_rule: int | None = None + if isinstance(batch_size, str): + if batch_size == "auto": + self._auto_rule = 32 + elif batch_size.startswith("auto:"): + self._auto_rule = int(batch_size.split(":")[1]) + else: + self._auto_rule = 32 + # Default batch_size uses first frame's nloc (for total_batch estimate) + self.batch_size = _compute_batch_size(self._natoms, self._auto_rule) + else: + self.batch_size = int(batch_size) + + # Data requirements tracking + self._data_requirements: dict[str, DataRequirementItem] = {} + + def _compute_natoms_vec(self, atype: np.ndarray) -> np.ndarray: + """Compute natoms_vec from a frame's atype array. + + Returns [nloc, nloc, count_type0, count_type1, ...] with length ntypes+2. + """ + nloc = len(atype) + counts = np.bincount(atype, minlength=self._ntypes)[: self._ntypes] + vec = np.empty(self._ntypes + 2, dtype=np.int64) + vec[0] = nloc + vec[1] = nloc + vec[2:] = counts + return vec + + def get_batch_size_for_nloc(self, nloc: int) -> int: + """Get batch_size for a given nloc. Uses auto rule if configured.""" + if self._auto_rule is not None: + return _compute_batch_size(nloc, self._auto_rule) + return self.batch_size + + def __len__(self) -> int: + return self.nframes + + def __getitem__(self, index: int) -> dict[str, Any]: + """Read frame from LMDB, decode, remap keys, return dict of numpy arrays.""" + key = format(index, self._frame_fmt).encode() + raw = self._txn.get(key) + if raw is None: + raise IndexError(f"Frame {index} not found in LMDB") + frame = _decode_frame(raw) + frame = _remap_keys(frame) + + # Remove LMDB-specific metadata keys not needed by trainer + for meta_key in ("atom_numbs", "atom_names", "orig"): + frame.pop(meta_key, None) + + # Flatten arrays to match DeePMD convention + if "coord" in frame and isinstance(frame["coord"], np.ndarray): + frame["coord"] = frame["coord"].reshape(-1, 3).astype(np.float64) + if "box" in frame and isinstance(frame["box"], np.ndarray): + frame["box"] = frame["box"].reshape(9).astype(np.float64) + if "energy" in frame: + val = frame["energy"] + if isinstance(val, np.ndarray): + frame["energy"] = val.reshape(1).astype(np.float64) + else: + frame["energy"] = np.array([float(val)], dtype=np.float64) + if "force" in frame and isinstance(frame["force"], np.ndarray): + frame["force"] = frame["force"].reshape(-1, 3).astype(np.float64) + if "atype" in frame and isinstance(frame["atype"], np.ndarray): + frame["atype"] = frame["atype"].reshape(-1).astype(np.int64) + if "virial" in frame and isinstance(frame["virial"], np.ndarray): + frame["virial"] = frame["virial"].reshape(9).astype(np.float64) + + # Per-frame natoms_vec from atype + atype = frame.get("atype") + if atype is not None: + frame_natoms = len(atype) + natoms_vec = self._compute_natoms_vec(atype) + frame["natoms"] = natoms_vec + frame["real_natoms_vec"] = natoms_vec + else: + frame_natoms = self._natoms + fallback = np.array( + [self._natoms, self._natoms] + [0] * self._ntypes, dtype=np.int64 + ) + frame["natoms"] = fallback + frame["real_natoms_vec"] = fallback + + # Add find_* flags for known label keys + label_keys = [ + "energy", + "force", + "virial", + "atom_ener", + "atom_pref", + "drdq", + "atom_ener_coeff", + "hessian", + ] + for lk in label_keys: + frame[f"find_{lk}"] = np.float32(1.0) if lk in frame else np.float32(0.0) + + # Handle registered data requirements: fill defaults for missing keys + for req_key, req_item in self._data_requirements.items(): + if req_key not in frame: + frame[f"find_{req_key}"] = np.float32(0.0) + ndof = req_item["ndof"] + default = req_item["default"] + atomic = req_item["atomic"] + if atomic: + shape = (frame_natoms, ndof) + else: + shape = (ndof,) + frame[req_key] = np.full(shape, default, dtype=np.float64) + elif f"find_{req_key}" not in frame: + frame[f"find_{req_key}"] = np.float32(1.0) + + # Add find_* for fparam/aparam/spin if not already set + for extra_key in ["fparam", "aparam", "spin"]: + if f"find_{extra_key}" not in frame: + frame[f"find_{extra_key}"] = ( + np.float32(1.0) if extra_key in frame else np.float32(0.0) + ) + + frame["fid"] = index + + return frame + + # --- Data requirement interface --- + + def add_data_requirement(self, data_requirement: list[DataRequirementItem]) -> None: + """Register expected keys; missing keys get default fill + find_key=0.0.""" + for item in data_requirement: + self._data_requirements[item["key"]] = item + + def print_summary(self, name: str, prob: Any) -> None: + """Print basic dataset info.""" + unique_nlocs = sorted(self._nloc_groups.keys()) + nloc_info = ", ".join( + f"{nloc}({len(idxs)})" for nloc, idxs in sorted(self._nloc_groups.items()) + ) + log.info( + f"LMDB {name}: {self.lmdb_path}, " + f"{self.nframes} frames, nloc groups: [{nloc_info}], " + f"batch_size={'auto' if self._auto_rule else self.batch_size}, " + f"mixed_batch={self.mixed_batch}" + ) + + def set_noise(self, noise_settings: dict[str, Any]) -> None: + """No-op for now.""" + + # --- Properties --- + + @property + def index(self) -> list[int]: + """Number of batches per system (single system).""" + return [max(1, self.nframes // self.batch_size)] + + @property + def total_batch(self) -> int: + return self.index[0] + + @property + def batch_sizes(self) -> list[int]: + return [self.batch_size] + + @property + def nloc_groups(self) -> dict[int, list[int]]: + """Nloc → list of frame indices.""" + return self._nloc_groups + + @property + def frame_nlocs(self) -> list[int]: + """Per-frame atom count.""" + return self._frame_nlocs + + +class SameNlocBatchSampler: + """Batch sampler that groups frames by nloc. + + For mixed-nloc datasets with mixed_batch=False: each batch contains only + frames with the same nloc. Within each nloc group, frames are shuffled. + Groups are interleaved round-robin so training sees diverse nloc values. + + When auto batch_size is used, batch_size is computed per-nloc-group. + + Parameters + ---------- + reader : LmdbDataReader + The dataset reader (provides nloc_groups, get_batch_size_for_nloc). + shuffle : bool + Whether to shuffle within each nloc group each epoch. + seed : int or None + Random seed for reproducibility. + """ + + def __init__( + self, + reader: LmdbDataReader, + shuffle: bool = True, + seed: int | None = None, + ) -> None: + self._reader = reader + self._shuffle = shuffle + self._rng = np.random.default_rng(seed) + + def __iter__(self) -> Iterator[list[int]]: + """Yield batches of frame indices, all with the same nloc.""" + # Build per-group batches + group_batches: list[list[list[int]]] = [] + for nloc in sorted(self._reader.nloc_groups.keys()): + indices = list(self._reader.nloc_groups[nloc]) + if self._shuffle: + self._rng.shuffle(indices) + bs = self._reader.get_batch_size_for_nloc(nloc) + batches = [] + for start in range(0, len(indices), bs): + batches.append(indices[start : start + bs]) + group_batches.append(batches) + + # Interleave groups round-robin + all_batches: list[list[int]] = [] + max_len = max(len(gb) for gb in group_batches) if group_batches else 0 + for i in range(max_len): + for gb in group_batches: + if i < len(gb): + all_batches.append(gb[i]) + + # Optionally shuffle the interleaved order + if self._shuffle: + self._rng.shuffle(all_batches) + + yield from all_batches + + def __len__(self) -> int: + """Total number of batches across all nloc groups.""" + total = 0 + for nloc, indices in self._reader.nloc_groups.items(): + bs = self._reader.get_batch_size_for_nloc(nloc) + total += (len(indices) + bs - 1) // bs + return total + + +class LmdbTestData: + """LMDB-backed data reader for dp test. + + Mimics the DeepmdData interface used by test_ener(): + .add(), .get_test(), .mixed_type, .pbc + + For mixed-nloc datasets, frames are grouped by nloc. + get_test(nloc=...) returns data for a specific group. + """ + + def __init__( + self, + lmdb_path: str, + type_map: list[str] | None = None, + shuffle_test: bool = True, + **kwargs: Any, + ) -> None: + self.lmdb_path = str(lmdb_path) + self._type_map = type_map or [] + self._env = _open_lmdb(self.lmdb_path) + + with self._env.begin() as txn: + meta = _read_metadata(txn) + + self.nframes, self._frame_fmt, self._natoms_per_type = _parse_metadata(meta) + self._natoms = sum(self._natoms_per_type) + + # Read all frames + self._frames: list[dict[str, Any]] = [] + with self._env.begin() as txn: + for i in range(self.nframes): + key = format(i, self._frame_fmt).encode() + raw = txn.get(key) + if raw is not None: + self._frames.append(_remap_keys(_decode_frame(raw))) + + # Shuffle if requested + if shuffle_test: + rng = np.random.default_rng() + indices = rng.permutation(len(self._frames)) + self._frames = [self._frames[i] for i in indices] + + # Group frames by nloc + self._nloc_groups: dict[int, list[int]] = {} + for idx, frame in enumerate(self._frames): + atype = frame.get("atype") + nloc = len(atype) if isinstance(atype, np.ndarray) else self._natoms + self._nloc_groups.setdefault(nloc, []).append(idx) + + # Data requirements + self._requirements: dict[str, dict[str, Any]] = {} + + # Detect PBC: if any frame has a non-zero box + self.pbc = True + if len(self._frames) > 0: + f0 = self._frames[0] + if "box" not in f0: + self.pbc = False + elif isinstance(f0["box"], np.ndarray) and np.allclose(f0["box"], 0.0): + self.pbc = False + + self.mixed_type = False + + @property + def nloc_groups(self) -> dict[int, list[int]]: + """Nloc → list of frame indices in self._frames.""" + return self._nloc_groups + + def add( + self, + key: str, + ndof: int, + atomic: bool = False, + must: bool = True, + high_prec: bool = False, + repeat: int = 1, + default: float = 0.0, + **kwargs: Any, + ) -> None: + """Register a data requirement (mirrors DeepmdData.add).""" + self._requirements[key] = { + "ndof": ndof, + "atomic": atomic, + "must": must, + "high_prec": high_prec, + "repeat": repeat, + "default": default, + } + + def get_test(self, nloc: int | None = None) -> dict[str, Any]: + """Return frames stacked as numpy arrays. + + Parameters + ---------- + nloc : int or None + If specified, return only frames with this atom count. + If None and all frames have the same nloc, return all. + If None and mixed nloc, return the largest group and log a warning. + Returns dict matching DeepmdData.get_test() format: + """ + if nloc is not None: + if nloc not in self._nloc_groups: + raise ValueError( + f"No frames with nloc={nloc}. Available: {sorted(self._nloc_groups.keys())}" + ) + frame_indices = self._nloc_groups[nloc] + natoms = nloc + elif len(self._nloc_groups) == 1: + # Uniform nloc — use all frames + natoms = next(iter(self._nloc_groups)) + frame_indices = list(range(len(self._frames))) + else: + # Mixed nloc — use the largest group + natoms = max(self._nloc_groups, key=lambda k: len(self._nloc_groups[k])) + frame_indices = self._nloc_groups[natoms] + group_summary = {k: len(v) for k, v in sorted(self._nloc_groups.items())} + log.warning( + f"Mixed-nloc LMDB for dp test: using nloc={natoms} group " + f"({len(frame_indices)} frames). " + f"Available groups: {group_summary}" + ) + + frames = [self._frames[i] for i in frame_indices] + return self._stack_frames(frames, natoms) + + def _stack_frames( + self, frames: list[dict[str, Any]], natoms: int + ) -> dict[str, Any]: + """Stack a list of same-nloc frames into numpy arrays.""" + nframes = len(frames) + result: dict[str, Any] = {} + + # Core arrays + coords = [] + boxes = [] + atypes = [] + + for frame in frames: + if "coord" in frame and isinstance(frame["coord"], np.ndarray): + coords.append(frame["coord"].reshape(natoms * 3).astype(np.float64)) + if "box" in frame and isinstance(frame["box"], np.ndarray): + boxes.append(frame["box"].reshape(9).astype(np.float64)) + else: + boxes.append(np.zeros(9, dtype=np.float64)) + if "atype" in frame and isinstance(frame["atype"], np.ndarray): + atypes.append(frame["atype"].reshape(natoms).astype(np.int64)) + + result["coord"] = ( + np.stack(coords) if coords else np.zeros((0, natoms * 3), dtype=np.float64) + ) + result["box"] = np.stack(boxes) if boxes else np.zeros((0, 9), dtype=np.float64) + result["type"] = ( + np.stack(atypes) if atypes else np.zeros((0, natoms), dtype=np.int64) + ) + + # Label keys and registered requirements + all_keys: dict[str, dict[str, Any]] = {} + for key in [ + "energy", + "force", + "virial", + "atom_ener", + "atom_pref", + "force_mag", + "spin", + "fparam", + "aparam", + "hessian", + "efield", + ]: + all_keys[key] = {"ndof": None, "atomic": False, "default": 0.0} + for key, req in self._requirements.items(): + all_keys[key] = req + + for key, req_info in all_keys.items(): + has_key = any( + key in f and isinstance(f.get(key), np.ndarray) for f in frames + ) + result[f"find_{key}"] = 1.0 if has_key else 0.0 + + if has_key: + arrays = [] + for frame in frames: + val = frame.get(key) + if isinstance(val, np.ndarray): + arrays.append(val.astype(np.float64).ravel()) + elif val is not None: + arrays.append(np.array([float(val)], dtype=np.float64)) + else: + ref = next( + ( + f[key] + for f in frames + if isinstance(f.get(key), np.ndarray) + ), + None, + ) + if ref is not None: + arrays.append(np.zeros(ref.size, dtype=np.float64)) + else: + arrays.append(np.zeros(1, dtype=np.float64)) + result[key] = np.stack(arrays) + elif key in self._requirements: + ndof = self._requirements[key]["ndof"] + atomic = self._requirements[key]["atomic"] + default = self._requirements[key]["default"] + if atomic: + shape = (nframes, natoms * ndof) + else: + shape = (nframes, ndof) + result[key] = np.full(shape, default, dtype=np.float64) + + return result + + +def merge_lmdb( + src_paths: list[str], + dst_path: str, + *, + map_size: int = 1024**4, # 1 TB default +) -> str: + """Merge multiple LMDB datasets into one. + + Frames are concatenated in order. The output metadata includes a + ``frame_nlocs`` list for fast init (skips per-frame scan). + + Parameters + ---------- + src_paths : list[str] + Paths to source LMDB directories. + dst_path : str + Path for the merged LMDB output. + map_size : int + Maximum size of the output LMDB (default 1 TB). + + Returns + ------- + str + Path to the created LMDB. + """ + import os + import shutil + + if os.path.exists(dst_path): + shutil.rmtree(dst_path) + + dst_env = lmdb.open(dst_path, map_size=map_size) + frame_idx = 0 + fmt = "012d" + frame_nlocs: list[int] = [] + first_system_info: dict | None = None + + for src_path in src_paths: + src_env = _open_lmdb(src_path) + with src_env.begin() as txn: + meta = _read_metadata(txn) + nframes, src_fmt, natoms_per_type = _parse_metadata(meta) + fallback_natoms = sum(natoms_per_type) + + if first_system_info is None: + first_system_info = meta.get("system_info", {}) + + # Check for pre-computed frame_nlocs in source + src_nlocs = meta.get("frame_nlocs") + + with src_env.begin() as src_txn, dst_env.begin(write=True) as dst_txn: + for i in range(nframes): + src_key = format(i, src_fmt).encode() + raw = src_txn.get(src_key) + if raw is None: + continue + dst_key = format(frame_idx, fmt).encode() + dst_txn.put(dst_key, raw) + + # Get nloc for this frame + if src_nlocs is not None: + frame_nlocs.append(int(src_nlocs[i])) + else: + frame_raw = msgpack.unpackb(raw, raw=False) + atype_raw = frame_raw.get("atom_types") + if isinstance(atype_raw, dict): + shape = atype_raw.get("shape") or atype_raw.get(b"shape") + if shape: + frame_nlocs.append(int(shape[0])) + else: + frame_nlocs.append(fallback_natoms) + else: + frame_nlocs.append(fallback_natoms) + + frame_idx += 1 + src_env.close() + + # Write merged metadata with frame_nlocs for fast init + merged_meta = { + "nframes": frame_idx, + "frame_idx_fmt": fmt, + "system_info": first_system_info or {}, + "frame_nlocs": frame_nlocs, + } + with dst_env.begin(write=True) as txn: + txn.put(b"__metadata__", msgpack.packb(merged_meta, use_bin_type=True)) + dst_env.close() + + nloc_counts: dict[int, int] = {} + for n in frame_nlocs: + nloc_counts[n] = nloc_counts.get(n, 0) + 1 + log.info( + f"Merged {len(src_paths)} LMDBs → {dst_path}: " + f"{frame_idx} frames, nloc groups: {dict(sorted(nloc_counts.items()))}" + ) + return dst_path diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index 4a0cb27cb1..a81a992e4c 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -16,6 +16,10 @@ expand_sys_str, j_loader, ) +from deepmd.dpmodel.utils.lmdb_data import ( + LmdbTestData, + is_lmdb, +) from deepmd.infer.deep_dipole import ( DeepDipole, ) @@ -62,6 +66,24 @@ log = logging.getLogger(__name__) +class _LmdbTestDataNlocView: + """Thin wrapper that makes LmdbTestData.get_test() return a specific nloc group. + + Delegates all attributes to the underlying LmdbTestData, but get_test() + returns only frames with the specified nloc. + """ + + def __init__(self, lmdb_test_data: LmdbTestData, nloc: int) -> None: + self._inner = lmdb_test_data + self._nloc = nloc + + def __getattr__(self, name: str) -> Any: + return getattr(self._inner, name) + + def get_test(self) -> dict: + return self._inner.get_test(nloc=self._nloc) + + def test( *, model: str, @@ -126,8 +148,11 @@ def test( systems = str((root / Path(systems)).resolve()) else: systems = [str((root / Path(ss)).resolve()) for ss in systems] - patterns = data_params.get("rglob_patterns", None) - all_sys = process_systems(systems, patterns=patterns) + if is_lmdb(systems): + all_sys = [systems] + else: + patterns = data_params.get("rglob_patterns", None) + all_sys = process_systems(systems, patterns=patterns) elif valid_json is not None: jdata = j_loader(valid_json) jdata = update_deepmd_input(jdata) @@ -140,13 +165,19 @@ def test( systems = str((root / Path(systems)).resolve()) else: systems = [str((root / Path(ss)).resolve()) for ss in systems] - patterns = data_params.get("rglob_patterns", None) - all_sys = process_systems(systems, patterns=patterns) + if is_lmdb(systems): + all_sys = [systems] + else: + patterns = data_params.get("rglob_patterns", None) + all_sys = process_systems(systems, patterns=patterns) elif datafile is not None: with open(datafile) as datalist: all_sys = datalist.read().splitlines() elif system is not None: - all_sys = expand_sys_str(system) + if is_lmdb(system): + all_sys = [system] + else: + all_sys = expand_sys_str(system) else: raise RuntimeError("No data source specified for testing") @@ -168,61 +199,92 @@ def test( # create data class tmap = dp.get_type_map() - data = DeepmdData( - system, - set_prefix="set", - shuffle_test=shuffle_test, - type_map=tmap, - sort_atoms=False, - ) - - if isinstance(dp, DeepPot): - err = test_ener( - dp, - data, - system, - numb_test, - detail_file, - atomic, - append_detail=(cc != 0), - ) - elif isinstance(dp, DeepDOS): - err = test_dos( - dp, - data, + if is_lmdb(system): + lmdb_data = LmdbTestData( system, - numb_test, - detail_file, - atomic, - append_detail=(cc != 0), + type_map=tmap, + shuffle_test=shuffle_test, ) - elif isinstance(dp, DeepProperty): - err = test_property( - dp, - data, + # For mixed-nloc LMDB, test each nloc group separately + nloc_keys = sorted(lmdb_data.nloc_groups.keys()) + if len(nloc_keys) > 1: + group_summary = { + k: len(v) for k, v in sorted(lmdb_data.nloc_groups.items()) + } + log.info( + f"# mixed-nloc LMDB: testing {len(nloc_keys)} groups: " + f"{group_summary}" + ) + data_items: list[tuple[Any, str]] = [] + for nloc_val in nloc_keys: + label = f"{system} [nloc={nloc_val}]" if len(nloc_keys) > 1 else system + # Create a thin wrapper that returns only this nloc group + data_items.append((_LmdbTestDataNlocView(lmdb_data, nloc_val), label)) + else: + data = DeepmdData( system, - numb_test, - detail_file, - atomic, - append_detail=(cc != 0), + set_prefix="set", + shuffle_test=shuffle_test, + type_map=tmap, + sort_atoms=False, ) - elif isinstance(dp, DeepDipole): - err = test_dipole(dp, data, numb_test, detail_file, atomic) - elif isinstance(dp, DeepPolar): - err = test_polar(dp, data, numb_test, detail_file, atomic=atomic) - elif isinstance(dp, DeepGlobalPolar): # should not appear in this new version - log.warning( - "Global polar model is not currently supported. Please directly use the polar mode and change loss parameters." - ) - err = test_polar( - dp, data, numb_test, detail_file, atomic=False - ) # YWolfeee: downward compatibility - log.info("# ----------------------------------------------- ") - err_coll.append(err) + data_items = [(data, system)] + + for data, sys_label in data_items: + if sys_label != system: + log.info(f"# testing sub-group : {sys_label}") + + if isinstance(dp, DeepPot): + err = test_ener( + dp, + data, + sys_label, + numb_test, + detail_file, + atomic, + append_detail=(cc != 0), + ) + elif isinstance(dp, DeepDOS): + err = test_dos( + dp, + data, + sys_label, + numb_test, + detail_file, + atomic, + append_detail=(cc != 0), + ) + elif isinstance(dp, DeepProperty): + err = test_property( + dp, + data, + sys_label, + numb_test, + detail_file, + atomic, + append_detail=(cc != 0), + ) + elif isinstance(dp, DeepDipole): + err = test_dipole(dp, data, numb_test, detail_file, atomic) + elif isinstance(dp, DeepPolar): + err = test_polar(dp, data, numb_test, detail_file, atomic=atomic) + elif isinstance( + dp, DeepGlobalPolar + ): # should not appear in this new version + log.warning( + "Global polar model is not currently supported. Please directly use the polar mode and change loss parameters." + ) + err = test_polar( + dp, data, numb_test, detail_file, atomic=False + ) # YWolfeee: downward compatibility + log.info("# ----------------------------------------------- ") + err_coll.append(err) avg_err = weighted_average(err_coll) - if len(all_sys) != len(err_coll): + # For mixed-nloc LMDB, err_coll may have more entries than all_sys + # (one per nloc group per system). Only warn if fewer. + if len(err_coll) < len(all_sys): log.warning("Not all systems are tested! Check if the systems are valid") log.info("# ----------weighted average of errors----------- ") diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 46ad8a6cd0..ae82fb2b5a 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -70,6 +70,10 @@ from deepmd.pt.utils.finetune import ( get_finetune_rules, ) +from deepmd.pt.utils.lmdb_dataset import ( + LmdbDataset, + is_lmdb, +) from deepmd.pt.utils.multi_task import ( preprocess_shared_params, ) @@ -114,7 +118,9 @@ def prepare_trainer_input_single( data_dict_single: dict[str, Any], rank: int = 0, seed: int | None = None, - ) -> tuple[DpLoaderSet, DpLoaderSet | None, DPPath | None]: + ) -> tuple[ + DpLoaderSet | LmdbDataset, DpLoaderSet | LmdbDataset | None, DPPath | None + ]: # get data modifier modifier = None modifier_params = model_params_single.get("modifier", None) @@ -127,11 +133,6 @@ def prepare_trainer_input_single( validation_dataset_params["systems"] if validation_dataset_params else None ) training_systems = training_dataset_params["systems"] - trn_patterns = training_dataset_params.get("rglob_patterns", None) - training_systems = process_systems(training_systems, patterns=trn_patterns) - if validation_systems is not None: - val_patterns = validation_dataset_params.get("rglob_patterns", None) - validation_systems = process_systems(validation_systems, val_patterns) # stat files stat_file_path_single = data_dict_single.get("stat_file", None) @@ -146,27 +147,60 @@ def prepare_trainer_input_single( Path(stat_file_path_single).mkdir() stat_file_path_single = DPPath(stat_file_path_single, "a") - # validation and training data - # avoid the same batch sequence among devices - rank_seed = [rank, seed % (2**32)] if seed is not None else None - validation_data_single = ( - DpLoaderSet( - validation_systems, - validation_dataset_params["batch_size"], + # LMDB path: single string → LmdbDataset + if is_lmdb(training_systems): + train_data_single = LmdbDataset( + training_systems, + model_params_single["type_map"], + training_dataset_params["batch_size"], + ) + if validation_systems is not None and is_lmdb(validation_systems): + validation_data_single = LmdbDataset( + validation_systems, + model_params_single["type_map"], + validation_dataset_params["batch_size"], + ) + elif validation_systems is not None: + val_patterns = validation_dataset_params.get("rglob_patterns", None) + validation_systems = process_systems(validation_systems, val_patterns) + rank_seed = [rank, seed % (2**32)] if seed is not None else None + validation_data_single = DpLoaderSet( + validation_systems, + validation_dataset_params["batch_size"], + model_params_single["type_map"], + seed=rank_seed, + modifier=modifier, + ) + else: + validation_data_single = None + else: + # Standard npy path + trn_patterns = training_dataset_params.get("rglob_patterns", None) + training_systems = process_systems(training_systems, patterns=trn_patterns) + if validation_systems is not None: + val_patterns = validation_dataset_params.get("rglob_patterns", None) + validation_systems = process_systems(validation_systems, val_patterns) + + # avoid the same batch sequence among devices + rank_seed = [rank, seed % (2**32)] if seed is not None else None + validation_data_single = ( + DpLoaderSet( + validation_systems, + validation_dataset_params["batch_size"], + model_params_single["type_map"], + seed=rank_seed, + modifier=modifier, + ) + if validation_systems + else None + ) + train_data_single = DpLoaderSet( + training_systems, + training_dataset_params["batch_size"], model_params_single["type_map"], seed=rank_seed, modifier=modifier, ) - if validation_systems - else None - ) - train_data_single = DpLoaderSet( - training_systems, - training_dataset_params["batch_size"], - model_params_single["type_map"], - seed=rank_seed, - modifier=modifier, - ) return ( train_data_single, validation_data_single, diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 9d2298febc..04d13831d1 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -72,6 +72,11 @@ from deepmd.pt.utils.learning_rate import ( BaseLR, ) +from deepmd.pt.utils.lmdb_dataset import ( + LmdbDataset, + _collate_lmdb_batch, + _SameNlocBatchSamplerTorch, +) from deepmd.pt.utils.stat import ( make_stat_input, ) @@ -224,8 +229,8 @@ def cycle_iterator(iterable: Iterable) -> Generator[Any, None, None]: yield from it def get_data_loader( - _training_data: DpLoaderSet, - _validation_data: DpLoaderSet | None, + _training_data: DpLoaderSet | LmdbDataset, + _validation_data: DpLoaderSet | LmdbDataset | None, _training_params: dict[str, Any], ) -> tuple[ DataLoader, @@ -234,6 +239,42 @@ def get_data_loader( Generator[Any, None, None] | None, int, ]: + def get_dataloader_and_iter_lmdb( + _data: LmdbDataset, + ) -> tuple[DataLoader, Generator[Any, None, None]]: + if _data.mixed_batch: + # TODO [mixed_batch=True]: Replace SameNlocBatchSampler with + # RandomSampler(replacement=False) + padding collate_fn. + # Changes needed: + # 1. _collate_lmdb_batch: pad coord/force/atype to max_nloc, + # add "atom_mask" bool tensor (nframes, max_nloc) + # 2. Use RandomSampler(_data, replacement=False) as sampler + # 3. Use fixed batch_size in DataLoader (not batch_sampler) + # 4. Model forward: apply atom_mask to descriptor/fitting + # 5. Loss: mask out padded atoms in force loss + raise NotImplementedError( + "mixed_batch=True training is not yet supported." + ) + # mixed_batch=False: group frames by nloc, each batch same nloc. + # SameNlocBatchSampler yields list[int] per batch, all same nloc. + # Auto batch_size is computed per-nloc-group inside the sampler. + from deepmd.dpmodel.utils.lmdb_data import ( + SameNlocBatchSampler, + ) + + _batch_sampler = _SameNlocBatchSamplerTorch( + SameNlocBatchSampler(_data._reader, shuffle=True) + ) + _dataloader = DataLoader( + _data, + batch_sampler=_batch_sampler, + num_workers=0, + collate_fn=_collate_lmdb_batch, + pin_memory=(DEVICE != "cpu"), + ) + _data_iter = cycle_iterator(_dataloader) + return _dataloader, _data_iter + def get_dataloader_and_iter( _data: DpLoaderSet, _params: dict[str, Any] ) -> tuple[DataLoader, Generator[Any, None, None]]: @@ -256,17 +297,28 @@ def get_dataloader_and_iter( _data_iter = cycle_iterator(_dataloader) return _dataloader, _data_iter - training_dataloader, training_data_iter = get_dataloader_and_iter( - _training_data, _training_params["training_data"] - ) + if isinstance(_training_data, LmdbDataset): + training_dataloader, training_data_iter = get_dataloader_and_iter_lmdb( + _training_data + ) + else: + training_dataloader, training_data_iter = get_dataloader_and_iter( + _training_data, _training_params["training_data"] + ) if _validation_data is not None: - ( - validation_dataloader, - validation_data_iter, - ) = get_dataloader_and_iter( - _validation_data, _training_params["validation_data"] - ) + if isinstance(_validation_data, LmdbDataset): + ( + validation_dataloader, + validation_data_iter, + ) = get_dataloader_and_iter_lmdb(_validation_data) + else: + ( + validation_dataloader, + validation_data_iter, + ) = get_dataloader_and_iter( + _validation_data, _training_params["validation_data"] + ) valid_numb_batch = _training_params["validation_data"].get( "numb_btch", 1 ) @@ -403,12 +455,17 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR: self.valid_numb_batch, ) = get_data_loader(training_data, validation_data, training_params) training_data.print_summary( - "training", to_numpy_array(self.training_dataloader.sampler.weights) + "training", + to_numpy_array(self.training_dataloader.sampler.weights) + if not isinstance(training_data, LmdbDataset) + else [1.0], ) if validation_data is not None: validation_data.print_summary( "validation", - to_numpy_array(self.validation_dataloader.sampler.weights), + to_numpy_array(self.validation_dataloader.sampler.weights) + if not isinstance(validation_data, LmdbDataset) + else [1.0], ) else: ( @@ -481,13 +538,16 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR: ) if self.num_epoch <= 0: raise ValueError("training.num_epoch must be positive.") - sampler_weights = to_numpy_array( - self.training_dataloader.sampler.weights - ) - total_numb_batch = compute_total_numb_batch( - training_data.index, - sampler_weights, - ) + if isinstance(training_data, LmdbDataset): + total_numb_batch = training_data.total_batch + else: + sampler_weights = to_numpy_array( + self.training_dataloader.sampler.weights + ) + total_numb_batch = compute_total_numb_batch( + training_data.index, + sampler_weights, + ) if total_numb_batch <= 0: raise ValueError( "Total number of training batches must be positive." diff --git a/deepmd/pt/utils/lmdb_dataset.py b/deepmd/pt/utils/lmdb_dataset.py new file mode 100644 index 0000000000..2568d8efc9 --- /dev/null +++ b/deepmd/pt/utils/lmdb_dataset.py @@ -0,0 +1,229 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""PyTorch LMDB dataset — thin wrapper around framework-agnostic LmdbDataReader.""" + +import logging +from collections.abc import ( + Iterator, +) +from typing import ( + Any, +) + +import torch +from torch.utils.data import ( + DataLoader, + Dataset, + Sampler, +) +from torch.utils.data._utils.collate import ( + collate_tensor_fn, +) + +from deepmd.dpmodel.utils.lmdb_data import ( + LmdbDataReader, + LmdbTestData, + SameNlocBatchSampler, + is_lmdb, +) +from deepmd.utils.data import ( + DataRequirementItem, +) + +log = logging.getLogger(__name__) + +# Re-export for backward compatibility +__all__ = [ + "LmdbDataset", + "LmdbTestData", + "_collate_lmdb_batch", + "is_lmdb", +] + + +def _collate_lmdb_batch(batch: list[dict[str, Any]]) -> dict[str, Any]: + """Collate a list of frame dicts into a batch dict. + + All frames in the batch must have the same nloc (enforced by + SameNlocBatchSampler when mixed_batch=False). + + For mixed_batch=True, this function would need padding + mask. + Currently raises NotImplementedError for that case. + """ + if len(batch) > 1: + atypes = [d.get("atype") for d in batch if d.get("atype") is not None] + if atypes and any(len(a) != len(atypes[0]) for a in atypes): + raise NotImplementedError( + "mixed_batch collation (frames with different atom counts " + "in the same batch) is not yet supported. " + "Padding + mask in collate_fn needed." + ) + + example = batch[0] + result: dict[str, Any] = {} + for key in example: + if "find_" in key: + result[key] = batch[0][key] + elif key == "fid": + result[key] = [d[key] for d in batch] + elif key == "type": + continue + elif batch[0][key] is None: + result[key] = None + else: + with torch.device("cpu"): + result[key] = collate_tensor_fn( + [torch.as_tensor(d[key]) for d in batch] + ) + result["sid"] = torch.tensor([0], dtype=torch.long, device="cpu") + return result + + +class _SameNlocBatchSamplerTorch(Sampler): + """Torch Sampler adapter around the framework-agnostic SameNlocBatchSampler. + + PyTorch DataLoader with batch_sampler expects a Sampler that yields + lists of indices. This wraps SameNlocBatchSampler to satisfy that. + """ + + def __init__(self, inner: SameNlocBatchSampler) -> None: + self._inner = inner + + def __iter__(self) -> Iterator[list[int]]: + yield from self._inner + + def __len__(self) -> int: + return len(self._inner) + + +class LmdbDataset(Dataset): + """PyTorch Dataset backed by LMDB via LmdbDataReader. + + Parameters + ---------- + lmdb_path : str + Path to the LMDB directory. + type_map : list[str] + Global type map from model config. + batch_size : int or str + Batch size. Supports int, "auto", "auto:N". + mixed_batch : bool + If True, allow different nloc in the same batch (future). + If False (default), use SameNlocBatchSampler. + """ + + def __init__( + self, + lmdb_path: str, + type_map: list[str], + batch_size: int | str = "auto", + mixed_batch: bool = False, + ) -> None: + self._reader = LmdbDataReader( + lmdb_path, type_map, batch_size, mixed_batch=mixed_batch + ) + + if mixed_batch: + # Future: DataLoader with padding collate_fn + raise NotImplementedError( + "mixed_batch=True is not yet supported. " + "Requires padding + mask in collate_fn." + ) + + # Same-nloc batching: use SameNlocBatchSampler + sampler = SameNlocBatchSampler(self._reader, shuffle=True) + self._batch_sampler = _SameNlocBatchSamplerTorch(sampler) + + with torch.device("cpu"): + self._inner_dataloader = DataLoader( + self, + batch_sampler=self._batch_sampler, + num_workers=0, + collate_fn=_collate_lmdb_batch, + ) + + # Per-nloc-group dataloaders for make_stat_input. + # Each group gets its own DataLoader so torch.cat in stat collection + # only concatenates same-shape tensors. + self._nloc_dataloaders: list[DataLoader] = [] + for nloc in sorted(self._reader.nloc_groups.keys()): + indices = self._reader.nloc_groups[nloc] + subset = torch.utils.data.Subset(self, indices) + bs = self._reader.get_batch_size_for_nloc(nloc) + with torch.device("cpu"): + dl = DataLoader( + subset, + batch_size=bs, + shuffle=False, + num_workers=0, + drop_last=False, + collate_fn=_collate_lmdb_batch, + ) + self._nloc_dataloaders.append(dl) + + def __len__(self) -> int: + return len(self._reader) + + def __getitem__(self, index: int) -> dict[str, Any]: + return self._reader[index] + + # --- Delegated to reader --- + + @property + def lmdb_path(self) -> str: + return self._reader.lmdb_path + + @property + def nframes(self) -> int: + return self._reader.nframes + + @property + def mixed_batch(self) -> bool: + return self._reader.mixed_batch + + @property + def batch_size(self) -> int: + return self._reader.batch_size + + def add_data_requirement(self, data_requirement: list[DataRequirementItem]) -> None: + self._reader.add_data_requirement(data_requirement) + + def preload_and_modify_all_data_torch(self) -> None: + """No-op: LMDB reads on demand.""" + + def print_summary(self, name: str, prob: Any) -> None: + self._reader.print_summary(name, prob) + + def set_noise(self, noise_settings: dict[str, Any]) -> None: + self._reader.set_noise(noise_settings) + + @property + def index(self) -> list[int]: + return self._reader.index + + @property + def total_batch(self) -> int: + return self._reader.total_batch + + @property + def batch_sizes(self) -> list[int]: + return self._reader.batch_sizes + + # --- PyTorch-specific trainer compatibility --- + + @property + def systems(self) -> list: + """One 'system' per nloc group for stat collection compatibility.""" + return [self] * len(self._nloc_dataloaders) + + @property + def dataloaders(self) -> list: + """Per-nloc-group dataloaders for make_stat_input. + + Each dataloader yields batches with uniform nloc, so torch.cat + in stat collection only concatenates same-shape tensors. + """ + return self._nloc_dataloaders + + @property + def sampler_list(self) -> list: + return [] diff --git a/examples/lmdb_data/input_lmdb.json b/examples/lmdb_data/input_lmdb.json new file mode 100644 index 0000000000..6e7f469415 --- /dev/null +++ b/examples/lmdb_data/input_lmdb.json @@ -0,0 +1,65 @@ +{ + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.5, + "rcut": 6.0, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "seed": 1 + }, + "fitting_net": { + "type": "ener", + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1 + } + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-08 + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 + }, + "training": { + "training_data": { + "systems": "water_training.lmdb", + "batch_size": "auto" + }, + "validation_data": { + "systems": "water_validation.lmdb", + "batch_size": 1 + }, + "numb_steps": 100, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 10, + "save_freq": 100 + } +} diff --git a/source/tests/consistent/test_lmdb_data.py b/source/tests/consistent/test_lmdb_data.py new file mode 100644 index 0000000000..d39735f92b --- /dev/null +++ b/source/tests/consistent/test_lmdb_data.py @@ -0,0 +1,509 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Consistency tests: LmdbDataReader (dpmodel) vs LmdbDataset (pt). + +Verifies that the framework-agnostic reader and the PyTorch wrapper +produce identical outputs for the same LMDB data. +Also tests SameNlocBatchSampler and mixed_batch guards. +""" + +import tempfile +import unittest + +import lmdb +import msgpack +import numpy as np + +from deepmd.dpmodel.utils.lmdb_data import ( + LmdbDataReader, + LmdbTestData, + SameNlocBatchSampler, + is_lmdb, +) + +try: + from deepmd.pt.utils.lmdb_dataset import ( + LmdbDataset, + _collate_lmdb_batch, + ) + + INSTALLED_PT = True +except ImportError: + INSTALLED_PT = False + + +def _make_frame(natoms: int = 6, seed: int = 0) -> dict: + """Create a synthetic frame dict for testing. + + Generates atom_types with roughly 1/3 type-0 and 2/3 type-1. + """ + rng = np.random.RandomState(seed) + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + atype = np.array([0] * n_type0 + [1] * n_type1, dtype=np.int64) + return { + "atom_names": ["O", "H"], + "atom_numbs": [ + { + "type": " str: + """Create a test LMDB database with uniform nloc.""" + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [n_type0, n_type1], + "formula": "test", + }, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(nframes): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +def _create_mixed_nloc_lmdb(path: str) -> str: + """Create an LMDB with frames of different atom counts. + + Frames 0-3: 6 atoms, Frames 4-7: 9 atoms, Frames 8-9: 12 atoms. + """ + frames_spec = [(6, 4), (9, 4), (12, 2)] # (natoms, count) + total = sum(c for _, c in frames_spec) + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [2, 4], # first frame's type counts + "formula": "mixed", + }, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + idx = 0 + for natoms, count in frames_spec: + for j in range(count): + txn.put( + format(idx, "012d").encode(), + msgpack.packb( + _make_frame(natoms=natoms, seed=idx), use_bin_type=True + ), + ) + idx += 1 + env.close() + return path + + +# ============================================================ +# Uniform nloc tests +# ============================================================ + + +class TestLmdbDataConsistency(unittest.TestCase): + """Verify LmdbDataReader (dpmodel) and LmdbDataset (pt) produce identical outputs.""" + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + cls._lmdb_path = _create_lmdb( + f"{cls._tmpdir.name}/test.lmdb", nframes=10, natoms=6 + ) + cls._type_map = ["O", "H"] + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + def test_same_len(self): + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(len(reader), 10) + if INSTALLED_PT: + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(len(ds), 10) + self.assertEqual(len(reader), len(ds)) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_same_frame_data(self): + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + for i in range(len(reader)): + frame_dp = reader[i] + frame_pt = ds[i] + self.assertEqual(set(frame_dp.keys()), set(frame_pt.keys())) + for key in frame_dp: + dp_val = frame_dp[key] + pt_val = frame_pt[key] + if isinstance(dp_val, np.ndarray): + np.testing.assert_array_equal( + dp_val, pt_val, err_msg=f"key={key}, frame={i}" + ) + else: + self.assertEqual(dp_val, pt_val, msg=f"key={key}, frame={i}") + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_same_batch_size(self): + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size="auto") + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size="auto") + self.assertEqual(reader.batch_size, ds.batch_size) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_same_properties(self): + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(reader.index, ds.index) + self.assertEqual(reader.total_batch, ds.total_batch) + self.assertEqual(reader.batch_sizes, ds.batch_sizes) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_data_requirement(self): + req = [ + { + "key": "virial", + "ndof": 9, + "atomic": False, + "must": False, + "high_prec": False, + "repeat": 1, + "default": 0.0, + } + ] + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + reader.add_data_requirement(req) + ds.add_data_requirement(req) + frame_dp = reader[0] + frame_pt = ds[0] + np.testing.assert_array_equal(frame_dp["virial"], frame_pt["virial"]) + self.assertEqual(frame_dp["find_virial"], frame_pt["find_virial"]) + + def test_lmdb_test_data(self): + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + result = td.get_test() + self.assertEqual(result["coord"].shape, (10, 18)) + self.assertEqual(result["box"].shape, (10, 9)) + self.assertEqual(result["type"].shape, (10, 6)) + self.assertEqual(result["energy"].shape, (10, 1)) + self.assertEqual(result["force"].shape, (10, 18)) + self.assertEqual(result["find_energy"], 1.0) + self.assertEqual(result["find_force"], 1.0) + + def test_is_lmdb(self): + self.assertTrue(is_lmdb(self._lmdb_path)) + self.assertTrue(is_lmdb("something.lmdb")) + self.assertFalse(is_lmdb("/some/npy/system")) + self.assertFalse(is_lmdb(["list", "of", "systems"])) + + def test_reader_standalone(self): + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + frame = reader[0] + self.assertIn("coord", frame) + self.assertIn("energy", frame) + self.assertIn("force", frame) + self.assertIn("atype", frame) + self.assertIn("box", frame) + self.assertIn("natoms", frame) + self.assertIn("real_natoms_vec", frame) + self.assertIn("find_energy", frame) + self.assertEqual(frame["coord"].dtype, np.float64) + self.assertEqual(frame["atype"].dtype, np.int64) + + def test_uniform_nloc_single_group(self): + """Uniform-nloc LMDB has exactly one nloc group.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(len(reader.nloc_groups), 1) + self.assertIn(6, reader.nloc_groups) + self.assertEqual(len(reader.nloc_groups[6]), 10) + + +# ============================================================ +# Mixed nloc tests +# ============================================================ + + +class TestMixedNloc(unittest.TestCase): + """Tests for mixed-nloc datasets and SameNlocBatchSampler.""" + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + cls._lmdb_path = _create_mixed_nloc_lmdb(f"{cls._tmpdir.name}/mixed.lmdb") + cls._type_map = ["O", "H"] + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + def test_nloc_groups_detected(self): + """LmdbDataReader correctly groups frames by nloc.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(set(reader.nloc_groups.keys()), {6, 9, 12}) + self.assertEqual(len(reader.nloc_groups[6]), 4) + self.assertEqual(len(reader.nloc_groups[9]), 4) + self.assertEqual(len(reader.nloc_groups[12]), 2) + + def test_per_frame_natoms_vec(self): + """Each frame gets its own natoms_vec matching its actual atom count.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + frame0 = reader[0] # 6 atoms + frame4 = reader[4] # 9 atoms + frame8 = reader[8] # 12 atoms + self.assertEqual(frame0["natoms"][0], 6) + self.assertEqual(frame4["natoms"][0], 9) + self.assertEqual(frame8["natoms"][0], 12) + np.testing.assert_array_equal(frame0["real_natoms_vec"], frame0["natoms"]) + + def test_per_frame_shapes(self): + """coord/force/atype shapes match per-frame atom count.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + frame0 = reader[0] # 6 atoms + frame4 = reader[4] # 9 atoms + self.assertEqual(frame0["coord"].shape, (6, 3)) + self.assertEqual(frame0["force"].shape, (6, 3)) + self.assertEqual(frame0["atype"].shape, (6,)) + self.assertEqual(frame4["coord"].shape, (9, 3)) + self.assertEqual(frame4["force"].shape, (9, 3)) + self.assertEqual(frame4["atype"].shape, (9,)) + + def test_frame_nlocs(self): + """frame_nlocs returns correct per-frame atom counts.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + expected = [6, 6, 6, 6, 9, 9, 9, 9, 12, 12] + self.assertEqual(reader.frame_nlocs, expected) + + # --- SameNlocBatchSampler tests --- + + def test_sampler_all_batches_same_nloc(self): + """Every batch from SameNlocBatchSampler has frames with identical nloc.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + sampler = SameNlocBatchSampler(reader, shuffle=False, seed=42) + for batch_indices in sampler: + nlocs_in_batch = [reader.frame_nlocs[i] for i in batch_indices] + self.assertTrue( + all(n == nlocs_in_batch[0] for n in nlocs_in_batch), + f"Mixed nloc in batch: {nlocs_in_batch} (indices={batch_indices})", + ) + + def test_sampler_covers_all_frames(self): + """SameNlocBatchSampler yields every frame exactly once.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + sampler = SameNlocBatchSampler(reader, shuffle=False, seed=42) + all_indices = [] + for batch_indices in sampler: + all_indices.extend(batch_indices) + self.assertEqual(sorted(all_indices), list(range(10))) + + def test_sampler_auto_batch_size_per_nloc(self): + """Auto batch_size varies by nloc group.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size="auto") + bs_6 = reader.get_batch_size_for_nloc(6) + bs_9 = reader.get_batch_size_for_nloc(9) + bs_12 = reader.get_batch_size_for_nloc(12) + # Larger nloc → smaller batch_size + self.assertGreaterEqual(bs_6, bs_9) + self.assertGreaterEqual(bs_9, bs_12) + + def test_sampler_shuffle_deterministic(self): + """Same seed produces same batch order.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + s1 = SameNlocBatchSampler(reader, shuffle=True, seed=123) + s2 = SameNlocBatchSampler(reader, shuffle=True, seed=123) + batches1 = list(s1) + batches2 = list(s2) + self.assertEqual(batches1, batches2) + + def test_sampler_len(self): + """__len__ matches actual number of batches yielded.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + sampler = SameNlocBatchSampler(reader, shuffle=False) + batches = list(sampler) + self.assertEqual(len(sampler), len(batches)) + + # --- Collate guard tests --- + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_collate_mixed_nloc_raises(self): + """Collating frames with different nloc raises NotImplementedError.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + frame_6 = reader[0] + frame_9 = reader[4] + with self.assertRaises(NotImplementedError) as ctx: + _collate_lmdb_batch([frame_6, frame_9]) + self.assertIn("mixed_batch", str(ctx.exception)) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_collate_same_nloc_ok(self): + """Collating frames with same nloc works fine.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) + frame0 = reader[0] + frame1 = reader[1] + batch = _collate_lmdb_batch([frame0, frame1]) + self.assertIn("coord", batch) + self.assertEqual(batch["coord"].shape[0], 2) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_mixed_batch_true_raises(self): + """LmdbDataset(mixed_batch=True) raises NotImplementedError.""" + with self.assertRaises(NotImplementedError) as ctx: + LmdbDataset(self._lmdb_path, self._type_map, batch_size=2, mixed_batch=True) + self.assertIn("mixed_batch", str(ctx.exception)) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_pt_dataset_iterates_same_nloc_batches(self): + """LmdbDataset iteration produces only same-nloc batches.""" + import torch + + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + with torch.device("cpu"): + for batch in ds.dataloaders[0]: + atype = batch.get("atype") + if atype is not None: + # All frames in batch have same nloc + self.assertEqual(atype.shape[1], atype.shape[1]) + break # just check first batch + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_pt_dataset_mixed_batch_flag(self): + """LmdbDataset exposes mixed_batch from reader.""" + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + self.assertFalse(ds.mixed_batch) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_pt_full_epoch_mixed_nloc(self): + """Full DataLoader epoch over mixed-nloc LMDB: all batches same-nloc, all frames covered.""" + import torch + + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + all_fids = [] + with torch.device("cpu"): + for dl in ds.dataloaders: + for batch in dl: + atype = batch["atype"] + nloc = atype.shape[1] + for i in range(atype.shape[0]): + self.assertEqual(atype[i].shape[0], nloc) + all_fids.extend(batch["fid"]) + # All 10 frames should be covered + self.assertEqual(sorted(all_fids), list(range(10))) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") + def test_pt_batch_shapes_consistent(self): + """Within each batch, coord/force/natoms shapes are consistent with atype.""" + import torch + + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=3) + with torch.device("cpu"): + for batch in ds.dataloaders[0]: + bs = batch["atype"].shape[0] + nloc = batch["atype"].shape[1] + self.assertEqual(batch["coord"].shape, (bs, nloc, 3)) + self.assertEqual(batch["force"].shape, (bs, nloc, 3)) + self.assertEqual(batch["natoms"].shape, (bs, 4)) # ntypes=2 → 2+2=4 + # natoms_vec[0] should equal nloc for all frames + for i in range(bs): + self.assertEqual(batch["natoms"][i, 0].item(), nloc) + + # --- LmdbTestData mixed-nloc tests --- + + def test_test_data_nloc_groups(self): + """LmdbTestData detects nloc groups in mixed-nloc LMDB.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + self.assertEqual(set(td.nloc_groups.keys()), {6, 9, 12}) + self.assertEqual(len(td.nloc_groups[6]), 4) + self.assertEqual(len(td.nloc_groups[9]), 4) + self.assertEqual(len(td.nloc_groups[12]), 2) + + def test_test_data_get_test_specific_nloc(self): + """get_test(nloc=N) returns only frames with that atom count.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + + result_6 = td.get_test(nloc=6) + self.assertEqual(result_6["coord"].shape, (4, 6 * 3)) + self.assertEqual(result_6["force"].shape, (4, 6 * 3)) + self.assertEqual(result_6["type"].shape, (4, 6)) + + result_9 = td.get_test(nloc=9) + self.assertEqual(result_9["coord"].shape, (4, 9 * 3)) + self.assertEqual(result_9["force"].shape, (4, 9 * 3)) + self.assertEqual(result_9["type"].shape, (4, 9)) + + result_12 = td.get_test(nloc=12) + self.assertEqual(result_12["coord"].shape, (2, 12 * 3)) + self.assertEqual(result_12["force"].shape, (2, 12 * 3)) + self.assertEqual(result_12["type"].shape, (2, 12)) + + def test_test_data_get_test_default_mixed(self): + """get_test() without nloc on mixed data returns largest group.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + # Largest groups are nloc=6 and nloc=9 (both 4 frames). + # max() picks the one with the largest nloc among tied groups. + result = td.get_test() + nframes = result["coord"].shape[0] + self.assertEqual(nframes, 4) + + def test_test_data_get_test_invalid_nloc(self): + """get_test(nloc=999) raises ValueError.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + with self.assertRaises(ValueError): + td.get_test(nloc=999) + + def test_test_data_uniform_nloc_no_warning(self): + """Uniform-nloc LMDB: get_test() returns all frames without warning.""" + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb(f"{tmpdir.name}/uniform.lmdb", nframes=5, natoms=6) + td = LmdbTestData(path, type_map=self._type_map, shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + result = td.get_test() + self.assertEqual(result["coord"].shape, (5, 18)) + tmpdir.cleanup() + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py new file mode 100644 index 0000000000..97d157f96e --- /dev/null +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -0,0 +1,464 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Unit tests for LmdbDataset.""" + +import lmdb +import msgpack +import numpy as np +import pytest +import torch + +from deepmd.dpmodel.utils.lmdb_data import ( + LmdbTestData, + _decode_frame, + _read_metadata, + _remap_keys, + is_lmdb, +) +from deepmd.pt.utils.lmdb_dataset import ( + LmdbDataset, + _collate_lmdb_batch, +) +from deepmd.utils.data import ( + DataRequirementItem, +) + + +def _make_frame(natoms: int = 6, seed: int = 0) -> dict: + """Create a synthetic frame dict as stored in LMDB.""" + rng = np.random.RandomState(seed) + + def _encode_array(arr: np.ndarray) -> dict: + return { + "nd": None, + "type": str(arr.dtype), + "kind": "", + "shape": list(arr.shape), + "data": arr.tobytes(), + } + + return { + "atom_numbs": [natoms // 2, natoms // 2], + "atom_names": ["O", "H"], + "atom_types": _encode_array( + np.array([0] * (natoms // 2) + [1] * (natoms // 2), dtype=np.int64) + ), + "orig": _encode_array(np.zeros(3, dtype=np.float64)), + "cells": _encode_array(rng.randn(3, 3).astype(np.float32)), + "coords": _encode_array(rng.randn(natoms, 3).astype(np.float32)), + "energies": _encode_array(np.array(rng.randn(), dtype=np.float32)), + "forces": _encode_array(rng.randn(natoms, 3).astype(np.float32)), + } + + +def _create_test_lmdb(path: str, nframes: int = 10, natoms: int = 6) -> None: + """Create a minimal LMDB dataset for testing.""" + env = lmdb.open(path, map_size=10 * 1024 * 1024) + fmt = "012d" + metadata = { + "nframes": nframes, + "frame_idx_fmt": fmt, + "system_info": { + "formula": f"O{natoms // 2}H{natoms // 2}", + "natoms": [natoms // 2, natoms // 2], + "nframes": nframes, + }, + } + with env.begin(write=True) as txn: + txn.put(b"__metadata__", msgpack.packb(metadata, use_bin_type=True)) + for i in range(nframes): + key = format(i, fmt).encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + + +@pytest.fixture +def lmdb_dir(tmp_path): + """Create a temporary LMDB dataset.""" + lmdb_path = str(tmp_path / "test.lmdb") + _create_test_lmdb(lmdb_path, nframes=10, natoms=6) + return lmdb_path + + +class TestHelpers: + """Test helper functions.""" + + def test_read_metadata(self, lmdb_dir): + env = lmdb.open(lmdb_dir, readonly=True, lock=False) + with env.begin() as txn: + meta = _read_metadata(txn) + assert meta["nframes"] == 10 + assert "system_info" in meta + env.close() + + def test_read_metadata_missing(self, tmp_path): + empty_path = str(tmp_path / "empty.lmdb") + env = lmdb.open(empty_path, map_size=1024 * 1024) + env.close() + env = lmdb.open(empty_path, readonly=True, lock=False) + with env.begin() as txn: + with pytest.raises(ValueError, match="missing __metadata__"): + _read_metadata(txn) + env.close() + + def test_decode_frame(self, lmdb_dir): + env = lmdb.open(lmdb_dir, readonly=True, lock=False) + with env.begin() as txn: + raw = txn.get(format(0, "012d").encode()) + frame = _decode_frame(raw) + assert "coords" in frame + assert "forces" in frame + assert isinstance(frame["coords"], np.ndarray) + assert frame["coords"].shape == (6, 3) + env.close() + + def test_remap_keys(self): + frame = { + "coords": np.zeros((3, 3)), + "cells": np.zeros((3, 3)), + "energies": np.array(1.0), + "forces": np.zeros((3, 3)), + "atom_types": np.array([0, 1, 0]), + "custom_key": np.array([42.0]), + } + remapped = _remap_keys(frame) + assert "coord" in remapped + assert "box" in remapped + assert "energy" in remapped + assert "force" in remapped + assert "atype" in remapped + assert "custom_key" in remapped # pass-through + assert "coords" not in remapped + + def test_is_lmdb(self, lmdb_dir, tmp_path): + assert is_lmdb(lmdb_dir) is True + assert is_lmdb(str(tmp_path / "nonexistent.lmdb")) is True # ends with .lmdb + assert is_lmdb(str(tmp_path / "nope")) is False + assert is_lmdb(["a", "b"]) is False + assert is_lmdb(42) is False + + +class TestLmdbDataset: + """Test LmdbDataset class.""" + + def test_len(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert len(ds) == 10 + + def test_getitem_keys(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + frame = ds[0] + # Required keys + assert "coord" in frame + assert "box" in frame + assert "energy" in frame + assert "force" in frame + assert "atype" in frame + assert "natoms" in frame + assert "fid" in frame + # find_* flags + assert "find_energy" in frame + assert "find_force" in frame + assert frame["find_energy"] == 1.0 + assert frame["find_force"] == 1.0 + assert frame["find_virial"] == 0.0 + # Metadata keys removed + assert "atom_numbs" not in frame + assert "atom_names" not in frame + assert "orig" not in frame + + def test_getitem_shapes(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + frame = ds[0] + assert frame["coord"].shape == (6, 3) + assert frame["box"].shape == (9,) + assert frame["energy"].shape == (1,) + assert frame["force"].shape == (6, 3) + assert frame["atype"].shape == (6,) + assert frame["natoms"].shape == (4,) # [natoms, natoms, nO, nH] + + def test_getitem_dtypes(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + frame = ds[0] + assert frame["coord"].dtype == np.float64 + assert frame["box"].dtype == np.float64 + assert frame["energy"].dtype == np.float64 + assert frame["force"].dtype == np.float64 + assert frame["atype"].dtype == np.int64 + + def test_getitem_out_of_range(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + with pytest.raises(IndexError): + ds[999] + + def test_natoms_vec(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + frame = ds[0] + natoms = frame["natoms"] + assert natoms[0] == 6 # total natoms + assert natoms[1] == 6 # total natoms (repeated) + assert natoms[2] == 3 # O count + assert natoms[3] == 3 # H count + + def test_auto_batch_size(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size="auto") + # rule=32, natoms=6, 32//6=5, 5*6=30<32 → 6 + assert ds.batch_size == 6 + + def test_auto_batch_size_with_rule(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size="auto:12") + # rule=12, natoms=6, 12//6=2, 2*6=12 → 2 + assert ds.batch_size == 2 + + def test_int_batch_size(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=3) + assert ds.batch_size == 3 + + +class TestTrainerInterface: + """Test Trainer compatibility interface.""" + + def test_systems(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert len(ds.systems) == 1 + assert ds.systems[0] is ds + + def test_dataloaders(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert len(ds.dataloaders) == 1 + + def test_index(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.index == [5] # 10 frames / 2 batch_size + + def test_total_batch(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.total_batch == 5 + + def test_batch_sizes(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.batch_sizes == [2] + + def test_sampler_list(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.sampler_list == [] + + def test_add_data_requirement(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + req = [ + DataRequirementItem("virial", 9, atomic=False, must=False, default=0.0), + ] + ds.add_data_requirement(req) + frame = ds[0] + assert frame["find_virial"] == 0.0 + assert frame["virial"].shape == (9,) + assert np.allclose(frame["virial"], 0.0) + + def test_add_data_requirement_existing_key(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + req = [ + DataRequirementItem("energy", 1, atomic=False, must=True), + ] + ds.add_data_requirement(req) + frame = ds[0] + assert frame["find_energy"] == 1.0 + + def test_preload_noop(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + ds.preload_and_modify_all_data_torch() # should not raise + + def test_set_noise_noop(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + ds.set_noise({}) # should not raise + + +class TestDataLoaderIteration: + """Test DataLoader iteration with LmdbDataset.""" + + def test_batch_iteration(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + from torch.utils.data import ( + DataLoader, + ) + + with torch.device("cpu"): + dl = DataLoader( + ds, + batch_size=2, + shuffle=False, + collate_fn=_collate_lmdb_batch, + ) + batch = next(iter(dl)) + assert "coord" in batch + assert "sid" in batch + assert batch["sid"] == 0 + assert batch["coord"].shape == (2, 6, 3) + assert batch["energy"].shape == (2, 1) + assert batch["force"].shape == (2, 6, 3) + assert batch["atype"].shape == (2, 6) + assert isinstance(batch["fid"], list) + assert len(batch["fid"]) == 2 + assert isinstance(batch["find_energy"], (float, np.floating)) + + def test_inner_dataloader(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + dl = ds.dataloaders[0] + with torch.device("cpu"): + batch = next(iter(dl)) + assert "coord" in batch + assert batch["coord"].shape[0] == 2 + + def test_full_epoch(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=3) + from torch.utils.data import ( + DataLoader, + ) + + with torch.device("cpu"): + dl = DataLoader( + ds, + batch_size=3, + shuffle=False, + collate_fn=_collate_lmdb_batch, + ) + total_frames = 0 + for batch in dl: + total_frames += batch["coord"].shape[0] + # 10 frames, batch_size=3 → 3+3+3+1 = 10 + assert total_frames == 10 + + +class TestCollate: + """Test collate function.""" + + def test_collate_basic(self): + rng = np.random.default_rng(42) + frames = [ + { + "coord": rng.standard_normal((4, 3)), + "energy": np.array([1.0]), + "find_energy": 1.0, + "fid": 0, + }, + { + "coord": rng.standard_normal((4, 3)), + "energy": np.array([2.0]), + "find_energy": 1.0, + "fid": 1, + }, + ] + batch = _collate_lmdb_batch(frames) + assert batch["coord"].shape == (2, 4, 3) + assert batch["energy"].shape == (2, 1) + assert batch["find_energy"] == 1.0 + assert batch["fid"] == [0, 1] + assert batch["sid"] == 0 + + def test_collate_skips_type(self): + frames = [ + {"coord": np.zeros((2, 3)), "type": np.array([0, 1])}, + {"coord": np.zeros((2, 3)), "type": np.array([0, 1])}, + ] + batch = _collate_lmdb_batch(frames) + assert "type" not in batch + + def test_collate_none_values(self): + frames = [ + {"coord": np.zeros((2, 3)), "box": None}, + {"coord": np.zeros((2, 3)), "box": None}, + ] + batch = _collate_lmdb_batch(frames) + assert batch["box"] is None + + +class TestLmdbTestData: + """Test LmdbTestData for dp test support.""" + + def test_get_test_keys(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + td.add("virial", 9, atomic=False, must=False, high_prec=False) + result = td.get_test() + assert "coord" in result + assert "box" in result + assert "type" in result + assert "energy" in result + assert "force" in result + assert "find_energy" in result + assert "find_force" in result + assert "find_virial" in result + + def test_get_test_shapes(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + td.add("virial", 9, atomic=False, must=False, high_prec=False) + result = td.get_test() + nframes = 10 + natoms = 6 + assert result["coord"].shape == (nframes, natoms * 3) + assert result["box"].shape == (nframes, 9) + assert result["type"].shape == (nframes, natoms) + assert result["energy"].shape == (nframes, 1) + assert result["force"].shape == (nframes, natoms * 3) + + def test_get_test_dtypes(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + result = td.get_test() + assert result["coord"].dtype == np.float64 + assert result["box"].dtype == np.float64 + assert result["type"].dtype == np.int64 + assert result["energy"].dtype == np.float64 + assert result["force"].dtype == np.float64 + + def test_get_test_find_flags(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("energy", 1, atomic=False, must=False, high_prec=True) + td.add("force", 3, atomic=True, must=False, high_prec=False) + td.add("virial", 9, atomic=False, must=False, high_prec=False) + result = td.get_test() + assert result["find_energy"] == 1.0 + assert result["find_force"] == 1.0 + assert result["find_virial"] == 0.0 # not in test LMDB data + + def test_get_test_missing_key_default(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("virial", 9, atomic=False, must=False, high_prec=False, default=0.0) + result = td.get_test() + assert result["find_virial"] == 0.0 + assert result["virial"].shape == (10, 9) + assert np.allclose(result["virial"], 0.0) + + def test_get_test_missing_atomic_key(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td.add("atom_ener", 1, atomic=True, must=False, high_prec=False, default=0.0) + result = td.get_test() + assert result["find_atom_ener"] == 0.0 + assert result["atom_ener"].shape == (10, 6) # natoms=6 + assert np.allclose(result["atom_ener"], 0.0) + + def test_pbc(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + assert td.pbc is True + + def test_mixed_type(self, lmdb_dir): + td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + assert td.mixed_type is False + + def test_shuffle(self, lmdb_dir): + td1 = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + td2 = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) + r1 = td1.get_test() + r2 = td2.get_test() + # Without shuffle, results should be identical + np.testing.assert_array_equal(r1["coord"], r2["coord"]) + + def test_type_map_global(self, lmdb_dir): + """Test with a larger global type_map than LMDB data.""" + td = LmdbTestData(lmdb_dir, type_map=["O", "H", "C"], shuffle_test=False) + result = td.get_test() + # type indices should still be 0 and 1 + assert result["type"].max() <= 1 From 5a10d4dd5de9efc2083c8b9d7b806b583bf7f8c2 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:55:21 +0800 Subject: [PATCH 02/22] update distributed sampler --- deepmd/dpmodel/utils/__init__.py | 2 + deepmd/dpmodel/utils/lmdb_data.py | 177 +++++++++++++++++++---- deepmd/pt/train/training.py | 19 ++- deepmd/pt/utils/lmdb_dataset.py | 15 +- source/tests/pt/test_lmdb_dataloader.py | 181 +++++++++++++++++++++++- 5 files changed, 362 insertions(+), 32 deletions(-) diff --git a/deepmd/dpmodel/utils/__init__.py b/deepmd/dpmodel/utils/__init__.py index 93a9ea520c..95a7fe7233 100644 --- a/deepmd/dpmodel/utils/__init__.py +++ b/deepmd/dpmodel/utils/__init__.py @@ -7,6 +7,7 @@ PairExcludeMask, ) from .lmdb_data import ( + DistributedSameNlocBatchSampler, LmdbDataReader, LmdbTestData, SameNlocBatchSampler, @@ -50,6 +51,7 @@ __all__ = [ "AtomExcludeMask", + "DistributedSameNlocBatchSampler", "EmbeddingNet", "EnvMat", "FittingNet", diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 327e260326..ac975e2521 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -6,6 +6,7 @@ """ import logging +import math from collections.abc import ( Iterator, ) @@ -33,6 +34,7 @@ "energies": "energy", "forces": "force", "atom_types": "atype", + "virials": "virial", } @@ -398,6 +400,11 @@ def total_batch(self) -> int: def batch_sizes(self) -> list[int]: return [self.batch_size] + @property + def mixed_type(self) -> bool: + """LMDB datasets are always mixed_type (frames may have different compositions).""" + return True + @property def nloc_groups(self) -> dict[int, list[int]]: """Nloc → list of frame indices.""" @@ -409,6 +416,58 @@ def frame_nlocs(self) -> list[int]: return self._frame_nlocs +def _build_all_batches( + reader: "LmdbDataReader", + shuffle: bool, + rng: np.random.Generator, +) -> list[list[int]]: + """Build the full list of same-nloc batches from the reader. + + This is the shared batch-construction logic used by both + SameNlocBatchSampler (single-GPU) and DistributedSameNlocBatchSampler. + + Parameters + ---------- + reader : LmdbDataReader + Provides nloc_groups and get_batch_size_for_nloc. + shuffle : bool + Whether to shuffle indices within each nloc group and + shuffle the final batch order. + rng : np.random.Generator + Random number generator (deterministic for reproducibility). + + Returns + ------- + list[list[int]] + Each inner list is a batch of frame indices, all with the same nloc. + """ + # Build per-group batches + group_batches: list[list[list[int]]] = [] + for nloc in sorted(reader.nloc_groups.keys()): + indices = list(reader.nloc_groups[nloc]) + if shuffle: + rng.shuffle(indices) + bs = reader.get_batch_size_for_nloc(nloc) + batches = [] + for start in range(0, len(indices), bs): + batches.append(indices[start : start + bs]) + group_batches.append(batches) + + # Interleave groups round-robin + all_batches: list[list[int]] = [] + max_len = max(len(gb) for gb in group_batches) if group_batches else 0 + for i in range(max_len): + for gb in group_batches: + if i < len(gb): + all_batches.append(gb[i]) + + # Optionally shuffle the interleaved order + if shuffle: + rng.shuffle(all_batches) + + return all_batches + + class SameNlocBatchSampler: """Batch sampler that groups frames by nloc. @@ -418,6 +477,9 @@ class SameNlocBatchSampler: When auto batch_size is used, batch_size is computed per-nloc-group. + The sampler is deterministic: given the same seed, repeated calls to + ``__iter__`` produce the same batch sequence. + Parameters ---------- reader : LmdbDataReader @@ -436,43 +498,108 @@ def __init__( ) -> None: self._reader = reader self._shuffle = shuffle - self._rng = np.random.default_rng(seed) + self._seed = seed def __iter__(self) -> Iterator[list[int]]: """Yield batches of frame indices, all with the same nloc.""" - # Build per-group batches - group_batches: list[list[list[int]]] = [] - for nloc in sorted(self._reader.nloc_groups.keys()): - indices = list(self._reader.nloc_groups[nloc]) - if self._shuffle: - self._rng.shuffle(indices) + rng = np.random.default_rng(self._seed) + yield from _build_all_batches(self._reader, self._shuffle, rng) + + def __len__(self) -> int: + """Total number of batches across all nloc groups.""" + total = 0 + for nloc, indices in self._reader.nloc_groups.items(): bs = self._reader.get_batch_size_for_nloc(nloc) - batches = [] - for start in range(0, len(indices), bs): - batches.append(indices[start : start + bs]) - group_batches.append(batches) + total += (len(indices) + bs - 1) // bs + return total + - # Interleave groups round-robin - all_batches: list[list[int]] = [] - max_len = max(len(gb) for gb in group_batches) if group_batches else 0 - for i in range(max_len): - for gb in group_batches: - if i < len(gb): - all_batches.append(gb[i]) +class DistributedSameNlocBatchSampler: + """Distributed wrapper for same-nloc batch sampling. - # Optionally shuffle the interleaved order - if self._shuffle: - self._rng.shuffle(all_batches) + All ranks build the same deterministic global batch list (using + ``seed + epoch``), then each rank takes a disjoint subset via + :meth:`_partition_batches`. - yield from all_batches + Override :meth:`_partition_batches` for custom load-balancing strategies. + The default uses strided partitioning which gives good nloc diversity per + rank. + + Parameters + ---------- + reader : LmdbDataReader + The dataset reader (provides nloc_groups, get_batch_size_for_nloc, + frame_nlocs). + rank : int + Rank of the current process. + world_size : int + Total number of processes. + shuffle : bool + Whether to shuffle batches. + seed : int or None + Base seed for deterministic RNG. All ranks must use the same seed. + """ + + def __init__( + self, + reader: LmdbDataReader, + rank: int, + world_size: int, + shuffle: bool = True, + seed: int | None = None, + ) -> None: + self._reader = reader + self._rank = rank + self._world_size = world_size + self._shuffle = shuffle + self._seed = seed if seed is not None else 0 + self._epoch = 0 + + def set_epoch(self, epoch: int) -> None: + """Set epoch for deterministic cross-rank shuffling. + + Call this before each training epoch/cycle to get different but + reproducible batch orderings across epochs. + """ + self._epoch = epoch + + def __iter__(self) -> Iterator[list[int]]: + """Yield this rank's partition of the global batch list.""" + # All ranks build the same global batch list deterministically + rng = np.random.default_rng(self._seed + self._epoch) + all_batches = _build_all_batches(self._reader, self._shuffle, rng) + # Partition to this rank + yield from self._partition_batches(all_batches) + + def _partition_batches(self, all_batches: list[list[int]]) -> list[list[int]]: + """Partition global batches to this rank. + + Default: strided partition ``all_batches[rank::world_size]``. + This gives good nloc diversity per rank since batches are + interleaved across nloc groups before shuffling. + + Override this method for custom load-balancing. For example, a + greedy algorithm could assign batches to ranks based on estimated + compute cost (``reader.frame_nlocs[batch[0]]`` gives the nloc of + each batch). + """ + return all_batches[self._rank :: self._world_size] def __len__(self) -> int: - """Total number of batches across all nloc groups.""" + """Number of batches for this rank.""" total = 0 for nloc, indices in self._reader.nloc_groups.items(): bs = self._reader.get_batch_size_for_nloc(nloc) total += (len(indices) + bs - 1) // bs - return total + return math.ceil(total / self._world_size) + + @property + def rank(self) -> int: + return self._rank + + @property + def world_size(self) -> int: + return self._world_size class LmdbTestData: @@ -536,7 +663,7 @@ def __init__( elif isinstance(f0["box"], np.ndarray) and np.allclose(f0["box"], 0.0): self.pbc = False - self.mixed_type = False + self.mixed_type = True @property def nloc_groups(self) -> dict[int, list[int]]: diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 04d13831d1..ef27b45435 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -262,9 +262,22 @@ def get_dataloader_and_iter_lmdb( SameNlocBatchSampler, ) - _batch_sampler = _SameNlocBatchSamplerTorch( - SameNlocBatchSampler(_data._reader, shuffle=True) - ) + if self.world_size > 1: + from deepmd.dpmodel.utils.lmdb_data import ( + DistributedSameNlocBatchSampler, + ) + + _inner_sampler = DistributedSameNlocBatchSampler( + _data._reader, + rank=self.rank, + world_size=self.world_size, + shuffle=True, + seed=_training_params.get("seed", None), + ) + else: + _inner_sampler = SameNlocBatchSampler(_data._reader, shuffle=True) + + _batch_sampler = _SameNlocBatchSamplerTorch(_inner_sampler) _dataloader = DataLoader( _data, batch_sampler=_batch_sampler, diff --git a/deepmd/pt/utils/lmdb_dataset.py b/deepmd/pt/utils/lmdb_dataset.py index 2568d8efc9..04bfb03190 100644 --- a/deepmd/pt/utils/lmdb_dataset.py +++ b/deepmd/pt/utils/lmdb_dataset.py @@ -82,7 +82,8 @@ class _SameNlocBatchSamplerTorch(Sampler): """Torch Sampler adapter around the framework-agnostic SameNlocBatchSampler. PyTorch DataLoader with batch_sampler expects a Sampler that yields - lists of indices. This wraps SameNlocBatchSampler to satisfy that. + lists of indices. This wraps SameNlocBatchSampler (or + DistributedSameNlocBatchSampler) to satisfy that. """ def __init__(self, inner: SameNlocBatchSampler) -> None: @@ -94,6 +95,11 @@ def __iter__(self) -> Iterator[list[int]]: def __len__(self) -> int: return len(self._inner) + def set_epoch(self, epoch: int) -> None: + """Forward set_epoch to inner sampler if it supports it.""" + if hasattr(self._inner, "set_epoch"): + self._inner.set_epoch(epoch) + class LmdbDataset(Dataset): """PyTorch Dataset backed by LMDB via LmdbDataReader. @@ -180,6 +186,11 @@ def nframes(self) -> int: def mixed_batch(self) -> bool: return self._reader.mixed_batch + @property + def mixed_type(self) -> bool: + """LMDB datasets are always mixed_type.""" + return self._reader.mixed_type + @property def batch_size(self) -> int: return self._reader.batch_size @@ -226,4 +237,4 @@ def dataloaders(self) -> list: @property def sampler_list(self) -> list: - return [] + return [self._batch_sampler] diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index 97d157f96e..ed4f14152b 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -8,7 +8,10 @@ import torch from deepmd.dpmodel.utils.lmdb_data import ( + DistributedSameNlocBatchSampler, + LmdbDataReader, LmdbTestData, + SameNlocBatchSampler, _decode_frame, _read_metadata, _remap_keys, @@ -241,7 +244,7 @@ def test_batch_sizes(self, lmdb_dir): def test_sampler_list(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - assert ds.sampler_list == [] + assert len(ds.sampler_list) == 1 def test_add_data_requirement(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) @@ -446,7 +449,7 @@ def test_pbc(self, lmdb_dir): def test_mixed_type(self, lmdb_dir): td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - assert td.mixed_type is False + assert td.mixed_type is True def test_shuffle(self, lmdb_dir): td1 = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) @@ -462,3 +465,177 @@ def test_type_map_global(self, lmdb_dir): result = td.get_test() # type indices should still be 0 and 1 assert result["type"].max() <= 1 + + +def _create_multi_nloc_lmdb(path: str) -> None: + """Create an LMDB with frames of varying nloc for distributed tests.""" + env = lmdb.open(path, map_size=10 * 1024 * 1024) + fmt = "012d" + # 30 frames: 10 with nloc=4, 10 with nloc=6, 10 with nloc=8 + nframes = 30 + frame_nlocs = [] + with env.begin(write=True) as txn: + idx = 0 + for natoms in [4, 6, 8]: + for i in range(10): + key = format(idx, fmt).encode() + frame = _make_frame(natoms=natoms, seed=idx * 100) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + frame_nlocs.append(natoms) + idx += 1 + metadata = { + "nframes": nframes, + "frame_idx_fmt": fmt, + "frame_nlocs": frame_nlocs, + } + txn.put(b"__metadata__", msgpack.packb(metadata, use_bin_type=True)) + env.close() + + +@pytest.fixture +def multi_nloc_lmdb(tmp_path): + """Create LMDB with multiple nloc groups for distributed tests.""" + lmdb_path = str(tmp_path / "multi_nloc.lmdb") + _create_multi_nloc_lmdb(lmdb_path) + return lmdb_path + + +class TestMixedTypeProperty: + """Test mixed_type property on LMDB classes.""" + + def test_lmdb_data_reader_mixed_type(self, lmdb_dir): + reader = LmdbDataReader(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert reader.mixed_type is True + + def test_lmdb_dataset_mixed_type(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.mixed_type is True + + +class TestDistributedSameNlocBatchSampler: + """Test DistributedSameNlocBatchSampler (pure logic, no torch.distributed).""" + + def test_disjoint_batches(self, multi_nloc_lmdb): + """Two ranks produce disjoint frame index sets.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + s0 = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + s1 = DistributedSameNlocBatchSampler( + reader, rank=1, world_size=2, shuffle=True, seed=42 + ) + frames0 = set() + for batch in s0: + frames0.update(batch) + frames1 = set() + for batch in s1: + frames1.update(batch) + # No overlap + assert frames0 & frames1 == set() + + def test_covers_all_frames(self, multi_nloc_lmdb): + """Union of all ranks covers all frames.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + s0 = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + s1 = DistributedSameNlocBatchSampler( + reader, rank=1, world_size=2, shuffle=True, seed=42 + ) + all_frames = set() + for batch in s0: + all_frames.update(batch) + for batch in s1: + all_frames.update(batch) + assert all_frames == set(range(30)) + + def test_len(self, multi_nloc_lmdb): + """__len__ returns approximately total // world_size.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + single = SameNlocBatchSampler(reader, shuffle=False) + total = len(single) + dist_s = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=False, seed=0 + ) + import math + + assert len(dist_s) == math.ceil(total / 2) + + def test_deterministic(self, multi_nloc_lmdb): + """Same parameters produce same batch sequence.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + s1 = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + s2 = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + batches1 = list(s1) + batches2 = list(s2) + assert batches1 == batches2 + + def test_set_epoch_changes_order(self, multi_nloc_lmdb): + """Different epochs produce different batch orderings.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + s = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + s.set_epoch(0) + batches_e0 = list(s) + s.set_epoch(1) + batches_e1 = list(s) + # Should produce different orderings + assert batches_e0 != batches_e1 + + def test_single_gpu_fallback(self, multi_nloc_lmdb): + """world_size=1 produces same frames as SameNlocBatchSampler.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + single = SameNlocBatchSampler(reader, shuffle=True, seed=42) + dist_single = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=1, shuffle=True, seed=42 + ) + frames_single = set() + for batch in single: + frames_single.update(batch) + frames_dist = set() + for batch in dist_single: + frames_dist.update(batch) + # Both should cover all frames + assert frames_single == frames_dist == set(range(30)) + + def test_partition_batches_overridable(self, multi_nloc_lmdb): + """Subclass can override _partition_batches for custom load balancing.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + + class ReversePartition(DistributedSameNlocBatchSampler): + def _partition_batches(self, all_batches): + # Take the complementary slice + return all_batches[ + self._world_size - 1 - self._rank :: self._world_size + ] + + s_default = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + s_custom = ReversePartition(reader, rank=0, world_size=2, shuffle=True, seed=42) + # Custom should get rank=1's batches (since it reverses) + s_rank1 = DistributedSameNlocBatchSampler( + reader, rank=1, world_size=2, shuffle=True, seed=42 + ) + frames_custom = set() + for batch in s_custom: + frames_custom.update(batch) + frames_rank1 = set() + for batch in s_rank1: + frames_rank1.update(batch) + assert frames_custom == frames_rank1 + + def test_same_nloc_per_batch(self, multi_nloc_lmdb): + """Each batch from distributed sampler has frames with same nloc.""" + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) + s = DistributedSameNlocBatchSampler( + reader, rank=0, world_size=2, shuffle=True, seed=42 + ) + for batch in s: + nlocs = [reader.frame_nlocs[idx] for idx in batch] + assert len(set(nlocs)) == 1, f"Mixed nloc in batch: {nlocs}" From 599d221288d3b599985fc8497c1a0f34d2bc3780 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:41:16 +0800 Subject: [PATCH 03/22] Update pyproject.toml --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2b6810a88c..4111708da8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,8 @@ dependencies = [ 'ml_dtypes', 'mendeleev', 'array-api-compat', + 'lmdb', + 'msgpack', ] requires-python = ">=3.10" keywords = ["deepmd"] From f91ba7dae5ca2b36c0bb8b12936db504f46a7fe1 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:43:43 +0800 Subject: [PATCH 04/22] resolve dtype --- deepmd/dpmodel/utils/lmdb_data.py | 134 +++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 20 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index ac975e2521..6910dd5a10 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -21,6 +21,10 @@ import msgpack import numpy as np +from deepmd.env import ( + GLOBAL_ENER_FLOAT_PRECISION, + GLOBAL_NP_FLOAT_PRECISION, +) from deepmd.utils.data import ( DataRequirementItem, ) @@ -37,6 +41,10 @@ "virials": "virial", } +# Keys whose high_prec is always True in the standard pipeline +# (energy is set by Loss DataRequirementItem; reduce() also sets high_prec=True) +_HIGH_PREC_KEYS = frozenset({"energy"}) + def _open_lmdb(path: str) -> lmdb.Environment: """Open LMDB environment readonly.""" @@ -267,6 +275,34 @@ def _compute_natoms_vec(self, atype: np.ndarray) -> np.ndarray: vec[2:] = counts return vec + def _resolve_dtype(self, key: str) -> np.dtype: + """Resolve the target numpy dtype for a given key. + + Priority: DataRequirementItem.dtype > DataRequirementItem.high_prec > + built-in defaults (energy=high, others=normal). + """ + if key in self._data_requirements: + req = self._data_requirements[key] + # Support both DataRequirementItem objects and plain dicts + if isinstance(req, dict): + dtype = req.get("dtype") + if dtype is not None: + return dtype + if req.get("high_prec", False): + return GLOBAL_ENER_FLOAT_PRECISION + return GLOBAL_NP_FLOAT_PRECISION + else: + # DataRequirementItem object + if hasattr(req, "dtype") and req.dtype is not None: + return req.dtype + if hasattr(req, "high_prec") and req.high_prec: + return GLOBAL_ENER_FLOAT_PRECISION + return GLOBAL_NP_FLOAT_PRECISION + # Fall back to built-in defaults + if key in _HIGH_PREC_KEYS: + return GLOBAL_ENER_FLOAT_PRECISION + return GLOBAL_NP_FLOAT_PRECISION + def get_batch_size_for_nloc(self, nloc: int) -> int: """Get batch_size for a given nloc. Uses auto rule if configured.""" if self._auto_rule is not None: @@ -291,21 +327,29 @@ def __getitem__(self, index: int) -> dict[str, Any]: # Flatten arrays to match DeePMD convention if "coord" in frame and isinstance(frame["coord"], np.ndarray): - frame["coord"] = frame["coord"].reshape(-1, 3).astype(np.float64) + frame["coord"] = ( + frame["coord"].reshape(-1, 3).astype(self._resolve_dtype("coord")) + ) if "box" in frame and isinstance(frame["box"], np.ndarray): - frame["box"] = frame["box"].reshape(9).astype(np.float64) + frame["box"] = frame["box"].reshape(9).astype(self._resolve_dtype("box")) if "energy" in frame: val = frame["energy"] if isinstance(val, np.ndarray): - frame["energy"] = val.reshape(1).astype(np.float64) + frame["energy"] = val.reshape(1).astype(self._resolve_dtype("energy")) else: - frame["energy"] = np.array([float(val)], dtype=np.float64) + frame["energy"] = np.array( + [float(val)], dtype=self._resolve_dtype("energy") + ) if "force" in frame and isinstance(frame["force"], np.ndarray): - frame["force"] = frame["force"].reshape(-1, 3).astype(np.float64) + frame["force"] = ( + frame["force"].reshape(-1, 3).astype(self._resolve_dtype("force")) + ) if "atype" in frame and isinstance(frame["atype"], np.ndarray): frame["atype"] = frame["atype"].reshape(-1).astype(np.int64) if "virial" in frame and isinstance(frame["virial"], np.ndarray): - frame["virial"] = frame["virial"].reshape(9).astype(np.float64) + frame["virial"] = ( + frame["virial"].reshape(9).astype(self._resolve_dtype("virial")) + ) # Per-frame natoms_vec from atype atype = frame.get("atype") @@ -340,14 +384,34 @@ def __getitem__(self, index: int) -> dict[str, Any]: for req_key, req_item in self._data_requirements.items(): if req_key not in frame: frame[f"find_{req_key}"] = np.float32(0.0) - ndof = req_item["ndof"] - default = req_item["default"] - atomic = req_item["atomic"] + # Support both dict and DataRequirementItem object + if isinstance(req_item, dict): + ndof = req_item["ndof"] + default = req_item["default"] + atomic = req_item["atomic"] + req_dtype = req_item.get("dtype") + if req_dtype is None: + req_dtype = ( + GLOBAL_ENER_FLOAT_PRECISION + if req_item.get("high_prec", False) + else GLOBAL_NP_FLOAT_PRECISION + ) + else: + ndof = req_item.ndof + default = req_item.default + atomic = req_item.atomic + req_dtype = req_item.dtype + if req_dtype is None: + req_dtype = ( + GLOBAL_ENER_FLOAT_PRECISION + if req_item.high_prec + else GLOBAL_NP_FLOAT_PRECISION + ) if atomic: shape = (frame_natoms, ndof) else: shape = (ndof,) - frame[req_key] = np.full(shape, default, dtype=np.float64) + frame[req_key] = np.full(shape, default, dtype=req_dtype) elif f"find_{req_key}" not in frame: frame[f"find_{req_key}"] = np.float32(1.0) @@ -679,6 +743,7 @@ def add( high_prec: bool = False, repeat: int = 1, default: float = 0.0, + dtype: np.dtype | None = None, **kwargs: Any, ) -> None: """Register a data requirement (mirrors DeepmdData.add).""" @@ -689,8 +754,23 @@ def add( "high_prec": high_prec, "repeat": repeat, "default": default, + "dtype": dtype, } + def _resolve_dtype(self, key: str) -> np.dtype: + """Resolve target dtype for a key using registered requirements.""" + if key in self._requirements: + req = self._requirements[key] + dtype = req.get("dtype") + if dtype is not None: + return dtype + if req.get("high_prec", False): + return GLOBAL_ENER_FLOAT_PRECISION + return GLOBAL_NP_FLOAT_PRECISION + if key in _HIGH_PREC_KEYS: + return GLOBAL_ENER_FLOAT_PRECISION + return GLOBAL_NP_FLOAT_PRECISION + def get_test(self, nloc: int | None = None) -> dict[str, Any]: """Return frames stacked as numpy arrays. @@ -741,18 +821,28 @@ def _stack_frames( for frame in frames: if "coord" in frame and isinstance(frame["coord"], np.ndarray): - coords.append(frame["coord"].reshape(natoms * 3).astype(np.float64)) + coords.append( + frame["coord"] + .reshape(natoms * 3) + .astype(self._resolve_dtype("coord")) + ) if "box" in frame and isinstance(frame["box"], np.ndarray): - boxes.append(frame["box"].reshape(9).astype(np.float64)) + boxes.append(frame["box"].reshape(9).astype(self._resolve_dtype("box"))) else: - boxes.append(np.zeros(9, dtype=np.float64)) + boxes.append(np.zeros(9, dtype=self._resolve_dtype("box"))) if "atype" in frame and isinstance(frame["atype"], np.ndarray): atypes.append(frame["atype"].reshape(natoms).astype(np.int64)) result["coord"] = ( - np.stack(coords) if coords else np.zeros((0, natoms * 3), dtype=np.float64) + np.stack(coords) + if coords + else np.zeros((0, natoms * 3), dtype=self._resolve_dtype("coord")) + ) + result["box"] = ( + np.stack(boxes) + if boxes + else np.zeros((0, 9), dtype=self._resolve_dtype("box")) ) - result["box"] = np.stack(boxes) if boxes else np.zeros((0, 9), dtype=np.float64) result["type"] = ( np.stack(atypes) if atypes else np.zeros((0, natoms), dtype=np.int64) ) @@ -787,9 +877,11 @@ def _stack_frames( for frame in frames: val = frame.get(key) if isinstance(val, np.ndarray): - arrays.append(val.astype(np.float64).ravel()) + arrays.append(val.astype(self._resolve_dtype(key)).ravel()) elif val is not None: - arrays.append(np.array([float(val)], dtype=np.float64)) + arrays.append( + np.array([float(val)], dtype=self._resolve_dtype(key)) + ) else: ref = next( ( @@ -800,9 +892,11 @@ def _stack_frames( None, ) if ref is not None: - arrays.append(np.zeros(ref.size, dtype=np.float64)) + arrays.append( + np.zeros(ref.size, dtype=self._resolve_dtype(key)) + ) else: - arrays.append(np.zeros(1, dtype=np.float64)) + arrays.append(np.zeros(1, dtype=self._resolve_dtype(key))) result[key] = np.stack(arrays) elif key in self._requirements: ndof = self._requirements[key]["ndof"] @@ -812,7 +906,7 @@ def _stack_frames( shape = (nframes, natoms * ndof) else: shape = (nframes, ndof) - result[key] = np.full(shape, default, dtype=np.float64) + result[key] = np.full(shape, default, dtype=self._resolve_dtype(key)) return result From d9a4db47f9593667c620b3f291d734f8a4222aaf Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:50:33 +0800 Subject: [PATCH 05/22] fix type map --- deepmd/dpmodel/utils/lmdb_data.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 6910dd5a10..61a440f149 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -222,6 +222,26 @@ def __init__( self._natoms = sum(self._natoms_per_type) self._ntypes = len(type_map) + # Build type remapping if LMDB's type_map differs from model's type_map + lmdb_type_map = meta.get("type_map") + self._lmdb_type_map = lmdb_type_map + self._type_remap: np.ndarray | None = None + if lmdb_type_map is not None and list(lmdb_type_map) != list(type_map): + # Build remap: lmdb_type_idx -> model_type_idx + remap = np.empty(len(lmdb_type_map), dtype=np.int32) + for i, name in enumerate(lmdb_type_map): + if name not in type_map: + raise ValueError( + f"Element '{name}' in LMDB type_map {lmdb_type_map} " + f"not found in model type_map {type_map}" + ) + remap[i] = type_map.index(name) + self._type_remap = remap + log.info( + f"Type remapping: LMDB {lmdb_type_map} -> model {type_map}, " + f"remap={list(remap)}" + ) + # Persistent read-only transaction for __getitem__ (avoids per-read overhead). # Safe because we use num_workers=0 in DataLoader. self._txn = self._env.begin() @@ -346,6 +366,9 @@ def __getitem__(self, index: int) -> dict[str, Any]: ) if "atype" in frame and isinstance(frame["atype"], np.ndarray): frame["atype"] = frame["atype"].reshape(-1).astype(np.int64) + # Remap atom types from LMDB's type_map to model's type_map + if self._type_remap is not None: + frame["atype"] = self._type_remap[frame["atype"]].astype(np.int64) if "virial" in frame and isinstance(frame["virial"], np.ndarray): frame["virial"] = ( frame["virial"].reshape(9).astype(self._resolve_dtype("virial")) From 1a1f41d453abd5ea582b425eaf4c9d38b6989ee6 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:09:32 +0800 Subject: [PATCH 06/22] fix type map for test --- deepmd/dpmodel/utils/lmdb_data.py | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 61a440f149..821f357bba 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -716,6 +716,29 @@ def __init__( self.nframes, self._frame_fmt, self._natoms_per_type = _parse_metadata(meta) self._natoms = sum(self._natoms_per_type) + # Build type remapping if LMDB's type_map differs from model's type_map + lmdb_type_map = meta.get("type_map") + self._lmdb_type_map = lmdb_type_map + self._type_remap: np.ndarray | None = None + if ( + lmdb_type_map is not None + and self._type_map + and list(lmdb_type_map) != list(self._type_map) + ): + remap = np.empty(len(lmdb_type_map), dtype=np.int32) + for i, name in enumerate(lmdb_type_map): + if name not in self._type_map: + raise ValueError( + f"Element '{name}' in LMDB type_map {lmdb_type_map} " + f"not found in model type_map {self._type_map}" + ) + remap[i] = self._type_map.index(name) + self._type_remap = remap + log.info( + f"LmdbTestData type remapping: LMDB {lmdb_type_map} -> " + f"model {self._type_map}, remap={list(remap)}" + ) + # Read all frames self._frames: list[dict[str, Any]] = [] with self._env.begin() as txn: @@ -723,7 +746,17 @@ def __init__( key = format(i, self._frame_fmt).encode() raw = txn.get(key) if raw is not None: - self._frames.append(_remap_keys(_decode_frame(raw))) + frame = _remap_keys(_decode_frame(raw)) + # Apply type remapping to atype + if ( + self._type_remap is not None + and "atype" in frame + and isinstance(frame["atype"], np.ndarray) + ): + frame["atype"] = self._type_remap[ + frame["atype"].reshape(-1) + ].astype(np.int64) + self._frames.append(frame) # Shuffle if requested if shuffle_test: From b14c6cc9b3ba5ef959062019dff83e354180bbff Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:12:18 +0800 Subject: [PATCH 07/22] Update lmdb_data.py --- deepmd/dpmodel/utils/lmdb_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 821f357bba..3d9dafd42e 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -239,7 +239,7 @@ def __init__( self._type_remap = remap log.info( f"Type remapping: LMDB {lmdb_type_map} -> model {type_map}, " - f"remap={list(remap)}" + f"remap={remap}" ) # Persistent read-only transaction for __getitem__ (avoids per-read overhead). @@ -736,7 +736,7 @@ def __init__( self._type_remap = remap log.info( f"LmdbTestData type remapping: LMDB {lmdb_type_map} -> " - f"model {self._type_map}, remap={list(remap)}" + f"model {self._type_map}, remap={remap}" ) # Read all frames From 81ee6c65d7575458f9d7812685e3b5a381456399 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:53:01 +0800 Subject: [PATCH 08/22] add autoprob --- deepmd/dpmodel/utils/lmdb_data.py | 281 +++++++++++- deepmd/pt/entrypoints/main.py | 2 + deepmd/pt/train/training.py | 9 +- deepmd/pt/utils/lmdb_dataset.py | 61 ++- source/tests/consistent/test_lmdb_data.py | 499 ++++++++++++++++++++++ source/tests/pt/test_lmdb_dataloader.py | 334 +++++++++++++++ 6 files changed, 1180 insertions(+), 6 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 3d9dafd42e..d8fa454b5b 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -265,6 +265,23 @@ def __init__( self._frame_nlocs = [] self._nloc_groups = {} + # Parse frame_system_ids for auto_prob support + meta_sys_ids = meta.get("frame_system_ids") + if meta_sys_ids is not None: + self._frame_system_ids: list[int] | None = [int(s) for s in meta_sys_ids] + self._nsystems = max(self._frame_system_ids) + 1 + self._system_groups: dict[int, list[int]] = {} + for idx, sid in enumerate(self._frame_system_ids): + self._system_groups.setdefault(sid, []).append(idx) + self._system_nframes: list[int] = [ + len(self._system_groups.get(i, [])) for i in range(self._nsystems) + ] + else: + self._frame_system_ids = None + self._nsystems = 1 + self._system_groups = {0: list(range(self.nframes))} + self._system_nframes = [self.nframes] + # Parse batch_size spec self._auto_rule: int | None = None if isinstance(batch_size, str): @@ -502,11 +519,196 @@ def frame_nlocs(self) -> list[int]: """Per-frame atom count.""" return self._frame_nlocs + @property + def nsystems(self) -> int: + """Number of original systems merged into this LMDB.""" + return self._nsystems + + @property + def frame_system_ids(self) -> list[int] | None: + """Per-frame system index, or None if not available.""" + return self._frame_system_ids + + @property + def system_groups(self) -> dict[int, list[int]]: + """System index → list of frame indices.""" + return self._system_groups + + @property + def system_nframes(self) -> list[int]: + """Number of frames per system.""" + return self._system_nframes + + +def compute_block_targets( + auto_prob_style: str, + nsystems: int, + system_nframes: list[int], +) -> list[tuple[list[int], int]]: + """Compute target frame count per block from auto_prob config. + + Uses the same ``prob_sys_size_ext`` logic as the npy pipeline to parse + the ``auto_prob`` string, then converts per-system probabilities into + per-block target frame counts using the "max(frames/prob)" strategy. + + Parameters + ---------- + auto_prob_style : str + e.g. ``"prob_sys_size;0:3:0.5;3:10:0.5"`` + nsystems : int + Total number of systems in the LMDB. + system_nframes : list[int] + Number of frames per system. + + Returns + ------- + list[tuple[list[int], int]] + Each element is ``(system_indices_in_block, target_frame_count)``. + Returns empty list if no expansion is needed (all targets == actual). + """ + from deepmd.utils.data_system import ( + prob_sys_size_ext, + ) + + # Parse block definitions from the auto_prob string + # Format: "prob_sys_size;stt:end:weight;stt:end:weight;..." + block_str = auto_prob_style.split(";")[1:] + blocks: list[tuple[int, int, float]] = [] + for part in block_str: + stt, end, weight = part.split(":") + blocks.append((int(stt), int(end), float(weight))) + + # Compute per-system probabilities using the standard function + sys_probs = prob_sys_size_ext(auto_prob_style, nsystems, system_nframes) + + # Group systems by block, compute block-level frames and prob + block_info: list[tuple[list[int], int, float]] = [] # (sys_ids, frames, prob) + for stt, end, _weight in blocks: + sys_ids = list(range(stt, end)) + block_frames = sum(system_nframes[i] for i in sys_ids) + block_prob = sum(sys_probs[i] for i in sys_ids) + block_info.append((sys_ids, block_frames, block_prob)) + + # Step 1-2: total_target = ceil(max(block_frames / block_prob)) + ratios = [] + for sys_ids, block_frames, block_prob in block_info: + if block_prob > 0: + ratios.append(block_frames / block_prob) + else: + ratios.append(0.0) + total_target = math.ceil(max(ratios)) if ratios else 0 + + # Step 3: per-block target = round(total_target * block_prob) + result: list[tuple[list[int], int]] = [] + needs_expansion = False + for sys_ids, block_frames, block_prob in block_info: + target = round(total_target * block_prob) + target = max(target, block_frames) # never shrink + if target > block_frames: + needs_expansion = True + result.append((sys_ids, target)) + + if not needs_expansion: + return [] + + return result + + +def _expand_indices_by_blocks( + indices: list[int], + frame_system_ids: list[int], + block_targets: list[tuple[list[int], int]], + rng: np.random.Generator, +) -> list[int]: + """Expand frame indices according to block targets. + + For each block, computes the proportional target for the subset of + indices belonging to that block (within the current nloc group), + then applies full-copy + remainder sampling. + + Parameters + ---------- + indices : list[int] + Frame indices in the current nloc group. + frame_system_ids : list[int] + Per-frame system id for the entire dataset. + block_targets : list[tuple[list[int], int]] + Per-block (system_ids, total_target_frames). + rng : np.random.Generator + RNG for remainder sampling. + + Returns + ------- + list[int] + Expanded indices. + """ + # Build sys_id -> block_idx mapping + sys_to_block: dict[int, int] = {} + for blk_idx, (sys_ids, _target) in enumerate(block_targets): + for sid in sys_ids: + sys_to_block[sid] = blk_idx + + # Partition indices by block + block_indices: dict[int, list[int]] = {i: [] for i in range(len(block_targets))} + unassigned: list[int] = [] + for idx in indices: + sid = frame_system_ids[idx] + blk = sys_to_block.get(sid) + if blk is not None: + block_indices[blk].append(idx) + else: + unassigned.append(idx) + + # Compute total actual frames across all blocks (for proportional scaling) + total_actual = sum(len(block_indices[i]) for i in range(len(block_targets))) + total_target_all = sum(t for _, t in block_targets) + + expanded: list[int] = list(unassigned) + + for blk_idx, (sys_ids, block_total_target) in enumerate(block_targets): + blk_idxs = block_indices[blk_idx] + n_actual = len(blk_idxs) + if n_actual == 0: + continue + + # Proportional target for this nloc subset of the block + # block_total_target is for the entire block; scale by the fraction + # of block frames that fall in this nloc group + _, block_total_target_all = block_targets[blk_idx] + # Get total frames in this block across all nloc groups + block_total_actual = sum( + 1 + for i in range(len(frame_system_ids)) + if frame_system_ids[i] in set(sys_ids) + ) + if block_total_actual > 0: + target = round(block_total_target_all * n_actual / block_total_actual) + else: + target = n_actual + target = max(target, n_actual) # never shrink + + # Full copies + remainder + deficit = target - n_actual + if deficit <= 0: + expanded.extend(blk_idxs) + else: + full_copies = deficit // n_actual + remainder = deficit % n_actual + # Original + full copies + expanded.extend(blk_idxs * (1 + full_copies)) + # Remainder: sample without replacement + if remainder > 0: + sampled = rng.choice(blk_idxs, size=remainder, replace=False) + expanded.extend(sampled.tolist()) + + return expanded + def _build_all_batches( reader: "LmdbDataReader", shuffle: bool, rng: np.random.Generator, + block_targets: list[tuple[list[int], int]] | None = None, ) -> list[list[int]]: """Build the full list of same-nloc batches from the reader. @@ -522,6 +724,9 @@ def _build_all_batches( shuffle the final batch order. rng : np.random.Generator Random number generator (deterministic for reproducibility). + block_targets : list[tuple[list[int], int]] or None + Per-block (system_ids, target_frame_count) from compute_block_targets. + When provided, indices are expanded via full-copy + remainder sampling. Returns ------- @@ -532,6 +737,11 @@ def _build_all_batches( group_batches: list[list[list[int]]] = [] for nloc in sorted(reader.nloc_groups.keys()): indices = list(reader.nloc_groups[nloc]) + # Expand indices by block targets if provided + if block_targets and reader.frame_system_ids is not None: + indices = _expand_indices_by_blocks( + indices, reader.frame_system_ids, block_targets, rng + ) if shuffle: rng.shuffle(indices) bs = reader.get_batch_size_for_nloc(nloc) @@ -575,6 +785,8 @@ class SameNlocBatchSampler: Whether to shuffle within each nloc group each epoch. seed : int or None Random seed for reproducibility. + block_targets : list[tuple[list[int], int]] or None + Per-block expansion targets from compute_block_targets. """ def __init__( @@ -582,22 +794,54 @@ def __init__( reader: LmdbDataReader, shuffle: bool = True, seed: int | None = None, + block_targets: list[tuple[list[int], int]] | None = None, ) -> None: self._reader = reader self._shuffle = shuffle self._seed = seed + self._block_targets = block_targets def __iter__(self) -> Iterator[list[int]]: """Yield batches of frame indices, all with the same nloc.""" rng = np.random.default_rng(self._seed) - yield from _build_all_batches(self._reader, self._shuffle, rng) + yield from _build_all_batches( + self._reader, self._shuffle, rng, self._block_targets + ) def __len__(self) -> int: - """Total number of batches across all nloc groups.""" + """Total number of batches across all nloc groups (estimated).""" total = 0 for nloc, indices in self._reader.nloc_groups.items(): + n = len(indices) + if self._block_targets and self._reader.frame_system_ids is not None: + # Estimate expanded count for this nloc group + n = self._estimate_expanded_count(indices) bs = self._reader.get_batch_size_for_nloc(nloc) - total += (len(indices) + bs - 1) // bs + total += (n + bs - 1) // bs + return total + + def _estimate_expanded_count(self, indices: list[int]) -> int: + """Estimate expanded index count for __len__ without RNG.""" + if not self._block_targets or self._reader.frame_system_ids is None: + return len(indices) + sys_ids = self._reader.frame_system_ids + total = 0 + for blk_idx, (blk_sys_ids, block_target) in enumerate(self._block_targets): + blk_sys_set = set(blk_sys_ids) + n_in_nloc = sum(1 for i in indices if sys_ids[i] in blk_sys_set) + if n_in_nloc == 0: + continue + block_total_actual = sum(1 for sid in sys_ids if sid in blk_sys_set) + if block_total_actual > 0: + target = round(block_target * n_in_nloc / block_total_actual) + else: + target = n_in_nloc + total += max(target, n_in_nloc) + # Add unassigned + all_sys = set() + for blk_sys_ids, _ in self._block_targets: + all_sys.update(blk_sys_ids) + total += sum(1 for i in indices if sys_ids[i] not in all_sys) return total @@ -625,6 +869,8 @@ class DistributedSameNlocBatchSampler: Whether to shuffle batches. seed : int or None Base seed for deterministic RNG. All ranks must use the same seed. + block_targets : list[tuple[list[int], int]] or None + Per-block expansion targets from compute_block_targets. """ def __init__( @@ -634,6 +880,7 @@ def __init__( world_size: int, shuffle: bool = True, seed: int | None = None, + block_targets: list[tuple[list[int], int]] | None = None, ) -> None: self._reader = reader self._rank = rank @@ -641,6 +888,7 @@ def __init__( self._shuffle = shuffle self._seed = seed if seed is not None else 0 self._epoch = 0 + self._block_targets = block_targets def set_epoch(self, epoch: int) -> None: """Set epoch for deterministic cross-rank shuffling. @@ -654,7 +902,9 @@ def __iter__(self) -> Iterator[list[int]]: """Yield this rank's partition of the global batch list.""" # All ranks build the same global batch list deterministically rng = np.random.default_rng(self._seed + self._epoch) - all_batches = _build_all_batches(self._reader, self._shuffle, rng) + all_batches = _build_all_batches( + self._reader, self._shuffle, rng, self._block_targets + ) # Partition to this rank yield from self._partition_batches(all_batches) @@ -1002,7 +1252,10 @@ def merge_lmdb( frame_idx = 0 fmt = "012d" frame_nlocs: list[int] = [] + frame_system_ids: list[int] = [] first_system_info: dict | None = None + first_type_map: list[str] | None = None + sys_id_offset = 0 for src_path in src_paths: src_env = _open_lmdb(src_path) @@ -1013,9 +1266,13 @@ def merge_lmdb( if first_system_info is None: first_system_info = meta.get("system_info", {}) + if first_type_map is None: + first_type_map = meta.get("type_map") # Check for pre-computed frame_nlocs in source src_nlocs = meta.get("frame_nlocs") + # Check for frame_system_ids in source + src_sys_ids = meta.get("frame_system_ids") with src_env.begin() as src_txn, dst_env.begin(write=True) as dst_txn: for i in range(nframes): @@ -1041,7 +1298,20 @@ def merge_lmdb( else: frame_nlocs.append(fallback_natoms) + # Propagate system IDs with offset + if src_sys_ids is not None and i < len(src_sys_ids): + frame_system_ids.append(int(src_sys_ids[i]) + sys_id_offset) + else: + frame_system_ids.append(sys_id_offset) + frame_idx += 1 + + # Update sys_id_offset for next source + if src_sys_ids is not None and len(src_sys_ids) > 0: + sys_id_offset += max(int(s) for s in src_sys_ids) + 1 + else: + sys_id_offset += 1 + src_env.close() # Write merged metadata with frame_nlocs for fast init @@ -1050,7 +1320,10 @@ def merge_lmdb( "frame_idx_fmt": fmt, "system_info": first_system_info or {}, "frame_nlocs": frame_nlocs, + "frame_system_ids": frame_system_ids, } + if first_type_map is not None: + merged_meta["type_map"] = first_type_map with dst_env.begin(write=True) as txn: txn.put(b"__metadata__", msgpack.packb(merged_meta, use_bin_type=True)) dst_env.close() diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index ae82fb2b5a..a3d24fe8e9 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -149,10 +149,12 @@ def prepare_trainer_input_single( # LMDB path: single string → LmdbDataset if is_lmdb(training_systems): + auto_prob = training_dataset_params.get("auto_prob", None) train_data_single = LmdbDataset( training_systems, model_params_single["type_map"], training_dataset_params["batch_size"], + auto_prob_style=auto_prob, ) if validation_systems is not None and is_lmdb(validation_systems): validation_data_single = LmdbDataset( diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index ca7044cd39..da01a30a63 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -255,6 +255,8 @@ def get_dataloader_and_iter_lmdb( SameNlocBatchSampler, ) + _block_targets = getattr(_data, "_block_targets", None) + if self.world_size > 1: from deepmd.dpmodel.utils.lmdb_data import ( DistributedSameNlocBatchSampler, @@ -266,9 +268,14 @@ def get_dataloader_and_iter_lmdb( world_size=self.world_size, shuffle=True, seed=_training_params.get("seed", None), + block_targets=_block_targets, ) else: - _inner_sampler = SameNlocBatchSampler(_data._reader, shuffle=True) + _inner_sampler = SameNlocBatchSampler( + _data._reader, + shuffle=True, + block_targets=_block_targets, + ) _batch_sampler = _SameNlocBatchSamplerTorch(_inner_sampler) _dataloader = DataLoader( diff --git a/deepmd/pt/utils/lmdb_dataset.py b/deepmd/pt/utils/lmdb_dataset.py index 04bfb03190..22e2a173f7 100644 --- a/deepmd/pt/utils/lmdb_dataset.py +++ b/deepmd/pt/utils/lmdb_dataset.py @@ -9,6 +9,7 @@ Any, ) +import numpy as np import torch from torch.utils.data import ( DataLoader, @@ -23,6 +24,7 @@ LmdbDataReader, LmdbTestData, SameNlocBatchSampler, + compute_block_targets, is_lmdb, ) from deepmd.utils.data import ( @@ -123,6 +125,7 @@ def __init__( type_map: list[str], batch_size: int | str = "auto", mixed_batch: bool = False, + auto_prob_style: str | None = None, ) -> None: self._reader = LmdbDataReader( lmdb_path, type_map, batch_size, mixed_batch=mixed_batch @@ -135,8 +138,26 @@ def __init__( "Requires padding + mask in collate_fn." ) + # Compute block_targets from auto_prob_style if provided + self._block_targets = None + if auto_prob_style is not None and self._reader.frame_system_ids is not None: + self._block_targets = compute_block_targets( + auto_prob_style, + self._reader.nsystems, + self._reader.system_nframes, + ) + if self._block_targets is not None: + log.info( + f"LMDB auto_prob: {len(self._block_targets)} blocks, " + f"nsystems={self._reader.nsystems}" + ) + # Same-nloc batching: use SameNlocBatchSampler - sampler = SameNlocBatchSampler(self._reader, shuffle=True) + sampler = SameNlocBatchSampler( + self._reader, + shuffle=True, + block_targets=self._block_targets, + ) self._batch_sampler = _SameNlocBatchSamplerTorch(sampler) with torch.device("cpu"): @@ -203,6 +224,44 @@ def preload_and_modify_all_data_torch(self) -> None: def print_summary(self, name: str, prob: Any) -> None: self._reader.print_summary(name, prob) + if self._block_targets: + reader = self._reader + # Per-block summary: original vs target frames + block_lines = [] + total_original = 0 + total_target = 0 + for sys_ids, target in self._block_targets: + actual = sum(reader.system_nframes[s] for s in sys_ids) + total_original += actual + total_target += target + sys_str = ",".join(str(s) for s in sys_ids) + block_lines.append(f"sys[{sys_str}]:{actual}->{target}") + # Expanded nloc groups (simulate one expansion to get counts) + from deepmd.dpmodel.utils.lmdb_data import ( + _expand_indices_by_blocks, + ) + + expanded_nloc_info = {} + for nloc, indices in sorted(reader.nloc_groups.items()): + rng = np.random.default_rng(0) + expanded = _expand_indices_by_blocks( + list(indices), + reader.frame_system_ids, + self._block_targets, + rng, + ) + expanded_nloc_info[nloc] = len(expanded) + nloc_str = ", ".join( + f"{nloc}({orig}->{expanded_nloc_info[nloc]})" + for nloc, orig in sorted( + (n, len(idx)) for n, idx in reader.nloc_groups.items() + ) + ) + log.info( + f"LMDB {name} auto_prob: {total_original}->{total_target} frames, " + f"blocks: [{', '.join(block_lines)}], " + f"nloc groups: [{nloc_str}]" + ) def set_noise(self, noise_settings: dict[str, Any]) -> None: self._reader.set_noise(noise_settings) diff --git a/source/tests/consistent/test_lmdb_data.py b/source/tests/consistent/test_lmdb_data.py index d39735f92b..168acf0d0d 100644 --- a/source/tests/consistent/test_lmdb_data.py +++ b/source/tests/consistent/test_lmdb_data.py @@ -17,6 +17,8 @@ LmdbDataReader, LmdbTestData, SameNlocBatchSampler, + _expand_indices_by_blocks, + compute_block_targets, is_lmdb, ) @@ -505,5 +507,502 @@ def test_test_data_uniform_nloc_no_warning(self): tmpdir.cleanup() +def _create_lmdb_with_type_map( + path: str, + nframes: int = 6, + natoms: int = 6, + lmdb_type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with type_map stored in metadata. + + Atom types: first 1/3 are type-0, rest are type-1. + """ + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [n_type0, n_type1], + }, + } + if lmdb_type_map is not None: + meta["type_map"] = lmdb_type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(nframes): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +# ============================================================ +# Type map remapping tests +# ============================================================ + + +class TestTypeMapRemapping(unittest.TestCase): + """Test type_map remapping in LmdbDataReader and LmdbTestData.""" + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + # LMDB with type_map=["O","H"]: type-0 = O, type-1 = H + cls._lmdb_path = _create_lmdb_with_type_map( + f"{cls._tmpdir.name}/remap.lmdb", + nframes=6, + natoms=6, + lmdb_type_map=["O", "H"], + ) + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + # --- LmdbDataReader tests --- + + def test_reader_no_remap_when_match(self): + """No remap when model type_map matches LMDB type_map.""" + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + self.assertIsNone(reader._type_remap) + frame = reader[0] + # type-0 = O, type-1 = H, unchanged + self.assertEqual(frame["atype"][0], 0) + + def test_reader_remap_when_reversed(self): + """Remap when model type_map=["H","O"] vs LMDB ["O","H"].""" + reader = LmdbDataReader(self._lmdb_path, ["H", "O"]) + self.assertIsNotNone(reader._type_remap) + np.testing.assert_array_equal(reader._type_remap, [1, 0]) + frame = reader[0] + # Original type-0 (O) -> 1, type-1 (H) -> 0 + atype = frame["atype"] + n_type0_orig = max(1, 6 // 3) # 2 atoms of original type-0 + # First n_type0_orig atoms were type-0 (O), now should be 1 + for i in range(n_type0_orig): + self.assertEqual(atype[i], 1) + # Remaining atoms were type-1 (H), now should be 0 + for i in range(n_type0_orig, 6): + self.assertEqual(atype[i], 0) + + def test_reader_remap_superset(self): + """Remap when model type_map has extra elements.""" + reader = LmdbDataReader(self._lmdb_path, ["C", "O", "H"]) + self.assertIsNotNone(reader._type_remap) + np.testing.assert_array_equal(reader._type_remap, [1, 2]) + frame = reader[0] + # O -> 1, H -> 2 + n_type0_orig = max(1, 6 // 3) + for i in range(n_type0_orig): + self.assertEqual(frame["atype"][i], 1) + for i in range(n_type0_orig, 6): + self.assertEqual(frame["atype"][i], 2) + + def test_reader_natoms_vec_after_remap(self): + """natoms_vec reflects remapped types.""" + reader = LmdbDataReader(self._lmdb_path, ["H", "O"]) + frame = reader[0] + natoms = frame["natoms"] + # model type_map=["H","O"], ntypes=2 + # Original: 2 O + 4 H. After remap: O->1, H->0 + # So count_H(idx=0)=4, count_O(idx=1)=2 + self.assertEqual(natoms[0], 6) # nloc + self.assertEqual(natoms[2], 4) # H count (model idx 0) + self.assertEqual(natoms[3], 2) # O count (model idx 1) + + def test_reader_missing_element_raises(self): + """ValueError when model type_map misses an LMDB element.""" + with self.assertRaises(ValueError): + LmdbDataReader(self._lmdb_path, ["O"]) # missing H + + def test_reader_no_type_map_in_metadata(self): + """Old LMDB without type_map -> no remap.""" + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb_with_type_map( + f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None + ) + reader = LmdbDataReader(path, ["H", "O"]) + self.assertIsNone(reader._type_remap) + tmpdir.cleanup() + + # --- LmdbTestData tests --- + + def test_testdata_no_remap_when_match(self): + """LmdbTestData: no remap when type_maps match.""" + td = LmdbTestData(self._lmdb_path, type_map=["O", "H"], shuffle_test=False) + self.assertIsNone(td._type_remap) + data = td.get_test() + self.assertEqual(data["type"][0, 0], 0) + + def test_testdata_remap_when_reversed(self): + """LmdbTestData: remap when model ["H","O"] vs LMDB ["O","H"].""" + td = LmdbTestData(self._lmdb_path, type_map=["H", "O"], shuffle_test=False) + self.assertIsNotNone(td._type_remap) + data = td.get_test() + n_type0_orig = max(1, 6 // 3) + # Original type-0 (O) -> 1, type-1 (H) -> 0 + for i in range(n_type0_orig): + self.assertEqual(data["type"][0, i], 1) + for i in range(n_type0_orig, 6): + self.assertEqual(data["type"][0, i], 0) + + def test_testdata_remap_superset(self): + """LmdbTestData: remap with superset model type_map.""" + td = LmdbTestData(self._lmdb_path, type_map=["C", "O", "H"], shuffle_test=False) + self.assertIsNotNone(td._type_remap) + data = td.get_test() + n_type0_orig = max(1, 6 // 3) + # O -> 1, H -> 2 + for i in range(n_type0_orig): + self.assertEqual(data["type"][0, i], 1) + for i in range(n_type0_orig, 6): + self.assertEqual(data["type"][0, i], 2) + + def test_testdata_missing_element_raises(self): + """LmdbTestData: ValueError when model misses LMDB element.""" + with self.assertRaises(ValueError): + LmdbTestData(self._lmdb_path, type_map=["O"], shuffle_test=False) + + def test_testdata_no_type_map_in_metadata(self): + """LmdbTestData: old LMDB without type_map -> no remap.""" + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb_with_type_map( + f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None + ) + td = LmdbTestData(path, type_map=["H", "O"], shuffle_test=False) + self.assertIsNone(td._type_remap) + tmpdir.cleanup() + + +# ============================================================ +# auto_prob / frame_system_ids tests +# ============================================================ + + +def _create_lmdb_with_system_ids( + path: str, + system_frames: list[int], + natoms: int = 6, + type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with frame_system_ids in metadata. + + Parameters + ---------- + system_frames : list[int] + Number of frames per system, e.g. [100, 500] means sys0 has 100 + frames and sys1 has 500 frames. + """ + total = sum(system_frames) + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + frame_system_ids = [] + for sid, nf in enumerate(system_frames): + frame_system_ids.extend([sid] * nf) + + env = lmdb.open(path, map_size=50 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": "012d", + "system_info": {"natoms": [n_type0, n_type1]}, + "frame_system_ids": frame_system_ids, + "frame_nlocs": [natoms] * total, + } + if type_map is not None: + meta["type_map"] = type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(total): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i % 100) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +def _create_lmdb_with_system_ids_mixed_nloc( + path: str, + system_specs: list[tuple[int, int, int]], + type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with frame_system_ids and mixed nloc. + + Parameters + ---------- + system_specs : list[tuple[int, int, int]] + Each tuple is (system_id, natoms, nframes). + """ + total = sum(nf for _, _, nf in system_specs) + frame_system_ids = [] + frame_nlocs = [] + frames_data = [] + for sid, natoms, nf in system_specs: + for _ in range(nf): + frame_system_ids.append(sid) + frame_nlocs.append(natoms) + frames_data.append(_make_frame(natoms=natoms, seed=len(frames_data) % 100)) + + env = lmdb.open(path, map_size=50 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": "012d", + "system_info": {"natoms": [2, 4]}, + "frame_system_ids": frame_system_ids, + "frame_nlocs": frame_nlocs, + } + if type_map is not None: + meta["type_map"] = type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i, frame in enumerate(frames_data): + key = format(i, "012d").encode() + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +class TestAutoProb(unittest.TestCase): + """Test auto_prob support: frame_system_ids, compute_block_targets, + _expand_indices_by_blocks, and SameNlocBatchSampler with block_targets. + """ + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + # 3 systems: sys0=100 frames, sys1=200 frames, sys2=300 frames + cls._lmdb_path = _create_lmdb_with_system_ids( + f"{cls._tmpdir.name}/auto_prob.lmdb", + system_frames=[100, 200, 300], + natoms=6, + type_map=["O", "H"], + ) + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + # --- Reader system tracking --- + + def test_reader_system_groups(self): + """LmdbDataReader correctly parses frame_system_ids.""" + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + self.assertEqual(reader.nsystems, 3) + self.assertEqual(reader.system_nframes, [100, 200, 300]) + self.assertIsNotNone(reader.frame_system_ids) + self.assertEqual(len(reader.frame_system_ids), 600) + # First 100 frames belong to sys 0 + self.assertTrue(all(s == 0 for s in reader.frame_system_ids[:100])) + # Next 200 to sys 1 + self.assertTrue(all(s == 1 for s in reader.frame_system_ids[100:300])) + # Last 300 to sys 2 + self.assertTrue(all(s == 2 for s in reader.frame_system_ids[300:600])) + # system_groups + self.assertEqual(len(reader.system_groups[0]), 100) + self.assertEqual(len(reader.system_groups[1]), 200) + self.assertEqual(len(reader.system_groups[2]), 300) + + def test_reader_no_system_ids_backward_compat(self): + """Old LMDB without frame_system_ids: nsystems=1, no crash.""" + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb(f"{tmpdir.name}/old.lmdb", nframes=10, natoms=6) + reader = LmdbDataReader(path, ["O", "H"]) + self.assertEqual(reader.nsystems, 1) + self.assertIsNone(reader.frame_system_ids) + self.assertEqual(reader.system_nframes, [10]) + self.assertEqual(len(reader.system_groups[0]), 10) + tmpdir.cleanup() + + # --- compute_block_targets --- + + def test_compute_block_targets_equal_weight(self): + """Equal weight blocks with equal frames -> no expansion needed.""" + # sys0=100, sys1=100; prob 0.5 each + # ratio = 100/0.5 = 200 for both -> target = 100 each -> no expansion + result = compute_block_targets( + "prob_sys_size;0:1:0.5;1:2:0.5", + nsystems=2, + system_nframes=[100, 100], + ) + # No expansion needed (targets == actual) + self.assertEqual(result, []) + + def test_compute_block_targets_unequal(self): + """Unequal frames with equal weight -> expansion needed.""" + # sys0=100, sys1=500; prob 0.5 each + # ratio_A = 100/0.5 = 200, ratio_B = 500/0.5 = 1000 + # total_target = ceil(max(200, 1000)) = 1000 + # target_A = round(1000 * 0.5) = 500, target_B = round(1000 * 0.5) = 500 + result = compute_block_targets( + "prob_sys_size;0:1:0.5;1:2:0.5", + nsystems=2, + system_nframes=[100, 500], + ) + self.assertEqual(len(result), 2) + sys_ids_a, target_a = result[0] + sys_ids_b, target_b = result[1] + self.assertEqual(sys_ids_a, [0]) + self.assertEqual(sys_ids_b, [1]) + self.assertEqual(target_a, 500) + self.assertEqual(target_b, 500) + + def test_compute_block_targets_multi_sys_block(self): + """Block spanning multiple systems.""" + # sys0=100, sys1=200, sys2=300; block A=[0,1] prob=0.5, block B=[2] prob=0.5 + # block_A frames=300, block_B frames=300 + # ratio_A = 300/0.5 = 600, ratio_B = 300/0.5 = 600 + # total_target = 600, target_A = 300, target_B = 300 -> no expansion + result = compute_block_targets( + "prob_sys_size;0:2:0.5;2:3:0.5", + nsystems=3, + system_nframes=[100, 200, 300], + ) + self.assertEqual(result, []) + + def test_compute_block_targets_asymmetric(self): + """Asymmetric: small block needs expansion.""" + # sys0=50, sys1=50, sys2=400; block A=[0,1] prob=0.5, block B=[2] prob=0.5 + # block_A frames=100, block_B frames=400 + # ratio_A = 100/0.5 = 200, ratio_B = 400/0.5 = 800 + # total_target = 800, target_A = 400, target_B = 400 + result = compute_block_targets( + "prob_sys_size;0:2:0.5;2:3:0.5", + nsystems=3, + system_nframes=[50, 50, 400], + ) + self.assertEqual(len(result), 2) + sys_ids_a, target_a = result[0] + sys_ids_b, target_b = result[1] + self.assertEqual(sys_ids_a, [0, 1]) + self.assertEqual(sys_ids_b, [2]) + self.assertEqual(target_a, 400) + self.assertEqual(target_b, 400) + + # --- _expand_indices_by_blocks --- + + def test_expand_indices_basic(self): + """Expand indices: block A needs 5x expansion.""" + # 10 frames: sys0=[0..4], sys1=[5..9] + frame_system_ids = [0] * 5 + [1] * 5 + block_targets = [([0], 25), ([1], 25)] + rng = np.random.default_rng(42) + indices = list(range(10)) + expanded = _expand_indices_by_blocks( + indices, + frame_system_ids, + block_targets, + rng, + ) + # Each block should have 25 indices + sys0_expanded = [i for i in expanded if frame_system_ids[i] == 0] + sys1_expanded = [i for i in expanded if frame_system_ids[i] == 1] + self.assertEqual(len(sys0_expanded), 25) + self.assertEqual(len(sys1_expanded), 25) + # All original indices present + self.assertTrue(set(range(5)).issubset(set(sys0_expanded))) + self.assertTrue(set(range(5, 10)).issubset(set(sys1_expanded))) + + def test_expand_indices_no_expansion(self): + """No expansion when target == actual.""" + frame_system_ids = [0] * 5 + [1] * 5 + block_targets = [([0], 5), ([1], 5)] + rng = np.random.default_rng(42) + indices = list(range(10)) + expanded = _expand_indices_by_blocks( + indices, + frame_system_ids, + block_targets, + rng, + ) + self.assertEqual(sorted(expanded), list(range(10))) + + def test_expand_indices_remainder_sampling(self): + """Remainder part uses without-replacement sampling.""" + # sys0=10 frames, target=23 -> 1 full copy + 3 remainder + frame_system_ids = [0] * 10 + block_targets = [([0], 23)] + rng = np.random.default_rng(42) + indices = list(range(10)) + expanded = _expand_indices_by_blocks( + indices, + frame_system_ids, + block_targets, + rng, + ) + self.assertEqual(len(expanded), 23) + # Each original index appears at least twice (1 original + 1 full copy) + from collections import ( + Counter, + ) + + counts = Counter(expanded) + for i in range(10): + self.assertGreaterEqual(counts[i], 2) + # 3 indices appear 3 times (the remainder) + n_three = sum(1 for c in counts.values() if c == 3) + self.assertEqual(n_three, 3) + + def test_expand_epoch_diversity(self): + """Different RNG seeds produce different remainder samples.""" + frame_system_ids = [0] * 10 + block_targets = [([0], 15)] # 10 + 5 remainder + indices = list(range(10)) + results = [] + for seed in range(5): + rng = np.random.default_rng(seed) + expanded = _expand_indices_by_blocks( + indices, + frame_system_ids, + block_targets, + rng, + ) + # Sort the "extra" part (beyond the first 10) + results.append(sorted(expanded[10:])) + # At least 2 different remainder sets across 5 seeds + unique = {tuple(r) for r in results} + self.assertGreater(len(unique), 1) + + # --- SameNlocBatchSampler with block_targets --- + + def test_sampler_with_block_targets(self): + """SameNlocBatchSampler expands frames when block_targets provided.""" + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + # sys0=100, sys1=200, sys2=300; make block A=[0] prob=0.5, B=[1,2] prob=0.5 + # block_A frames=100, block_B frames=500 + # ratio_A=200, ratio_B=1000 -> total_target=1000 + # target_A=500, target_B=500 + block_targets = compute_block_targets( + "prob_sys_size;0:1:0.5;1:3:0.5", + nsystems=3, + system_nframes=[100, 200, 300], + ) + self.assertTrue(len(block_targets) > 0) + + sampler = SameNlocBatchSampler( + reader, + shuffle=True, + block_targets=block_targets, + ) + all_indices = [] + for batch in sampler: + all_indices.extend(batch) + # Total should be ~1000 (500 from block A + 500 from block B) + self.assertGreater(len(all_indices), 600) # more than original 600 + # All original indices should appear at least once + self.assertEqual(len(set(all_indices)), 600) + + def test_sampler_without_block_targets(self): + """SameNlocBatchSampler without block_targets: no expansion.""" + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + sampler = SameNlocBatchSampler(reader, shuffle=False) + all_indices = [] + for batch in sampler: + all_indices.extend(batch) + self.assertEqual(len(all_indices), 600) + self.assertEqual(sorted(all_indices), list(range(600))) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index ed4f14152b..f0fd709f67 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -16,6 +16,7 @@ _read_metadata, _remap_keys, is_lmdb, + merge_lmdb, ) from deepmd.pt.utils.lmdb_dataset import ( LmdbDataset, @@ -467,6 +468,159 @@ def test_type_map_global(self, lmdb_dir): assert result["type"].max() <= 1 +def _create_test_lmdb_with_type_map( + path: str, + nframes: int = 10, + natoms: int = 6, + lmdb_type_map: list[str] | None = None, +) -> None: + """Create a minimal LMDB dataset with type_map in metadata.""" + env = lmdb.open(path, map_size=10 * 1024 * 1024) + fmt = "012d" + metadata = { + "nframes": nframes, + "frame_idx_fmt": fmt, + "system_info": { + "formula": f"O{natoms // 2}H{natoms // 2}", + "natoms": [natoms // 2, natoms // 2], + "nframes": nframes, + }, + } + if lmdb_type_map is not None: + metadata["type_map"] = lmdb_type_map + with env.begin(write=True) as txn: + txn.put(b"__metadata__", msgpack.packb(metadata, use_bin_type=True)) + for i in range(nframes): + key = format(i, fmt).encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + + +@pytest.fixture +def lmdb_with_type_map(tmp_path): + """Create LMDB with type_map=["O","H"] in metadata.""" + lmdb_path = str(tmp_path / "typed.lmdb") + _create_test_lmdb_with_type_map( + lmdb_path, nframes=10, natoms=6, lmdb_type_map=["O", "H"] + ) + return lmdb_path + + +@pytest.fixture +def lmdb_no_type_map(tmp_path): + """Create LMDB without type_map in metadata (old format).""" + lmdb_path = str(tmp_path / "old.lmdb") + _create_test_lmdb_with_type_map(lmdb_path, nframes=6, natoms=6, lmdb_type_map=None) + return lmdb_path + + +class TestTypeMapRemapping: + """Test type_map remapping in LmdbDataReader, LmdbDataset, and LmdbTestData.""" + + # --- LmdbDataReader --- + + def test_reader_no_remap_when_match(self, lmdb_with_type_map): + reader = LmdbDataReader(lmdb_with_type_map, ["O", "H"]) + assert reader._type_remap is None + frame = reader[0] + assert frame["atype"][0] == 0 # type-0 stays 0 + + def test_reader_remap_reversed(self, lmdb_with_type_map): + reader = LmdbDataReader(lmdb_with_type_map, ["H", "O"]) + assert reader._type_remap is not None + np.testing.assert_array_equal(reader._type_remap, [1, 0]) + frame = reader[0] + # Original: first 3 atoms type-0 (O), last 3 type-1 (H) + # After remap: O->1, H->0 + np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) + np.testing.assert_array_equal(frame["atype"][3:], [0, 0, 0]) + + def test_reader_remap_superset(self, lmdb_with_type_map): + reader = LmdbDataReader(lmdb_with_type_map, ["C", "O", "H"]) + assert reader._type_remap is not None + np.testing.assert_array_equal(reader._type_remap, [1, 2]) + frame = reader[0] + np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) + np.testing.assert_array_equal(frame["atype"][3:], [2, 2, 2]) + + def test_reader_natoms_vec_after_remap(self, lmdb_with_type_map): + reader = LmdbDataReader(lmdb_with_type_map, ["H", "O"]) + frame = reader[0] + natoms = frame["natoms"] + assert natoms[0] == 6 # nloc + assert natoms[2] == 3 # H count (model idx 0) + assert natoms[3] == 3 # O count (model idx 1) + + def test_reader_missing_element_raises(self, lmdb_with_type_map): + with pytest.raises(ValueError, match="not found in model type_map"): + LmdbDataReader(lmdb_with_type_map, ["O"]) + + def test_reader_no_remap_old_format(self, lmdb_no_type_map): + reader = LmdbDataReader(lmdb_no_type_map, ["H", "O"]) + assert reader._type_remap is None + + # --- LmdbDataset --- + + def test_dataset_remap_reversed(self, lmdb_with_type_map): + ds = LmdbDataset(lmdb_with_type_map, type_map=["H", "O"], batch_size=2) + frame = ds[0] + # O->1, H->0 + np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) + np.testing.assert_array_equal(frame["atype"][3:], [0, 0, 0]) + + def test_dataset_remap_batch(self, lmdb_with_type_map): + ds = LmdbDataset(lmdb_with_type_map, type_map=["H", "O"], batch_size=2) + with torch.device("cpu"): + dl = ds.dataloaders[0] + batch = next(iter(dl)) + # All frames in batch should have remapped types + atype = batch["atype"] + for i in range(atype.shape[0]): + np.testing.assert_array_equal(atype[i, :3].numpy(), [1, 1, 1]) + np.testing.assert_array_equal(atype[i, 3:].numpy(), [0, 0, 0]) + + def test_dataset_no_remap_when_match(self, lmdb_with_type_map): + ds = LmdbDataset(lmdb_with_type_map, type_map=["O", "H"], batch_size=2) + frame = ds[0] + np.testing.assert_array_equal(frame["atype"][:3], [0, 0, 0]) + np.testing.assert_array_equal(frame["atype"][3:], [1, 1, 1]) + + # --- LmdbTestData --- + + def test_testdata_no_remap_when_match(self, lmdb_with_type_map): + td = LmdbTestData(lmdb_with_type_map, type_map=["O", "H"], shuffle_test=False) + assert td._type_remap is None + data = td.get_test() + assert data["type"][0, 0] == 0 + + def test_testdata_remap_reversed(self, lmdb_with_type_map): + td = LmdbTestData(lmdb_with_type_map, type_map=["H", "O"], shuffle_test=False) + assert td._type_remap is not None + data = td.get_test() + # O->1, H->0 + np.testing.assert_array_equal(data["type"][0, :3], [1, 1, 1]) + np.testing.assert_array_equal(data["type"][0, 3:], [0, 0, 0]) + + def test_testdata_remap_superset(self, lmdb_with_type_map): + td = LmdbTestData( + lmdb_with_type_map, type_map=["C", "O", "H"], shuffle_test=False + ) + assert td._type_remap is not None + data = td.get_test() + # O->1, H->2 + np.testing.assert_array_equal(data["type"][0, :3], [1, 1, 1]) + np.testing.assert_array_equal(data["type"][0, 3:], [2, 2, 2]) + + def test_testdata_missing_element_raises(self, lmdb_with_type_map): + with pytest.raises(ValueError, match="not found in model type_map"): + LmdbTestData(lmdb_with_type_map, type_map=["O"], shuffle_test=False) + + def test_testdata_no_remap_old_format(self, lmdb_no_type_map): + td = LmdbTestData(lmdb_no_type_map, type_map=["H", "O"], shuffle_test=False) + assert td._type_remap is None + + def _create_multi_nloc_lmdb(path: str) -> None: """Create an LMDB with frames of varying nloc for distributed tests.""" env = lmdb.open(path, map_size=10 * 1024 * 1024) @@ -639,3 +793,183 @@ def test_same_nloc_per_batch(self, multi_nloc_lmdb): for batch in s: nlocs = [reader.frame_nlocs[idx] for idx in batch] assert len(set(nlocs)) == 1, f"Mixed nloc in batch: {nlocs}" + + +# ============================================================ +# auto_prob / merge_lmdb tests +# ============================================================ + + +def _create_lmdb_with_system_ids( + path: str, + system_frames: list[int], + natoms: int = 6, + type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with frame_system_ids in metadata.""" + total = sum(system_frames) + frame_system_ids = [] + for sid, nf in enumerate(system_frames): + frame_system_ids.extend([sid] * nf) + + env = lmdb.open(path, map_size=50 * 1024 * 1024) + fmt = "012d" + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": fmt, + "system_info": { + "natoms": [natoms // 2, natoms // 2], + }, + "frame_system_ids": frame_system_ids, + "frame_nlocs": [natoms] * total, + } + if type_map is not None: + meta["type_map"] = type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(total): + key = format(i, fmt).encode() + frame = _make_frame(natoms=natoms, seed=i % 100) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +@pytest.fixture +def auto_prob_lmdb(tmp_path): + """LMDB with 3 systems: 50, 100, 150 frames.""" + path = str(tmp_path / "auto_prob.lmdb") + _create_lmdb_with_system_ids( + path, + system_frames=[50, 100, 150], + natoms=6, + type_map=["O", "H"], + ) + return path + + +class TestAutoProbDataset: + """Test LmdbDataset with auto_prob_style.""" + + def test_dataset_auto_prob_passthrough(self, auto_prob_lmdb): + """LmdbDataset passes auto_prob_style to sampler.""" + ds = LmdbDataset( + auto_prob_lmdb, + type_map=["O", "H"], + batch_size=4, + auto_prob_style="prob_sys_size;0:1:0.5;1:3:0.5", + ) + assert ds._block_targets is not None + assert len(ds._block_targets) > 0 + + def test_dataset_auto_prob_none(self, auto_prob_lmdb): + """LmdbDataset without auto_prob_style: no block_targets.""" + ds = LmdbDataset( + auto_prob_lmdb, + type_map=["O", "H"], + batch_size=4, + ) + assert ds._block_targets is None + + def test_dataset_auto_prob_no_system_ids(self, lmdb_dir): + """LmdbDataset with auto_prob but old LMDB (no system_ids): no crash.""" + ds = LmdbDataset( + lmdb_dir, + type_map=["O", "H"], + batch_size=4, + auto_prob_style="prob_sys_size;0:1:1.0", + ) + # No system_ids -> block_targets stays None + assert ds._block_targets is None + + def test_dataset_auto_prob_iteration(self, auto_prob_lmdb): + """LmdbDataset with auto_prob can iterate and produce batches.""" + ds = LmdbDataset( + auto_prob_lmdb, + type_map=["O", "H"], + batch_size=4, + auto_prob_style="prob_sys_size;0:1:0.5;1:3:0.5", + ) + count = 0 + for batch in ds._batch_sampler: + assert len(batch) > 0 + count += len(batch) + # Should have more frames than original 300 due to expansion + assert count > 300 + + +class TestMergeLmdbSystemIds: + """Test merge_lmdb propagates frame_system_ids.""" + + def test_merge_propagates_system_ids(self, tmp_path): + """Merged LMDB has correct frame_system_ids with offsets.""" + src1 = str(tmp_path / "src1.lmdb") + src2 = str(tmp_path / "src2.lmdb") + _create_lmdb_with_system_ids( + src1, + system_frames=[5, 10], + natoms=6, + type_map=["O", "H"], + ) + _create_lmdb_with_system_ids( + src2, + system_frames=[3, 7], + natoms=6, + type_map=["O", "H"], + ) + dst = str(tmp_path / "merged.lmdb") + merge_lmdb([src1, src2], dst) + + reader = LmdbDataReader(dst, ["O", "H"]) + assert reader.nframes == 25 # 15 + 10 + assert reader.nsystems == 4 # 2 from src1 + 2 from src2 + sids = reader.frame_system_ids + assert sids is not None + # src1: sys0(5 frames) -> 0, sys1(10 frames) -> 1 + # src2: sys0(3 frames) -> 2, sys1(7 frames) -> 3 + assert sids[:5] == [0] * 5 + assert sids[5:15] == [1] * 10 + assert sids[15:18] == [2] * 3 + assert sids[18:25] == [3] * 7 + + def test_merge_old_lmdb_no_system_ids(self, tmp_path): + """Merging old LMDBs without system_ids: each source gets one sys id.""" + src1 = str(tmp_path / "old1.lmdb") + src2 = str(tmp_path / "old2.lmdb") + _create_test_lmdb(src1, nframes=5, natoms=6) + _create_test_lmdb(src2, nframes=3, natoms=6) + dst = str(tmp_path / "merged_old.lmdb") + merge_lmdb([src1, src2], dst) + + reader = LmdbDataReader(dst, ["O", "H"]) + assert reader.nframes == 8 + assert reader.nsystems == 2 + sids = reader.frame_system_ids + assert sids is not None + assert sids[:5] == [0] * 5 + assert sids[5:8] == [1] * 3 + + def test_merge_preserves_type_map(self, tmp_path): + """Merged LMDB preserves type_map from first source.""" + src1 = str(tmp_path / "tm1.lmdb") + src2 = str(tmp_path / "tm2.lmdb") + _create_lmdb_with_system_ids( + src1, + system_frames=[5], + natoms=6, + type_map=["O", "H"], + ) + _create_lmdb_with_system_ids( + src2, + system_frames=[5], + natoms=6, + type_map=["O", "H"], + ) + dst = str(tmp_path / "merged_tm.lmdb") + merge_lmdb([src1, src2], dst) + + env = lmdb.open(dst, readonly=True, lock=False) + with env.begin() as txn: + meta = _read_metadata(txn) + env.close() + assert meta.get("type_map") == ["O", "H"] From 28b9300cc069bec541210624c85f51a60c8a0832 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:46:45 +0800 Subject: [PATCH 09/22] fix efficiency --- deepmd/dpmodel/utils/lmdb_data.py | 154 ++++++++++++++++++++---------- deepmd/pt/utils/lmdb_dataset.py | 79 ++++++++++----- 2 files changed, 158 insertions(+), 75 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index d8fa454b5b..92c79bf5cd 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -475,16 +475,22 @@ def add_data_requirement(self, data_requirement: list[DataRequirementItem]) -> N def print_summary(self, name: str, prob: Any) -> None: """Print basic dataset info.""" - unique_nlocs = sorted(self._nloc_groups.keys()) - nloc_info = ", ".join( - f"{nloc}({len(idxs)})" for nloc, idxs in sorted(self._nloc_groups.items()) - ) + n_groups = len(self._nloc_groups) + log.info( f"LMDB {name}: {self.lmdb_path}, " - f"{self.nframes} frames, nloc groups: [{nloc_info}], " + f"{self.nframes} frames, {n_groups} nloc groups, " f"batch_size={'auto' if self._auto_rule else self.batch_size}, " f"mixed_batch={self.mixed_batch}" ) + # Print nloc groups in rows of ~10 for readability + items = [ + f"{nloc}({len(idxs)})" for nloc, idxs in sorted(self._nloc_groups.items()) + ] + per_row = 10 + for i in range(0, len(items), per_row): + row = ", ".join(items[i : i + per_row]) + log.info(f" nloc groups: {row}") def set_noise(self, noise_settings: dict[str, Any]) -> None: """No-op for now.""" @@ -616,9 +622,11 @@ def compute_block_targets( def _expand_indices_by_blocks( indices: list[int], - frame_system_ids: list[int], + frame_system_ids: np.ndarray, block_targets: list[tuple[list[int], int]], rng: np.random.Generator, + _block_total_actual: list[int] | None = None, + _sid_to_blk_arr: np.ndarray | None = None, ) -> list[int]: """Expand frame indices according to block targets. @@ -630,59 +638,71 @@ def _expand_indices_by_blocks( ---------- indices : list[int] Frame indices in the current nloc group. - frame_system_ids : list[int] - Per-frame system id for the entire dataset. + frame_system_ids : np.ndarray + Per-frame system id for the entire dataset (int64 array). block_targets : list[tuple[list[int], int]] Per-block (system_ids, total_target_frames). rng : np.random.Generator RNG for remainder sampling. + _block_total_actual : list[int] or None + Pre-computed total actual frame count per block (across all nloc + groups). When provided, avoids an O(N) scan of frame_system_ids. + _sid_to_blk_arr : np.ndarray or None + Pre-computed system-id to block-index lookup array. When provided, + avoids rebuilding the mapping for each call. Returns ------- list[int] Expanded indices. """ - # Build sys_id -> block_idx mapping - sys_to_block: dict[int, int] = {} - for blk_idx, (sys_ids, _target) in enumerate(block_targets): - for sid in sys_ids: - sys_to_block[sid] = blk_idx - - # Partition indices by block - block_indices: dict[int, list[int]] = {i: [] for i in range(len(block_targets))} - unassigned: list[int] = [] - for idx in indices: - sid = frame_system_ids[idx] - blk = sys_to_block.get(sid) - if blk is not None: - block_indices[blk].append(idx) - else: - unassigned.append(idx) - - # Compute total actual frames across all blocks (for proportional scaling) - total_actual = sum(len(block_indices[i]) for i in range(len(block_targets))) - total_target_all = sum(t for _, t in block_targets) - - expanded: list[int] = list(unassigned) - - for blk_idx, (sys_ids, block_total_target) in enumerate(block_targets): - blk_idxs = block_indices[blk_idx] + n_blocks = len(block_targets) + + # Build sys_id -> block_idx lookup array + if _sid_to_blk_arr is None: + sys_to_block: dict[int, int] = {} + for blk_idx, (sys_ids, _target) in enumerate(block_targets): + for sid in sys_ids: + sys_to_block[sid] = blk_idx + max_sid = max(sys_to_block.keys()) + 1 if sys_to_block else 0 + _sid_to_blk_arr = np.full(max_sid, -1, dtype=np.int32) + for sid, blk in sys_to_block.items(): + _sid_to_blk_arr[sid] = blk + + # Partition indices by block using numpy for speed + idx_arr = np.asarray(indices, dtype=np.int64) + sid_arr = np.asarray(frame_system_ids, dtype=np.int64) + # Vectorized lookup: get block id for each index + idx_sids = sid_arr[idx_arr] + idx_blks = _sid_to_blk_arr[idx_sids] + + # Pre-compute block_total_actual if not provided + if _block_total_actual is None: + _block_total_actual = [] + for sys_ids, _ in block_targets: + total = sum(int(np.sum(sid_arr == sid)) for sid in sys_ids) + _block_total_actual.append(total) + + expanded_parts: list[np.ndarray] = [] + + # Unassigned indices + unassigned_mask = idx_blks == -1 + if np.any(unassigned_mask): + expanded_parts.append(idx_arr[unassigned_mask]) + + for blk_idx in range(n_blocks): + blk_mask = idx_blks == blk_idx + blk_idxs = idx_arr[blk_mask] n_actual = len(blk_idxs) if n_actual == 0: continue - # Proportional target for this nloc subset of the block - # block_total_target is for the entire block; scale by the fraction - # of block frames that fall in this nloc group - _, block_total_target_all = block_targets[blk_idx] - # Get total frames in this block across all nloc groups - block_total_actual = sum( - 1 - for i in range(len(frame_system_ids)) - if frame_system_ids[i] in set(sys_ids) - ) - if block_total_actual > 0: - target = round(block_total_target_all * n_actual / block_total_actual) + _, block_total_target = block_targets[blk_idx] + block_total_act = _block_total_actual[blk_idx] + + # Proportional target for this nloc subset + if block_total_act > 0: + target = round(block_total_target * n_actual / block_total_act) else: target = n_actual target = max(target, n_actual) # never shrink @@ -690,18 +710,23 @@ def _expand_indices_by_blocks( # Full copies + remainder deficit = target - n_actual if deficit <= 0: - expanded.extend(blk_idxs) + expanded_parts.append(blk_idxs) else: full_copies = deficit // n_actual remainder = deficit % n_actual # Original + full copies - expanded.extend(blk_idxs * (1 + full_copies)) + if full_copies > 0: + expanded_parts.append(np.tile(blk_idxs, 1 + full_copies)) + else: + expanded_parts.append(blk_idxs) # Remainder: sample without replacement if remainder > 0: sampled = rng.choice(blk_idxs, size=remainder, replace=False) - expanded.extend(sampled.tolist()) + expanded_parts.append(sampled) - return expanded + if expanded_parts: + return np.concatenate(expanded_parts).tolist() + return [] def _build_all_batches( @@ -735,12 +760,39 @@ def _build_all_batches( """ # Build per-group batches group_batches: list[list[list[int]]] = [] + + # Pre-compute expensive objects once (avoids O(N) work per nloc group) + block_total_actual: list[int] | None = None + sid_arr: np.ndarray | None = None + sid_to_blk_arr: np.ndarray | None = None + if block_targets and reader.frame_system_ids is not None: + block_total_actual = [] + for sys_ids, _ in block_targets: + total = sum(reader.system_nframes[s] for s in sys_ids) + block_total_actual.append(total) + # Convert frame_system_ids to numpy once + sid_arr = np.array(reader.frame_system_ids, dtype=np.int64) + # Build sys_id -> block_idx lookup array once + sys_to_block: dict[int, int] = {} + for blk_idx, (sys_ids, _target) in enumerate(block_targets): + for sid in sys_ids: + sys_to_block[sid] = blk_idx + max_sid = max(sys_to_block.keys()) + 1 if sys_to_block else 0 + sid_to_blk_arr = np.full(max_sid, -1, dtype=np.int32) + for sid, blk in sys_to_block.items(): + sid_to_blk_arr[sid] = blk + for nloc in sorted(reader.nloc_groups.keys()): indices = list(reader.nloc_groups[nloc]) # Expand indices by block targets if provided - if block_targets and reader.frame_system_ids is not None: + if block_targets and sid_arr is not None: indices = _expand_indices_by_blocks( - indices, reader.frame_system_ids, block_targets, rng + indices, + sid_arr, + block_targets, + rng, + _block_total_actual=block_total_actual, + _sid_to_blk_arr=sid_to_blk_arr, ) if shuffle: rng.shuffle(indices) diff --git a/deepmd/pt/utils/lmdb_dataset.py b/deepmd/pt/utils/lmdb_dataset.py index 22e2a173f7..44d67be242 100644 --- a/deepmd/pt/utils/lmdb_dataset.py +++ b/deepmd/pt/utils/lmdb_dataset.py @@ -9,7 +9,6 @@ Any, ) -import numpy as np import torch from torch.utils.data import ( DataLoader, @@ -230,38 +229,70 @@ def print_summary(self, name: str, prob: Any) -> None: block_lines = [] total_original = 0 total_target = 0 + # Pre-compute block_total_actual for proportional scaling + block_total_actual: list[int] = [] for sys_ids, target in self._block_targets: actual = sum(reader.system_nframes[s] for s in sys_ids) + block_total_actual.append(actual) total_original += actual total_target += target - sys_str = ",".join(str(s) for s in sys_ids) - block_lines.append(f"sys[{sys_str}]:{actual}->{target}") - # Expanded nloc groups (simulate one expansion to get counts) - from deepmd.dpmodel.utils.lmdb_data import ( - _expand_indices_by_blocks, - ) + # Compact range notation: sys[0-146] instead of sys[0,1,2,...,146] + if len(sys_ids) > 3: + sys_str = f"{sys_ids[0]}-{sys_ids[-1]}" + else: + sys_str = ",".join(str(s) for s in sys_ids) + ratio = target / actual if actual > 0 else 0 + block_lines.append( + f"sys[{sys_str}]({len(sys_ids)}sys): " + f"{actual}->{target} (x{ratio:.2f})" + ) + # Build sys_id -> block_idx mapping + sys_to_block: dict[int, int] = {} + for blk_idx, (sys_ids, _) in enumerate(self._block_targets): + for sid in sys_ids: + sys_to_block[sid] = blk_idx + + # Compute expanded nloc counts analytically (no actual expansion) expanded_nloc_info = {} for nloc, indices in sorted(reader.nloc_groups.items()): - rng = np.random.default_rng(0) - expanded = _expand_indices_by_blocks( - list(indices), - reader.frame_system_ids, - self._block_targets, - rng, - ) - expanded_nloc_info[nloc] = len(expanded) - nloc_str = ", ".join( - f"{nloc}({orig}->{expanded_nloc_info[nloc]})" - for nloc, orig in sorted( - (n, len(idx)) for n, idx in reader.nloc_groups.items() - ) - ) + if reader.frame_system_ids is None: + expanded_nloc_info[nloc] = len(indices) + continue + # Count indices per block in this nloc group + blk_counts: dict[int, int] = {} + unassigned = 0 + for idx in indices: + sid = reader.frame_system_ids[idx] + blk = sys_to_block.get(sid) + if blk is not None: + blk_counts[blk] = blk_counts.get(blk, 0) + 1 + else: + unassigned += 1 + expanded = unassigned + for blk_idx, (_, blk_target) in enumerate(self._block_targets): + n_actual = blk_counts.get(blk_idx, 0) + if n_actual == 0: + continue + bta = block_total_actual[blk_idx] + if bta > 0: + t = max(round(blk_target * n_actual / bta), n_actual) + else: + t = n_actual + expanded += t + expanded_nloc_info[nloc] = expanded + + total_expanded = sum(expanded_nloc_info.values()) + n_groups = len(reader.nloc_groups) + ratio_all = total_expanded / total_original if total_original > 0 else 0 + log.info( - f"LMDB {name} auto_prob: {total_original}->{total_target} frames, " - f"blocks: [{', '.join(block_lines)}], " - f"nloc groups: [{nloc_str}]" + f"LMDB {name} auto_prob: " + f"{total_original}->{total_expanded} frames (x{ratio_all:.2f}), " + f"{n_groups} nloc groups, {len(self._block_targets)} blocks:" ) + for bl in block_lines: + log.info(f" {bl}") def set_noise(self, noise_settings: dict[str, Any]) -> None: self._reader.set_noise(noise_settings) From 792361e51004d8fea65b22e7707c879c65f7549d Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:49:21 +0800 Subject: [PATCH 10/22] fix multitask --- deepmd/pt/train/training.py | 27 ++- source/tests/pt/test_lmdb_dataloader.py | 265 ++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 10 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index c39175c44c..efe0759b50 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -535,7 +535,9 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR: training_data[model_key].print_summary( f"training in {model_key}", - to_numpy_array(self.training_dataloader[model_key].sampler.weights), + to_numpy_array(self.training_dataloader[model_key].sampler.weights) + if not isinstance(training_data[model_key], LmdbDataset) + else [1.0], ) if ( validation_data is not None @@ -545,7 +547,9 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR: f"validation in {model_key}", to_numpy_array( self.validation_dataloader[model_key].sampler.weights - ), + ) + if not isinstance(validation_data[model_key], LmdbDataset) + else [1.0], ) # Resolve training steps @@ -587,15 +591,18 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR: "are mutually exclusive." ) for model_key in self.model_keys: - sampler_weights = to_numpy_array( - self.training_dataloader[model_key].sampler.weights - ) - per_task_total.append( - compute_total_numb_batch( - training_data[model_key].index, - sampler_weights, + if isinstance(training_data[model_key], LmdbDataset): + per_task_total.append(training_data[model_key].total_batch) + else: + sampler_weights = to_numpy_array( + self.training_dataloader[model_key].sampler.weights + ) + per_task_total.append( + compute_total_numb_batch( + training_data[model_key].index, + sampler_weights, + ) ) - ) ( self.model_prob, self.num_steps, diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index f0fd709f67..fd7b4b2439 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -973,3 +973,268 @@ def test_merge_preserves_type_map(self, tmp_path): meta = _read_metadata(txn) env.close() assert meta.get("type_map") == ["O", "H"] + + +# ============================================================ +# Multitask LMDB training tests +# ============================================================ + + +@pytest.fixture +def multitask_lmdb_setup(tmp_path): + """Create two LMDB datasets and a multitask training config.""" + lmdb_train1 = str(tmp_path / "task1_train.lmdb") + lmdb_train2 = str(tmp_path / "task2_train.lmdb") + lmdb_val1 = str(tmp_path / "task1_val.lmdb") + lmdb_val2 = str(tmp_path / "task2_val.lmdb") + + _create_test_lmdb_with_type_map( + lmdb_train1, nframes=20, natoms=6, lmdb_type_map=["O", "H"] + ) + _create_test_lmdb_with_type_map( + lmdb_train2, nframes=20, natoms=6, lmdb_type_map=["O", "H"] + ) + _create_test_lmdb_with_type_map( + lmdb_val1, nframes=10, natoms=6, lmdb_type_map=["O", "H"] + ) + _create_test_lmdb_with_type_map( + lmdb_val2, nframes=10, natoms=6, lmdb_type_map=["O", "H"] + ) + + config = { + "model": { + "shared_dict": { + "type_map_all": ["O", "H"], + "my_descriptor": { + "type": "se_atten", + "sel": 40, + "rcut_smth": 0.5, + "rcut": 4.0, + "neuron": [4, 8, 16], + "axis_neuron": 4, + "attn": 4, + "attn_layer": 2, + "attn_dotr": True, + "attn_mask": False, + "precision": "float64", + }, + "my_fitting": { + "neuron": [16, 16], + "precision": "float64", + "seed": 1, + }, + }, + "model_dict": { + "model_1": { + "type_map": "type_map_all", + "descriptor": "my_descriptor", + "fitting_net": "my_fitting", + "data_stat_nbatch": 1, + }, + "model_2": { + "type_map": "type_map_all", + "descriptor": "my_descriptor", + "fitting_net": "my_fitting", + "data_stat_nbatch": 1, + }, + }, + }, + "learning_rate": { + "type": "exp", + "decay_steps": 50, + "start_lr": 1e-3, + "stop_lr": 1e-8, + }, + "loss_dict": { + "model_1": { + "type": "ener", + "start_pref_e": 0.2, + "limit_pref_e": 1, + "start_pref_f": 100, + "limit_pref_f": 1, + "start_pref_v": 0.0, + "limit_pref_v": 0.0, + }, + "model_2": { + "type": "ener", + "start_pref_e": 0.2, + "limit_pref_e": 1, + "start_pref_f": 100, + "limit_pref_f": 1, + "start_pref_v": 0.0, + "limit_pref_v": 0.0, + }, + }, + "training": { + "model_prob": {"model_1": 0.5, "model_2": 0.5}, + "data_dict": { + "model_1": { + "stat_file": str(tmp_path / "stat_model_1.hdf5"), + "training_data": { + "systems": lmdb_train1, + "batch_size": 4, + }, + "validation_data": { + "systems": lmdb_val1, + "batch_size": 2, + }, + }, + "model_2": { + "stat_file": str(tmp_path / "stat_model_2.hdf5"), + "training_data": { + "systems": lmdb_train2, + "batch_size": 4, + }, + "validation_data": { + "systems": lmdb_val2, + "batch_size": 2, + }, + }, + }, + "numb_steps": 5, + "seed": 10, + "disp_file": str(tmp_path / "lcurve.out"), + "disp_freq": 2, + "save_freq": 5, + }, + } + return config, tmp_path + + +class TestMultitaskLmdbTraining: + """Test multitask training with LMDB datasets.""" + + def test_multitask_lmdb_trainer_init(self, multitask_lmdb_setup, monkeypatch): + """Trainer initializes without error for multitask LMDB.""" + from copy import ( + deepcopy, + ) + + from deepmd.pt.entrypoints.main import ( + get_trainer, + ) + from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, + ) + from deepmd.utils.argcheck import ( + normalize, + ) + from deepmd.utils.compat import ( + update_deepmd_input, + ) + + config, tmp_path = multitask_lmdb_setup + monkeypatch.chdir(tmp_path) + + config = update_deepmd_input(deepcopy(config), warning=True) + config["model"], shared_links = preprocess_shared_params(config["model"]) + config = normalize(config, multi_task=True) + + trainer = get_trainer(config, shared_links=shared_links) + assert trainer.multi_task + assert set(trainer.model_keys) == {"model_1", "model_2"} + + def test_multitask_lmdb_training_runs(self, multitask_lmdb_setup, monkeypatch): + """Multitask LMDB training runs for a few steps without errors.""" + from copy import ( + deepcopy, + ) + + from deepmd.pt.entrypoints.main import ( + get_trainer, + ) + from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, + ) + from deepmd.utils.argcheck import ( + normalize, + ) + from deepmd.utils.compat import ( + update_deepmd_input, + ) + + config, tmp_path = multitask_lmdb_setup + monkeypatch.chdir(tmp_path) + + config = update_deepmd_input(deepcopy(config), warning=True) + config["model"], shared_links = preprocess_shared_params(config["model"]) + config = normalize(config, multi_task=True) + + trainer = get_trainer(config, shared_links=shared_links) + trainer.run() + + # Verify checkpoint was saved + ckpt_files = list(tmp_path.glob("model.ckpt*.pt")) + assert len(ckpt_files) > 0, "No checkpoint file was saved" + + def test_multitask_lmdb_get_data(self, multitask_lmdb_setup, monkeypatch): + """Verify get_data returns proper dicts for each task with LMDB.""" + from copy import ( + deepcopy, + ) + + from deepmd.pt.entrypoints.main import ( + get_trainer, + ) + from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, + ) + from deepmd.utils.argcheck import ( + normalize, + ) + from deepmd.utils.compat import ( + update_deepmd_input, + ) + + config, tmp_path = multitask_lmdb_setup + monkeypatch.chdir(tmp_path) + + config = update_deepmd_input(deepcopy(config), warning=True) + config["model"], shared_links = preprocess_shared_params(config["model"]) + config = normalize(config, multi_task=True) + + trainer = get_trainer(config, shared_links=shared_links) + + for task_key in ["model_1", "model_2"]: + input_dict, label_dict, log_dict = trainer.get_data( + is_train=True, task_key=task_key + ) + assert "coord" in input_dict + assert "atype" in input_dict + assert "energy" in label_dict or "find_energy" in label_dict + assert "sid" in log_dict + + def test_multitask_lmdb_shared_params(self, multitask_lmdb_setup, monkeypatch): + """Shared descriptor params are identical across tasks after init.""" + from copy import ( + deepcopy, + ) + + from deepmd.pt.entrypoints.main import ( + get_trainer, + ) + from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, + ) + from deepmd.utils.argcheck import ( + normalize, + ) + from deepmd.utils.compat import ( + update_deepmd_input, + ) + + config, tmp_path = multitask_lmdb_setup + monkeypatch.chdir(tmp_path) + + config = update_deepmd_input(deepcopy(config), warning=True) + config["model"], shared_links = preprocess_shared_params(config["model"]) + config = normalize(config, multi_task=True) + + trainer = get_trainer(config, shared_links=shared_links) + + state_dict = trainer.wrapper.model.state_dict() + for key in state_dict: + if "model_1.atomic_model.descriptor" in key: + key2 = key.replace("model_1", "model_2") + assert key2 in state_dict, f"Missing {key2}" + torch.testing.assert_close(state_dict[key], state_dict[key2]) From 6b275bfafdc24ed2ddf25e9a82c9ec9e306b1949 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:29:27 +0800 Subject: [PATCH 11/22] fix neighbor-stat --- deepmd/dpmodel/utils/__init__.py | 2 + deepmd/dpmodel/utils/lmdb_data.py | 63 ++ deepmd/pt/entrypoints/main.py | 36 +- source/tests/common/dpmodel/test_lmdb_data.py | 713 +++++++++++++++ source/tests/consistent/test_lmdb_data.py | 850 +++--------------- 5 files changed, 907 insertions(+), 757 deletions(-) create mode 100644 source/tests/common/dpmodel/test_lmdb_data.py diff --git a/deepmd/dpmodel/utils/__init__.py b/deepmd/dpmodel/utils/__init__.py index 95a7fe7233..6c433ba2ab 100644 --- a/deepmd/dpmodel/utils/__init__.py +++ b/deepmd/dpmodel/utils/__init__.py @@ -12,6 +12,7 @@ LmdbTestData, SameNlocBatchSampler, is_lmdb, + make_neighbor_stat_data, ) from .network import ( EmbeddingNet, @@ -75,6 +76,7 @@ "make_embedding_network", "make_fitting_network", "make_multilayer_network", + "make_neighbor_stat_data", "nlist_distinguish_types", "normalize_coord", "phys2inter", diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 92c79bf5cd..4874fa30ed 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -991,6 +991,69 @@ def world_size(self) -> int: return self._world_size +def make_neighbor_stat_data( + lmdb_path: str, + type_map: list[str] | None, + max_frames: int = 2000, +) -> Any: + """Create a duck-typed DeepmdDataSystem-like object for neighbor stat from LMDB. + + Samples up to *max_frames* frames, groups them by nloc, and returns an + object whose attributes satisfy the interface expected by + ``NeighborStat.iterator()`` and ``UpdateSel.get_nbor_stat()``. + """ + from types import ( + SimpleNamespace, + ) + + reader = LmdbDataReader(lmdb_path, type_map=type_map) + nframes = len(reader) + rng = np.random.RandomState(42) + if nframes > max_frames: + indices = np.sort(rng.choice(nframes, max_frames, replace=False)) + else: + indices = np.arange(nframes, dtype=np.int64) + + # Read sampled frames, group by nloc + nloc_frames: dict[int, list[tuple[np.ndarray, np.ndarray, np.ndarray | None]]] = {} + for idx in indices: + frame = reader[int(idx)] + atype = frame["atype"] + nloc = len(atype) + nloc_frames.setdefault(nloc, []).append( + (frame["coord"], atype, frame.get("box")) + ) + + # Build per-nloc data_system proxies + data_systems = [] + system_dirs: list[str] = [] + for nloc, frames in nloc_frames.items(): + coords = np.stack([c.reshape(nloc * 3) for c, _, _ in frames]) + types = np.stack([a.reshape(nloc) for _, a, _ in frames]) + has_box = frames[0][2] is not None + boxes = np.stack([b.reshape(9) for _, _, b in frames]) if has_box else None + set_data = {"coord": coords, "type": types, "box": boxes} + label = f"lmdb:{nloc}atoms" + proxy = SimpleNamespace( + dirs=[label], + pbc=has_box, + mixed_type=True, + get_natoms=lambda _nloc=nloc: _nloc, + _load_set=lambda _d, _sd=set_data: _sd, + ) + data_systems.append(proxy) + system_dirs.append(label) + + ntypes = len(type_map) if type_map else reader._ntypes + return SimpleNamespace( + system_dirs=system_dirs, + data_systems=data_systems, + get_batch=lambda: None, + get_ntypes=lambda: ntypes, + mixed_type=True, + ) + + class LmdbTestData: """LMDB-backed data reader for dp test. diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index a3d24fe8e9..c13c292be3 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -372,9 +372,17 @@ def train( if not multi_task: type_map = config["model"].get("type_map") - train_data = get_data( - config["training"]["training_data"], 0, type_map, None - ) + training_systems = config["training"]["training_data"].get("systems") + if training_systems is not None and is_lmdb(training_systems): + from deepmd.dpmodel.utils.lmdb_data import ( + make_neighbor_stat_data, + ) + + train_data = make_neighbor_stat_data(training_systems, type_map) + else: + train_data = get_data( + config["training"]["training_data"], 0, type_map, None + ) config["model"], min_nbor_dist = BaseModel.update_sel( train_data, type_map, config["model"] ) @@ -382,12 +390,22 @@ def train( min_nbor_dist = {} for model_item in config["model"]["model_dict"]: type_map = config["model"]["model_dict"][model_item].get("type_map") - train_data = get_data( - config["training"]["data_dict"][model_item]["training_data"], - 0, - type_map, - None, - ) + training_systems = config["training"]["data_dict"][model_item][ + "training_data" + ].get("systems") + if training_systems is not None and is_lmdb(training_systems): + from deepmd.dpmodel.utils.lmdb_data import ( + make_neighbor_stat_data, + ) + + train_data = make_neighbor_stat_data(training_systems, type_map) + else: + train_data = get_data( + config["training"]["data_dict"][model_item]["training_data"], + 0, + type_map, + None, + ) config["model"]["model_dict"][model_item], min_nbor_dist[model_item] = ( BaseModel.update_sel( train_data, type_map, config["model"]["model_dict"][model_item] diff --git a/source/tests/common/dpmodel/test_lmdb_data.py b/source/tests/common/dpmodel/test_lmdb_data.py new file mode 100644 index 0000000000..76d337156c --- /dev/null +++ b/source/tests/common/dpmodel/test_lmdb_data.py @@ -0,0 +1,713 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Unit tests for LmdbDataReader, LmdbTestData, SameNlocBatchSampler, etc. + +Pure dpmodel (NumPy/lmdb) tests — no PyTorch dependency. +""" + +import tempfile +import unittest + +import lmdb +import msgpack +import numpy as np + +from deepmd.dpmodel.utils.lmdb_data import ( + LmdbDataReader, + LmdbTestData, + SameNlocBatchSampler, + _expand_indices_by_blocks, + compute_block_targets, + is_lmdb, + make_neighbor_stat_data, +) + +# ============================================================ +# LMDB creation helpers +# ============================================================ + + +def _make_frame(natoms: int = 6, seed: int = 0) -> dict: + """Create a synthetic frame dict for testing. + + Generates atom_types with roughly 1/3 type-0 and 2/3 type-1. + """ + rng = np.random.RandomState(seed) + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + atype = np.array([0] * n_type0 + [1] * n_type1, dtype=np.int64) + return { + "atom_names": ["O", "H"], + "atom_numbs": [ + { + "type": " str: + """Create a test LMDB database with uniform nloc.""" + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [n_type0, n_type1], + "formula": "test", + }, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(nframes): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +def _create_mixed_nloc_lmdb(path: str) -> str: + """Create an LMDB with frames of different atom counts. + + Frames 0-3: 6 atoms, Frames 4-7: 9 atoms, Frames 8-9: 12 atoms. + """ + frames_spec = [(6, 4), (9, 4), (12, 2)] # (natoms, count) + total = sum(c for _, c in frames_spec) + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [2, 4], # first frame's type counts + "formula": "mixed", + }, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + idx = 0 + for natoms, count in frames_spec: + for j in range(count): + txn.put( + format(idx, "012d").encode(), + msgpack.packb( + _make_frame(natoms=natoms, seed=idx), use_bin_type=True + ), + ) + idx += 1 + env.close() + return path + + +def _create_lmdb_with_type_map( + path: str, + nframes: int = 6, + natoms: int = 6, + lmdb_type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with type_map stored in metadata.""" + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "system_info": { + "natoms": [n_type0, n_type1], + }, + } + if lmdb_type_map is not None: + meta["type_map"] = lmdb_type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(nframes): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +def _create_lmdb_with_system_ids( + path: str, + system_frames: list[int], + natoms: int = 6, + type_map: list[str] | None = None, +) -> str: + """Create a test LMDB with frame_system_ids in metadata.""" + total = sum(system_frames) + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + frame_system_ids = [] + for sid, nf in enumerate(system_frames): + frame_system_ids.extend([sid] * nf) + + env = lmdb.open(path, map_size=50 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": total, + "frame_idx_fmt": "012d", + "system_info": {"natoms": [n_type0, n_type1]}, + "frame_system_ids": frame_system_ids, + "frame_nlocs": [natoms] * total, + } + if type_map is not None: + meta["type_map"] = type_map + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(total): + key = format(i, "012d").encode() + frame = _make_frame(natoms=natoms, seed=i % 100) + txn.put(key, msgpack.packb(frame, use_bin_type=True)) + env.close() + return path + + +def _create_grid_lmdb(path: str, nframes: int = 3) -> str: + """Create a test LMDB with 3x3x3 grid of atoms (27 atoms, cell=3A). + + Same geometry as test_neighbor_stat.py: positions at integer coords + (0,1,2)^3, so min_nbor_dist = 1.0. + """ + X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # (27, 3) + natoms = 27 + cell = np.array([3.0, 0, 0, 0, 3.0, 0, 0, 0, 3.0], dtype=np.float64) + atype = np.zeros(natoms, dtype=np.int64) + + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "type_map": ["TYPE"], + "system_info": {"natoms": [natoms], "formula": "grid"}, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(nframes): + frame = { + "atom_types": { + "type": " 1 + for i in range(n0, 6): + self.assertEqual(atype[i], 0) # H -> 0 + + def test_reader_remap_superset(self): + reader = LmdbDataReader(self._lmdb_path, ["C", "O", "H"]) + np.testing.assert_array_equal(reader._type_remap, [1, 2]) + + def test_reader_natoms_vec_after_remap(self): + reader = LmdbDataReader(self._lmdb_path, ["H", "O"]) + natoms = reader[0]["natoms"] + self.assertEqual(natoms[0], 6) + self.assertEqual(natoms[2], 4) # H count + self.assertEqual(natoms[3], 2) # O count + + def test_reader_missing_element_raises(self): + with self.assertRaises(ValueError): + LmdbDataReader(self._lmdb_path, ["O"]) + + def test_reader_no_type_map_in_metadata(self): + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb_with_type_map( + f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None + ) + reader = LmdbDataReader(path, ["H", "O"]) + self.assertIsNone(reader._type_remap) + tmpdir.cleanup() + + def test_testdata_no_remap_when_match(self): + td = LmdbTestData(self._lmdb_path, type_map=["O", "H"], shuffle_test=False) + self.assertIsNone(td._type_remap) + + def test_testdata_remap_when_reversed(self): + td = LmdbTestData(self._lmdb_path, type_map=["H", "O"], shuffle_test=False) + self.assertIsNotNone(td._type_remap) + data = td.get_test() + n0 = max(1, 6 // 3) + for i in range(n0): + self.assertEqual(data["type"][0, i], 1) + for i in range(n0, 6): + self.assertEqual(data["type"][0, i], 0) + + def test_testdata_remap_superset(self): + td = LmdbTestData(self._lmdb_path, type_map=["C", "O", "H"], shuffle_test=False) + self.assertIsNotNone(td._type_remap) + + def test_testdata_missing_element_raises(self): + with self.assertRaises(ValueError): + LmdbTestData(self._lmdb_path, type_map=["O"], shuffle_test=False) + + def test_testdata_no_type_map_in_metadata(self): + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb_with_type_map( + f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None + ) + td = LmdbTestData(path, type_map=["H", "O"], shuffle_test=False) + self.assertIsNone(td._type_remap) + tmpdir.cleanup() + + +# ============================================================ +# auto_prob / frame_system_ids tests +# ============================================================ + + +class TestAutoProb(unittest.TestCase): + """Test auto_prob support: frame_system_ids, compute_block_targets, + _expand_indices_by_blocks, and SameNlocBatchSampler with block_targets. + """ + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + cls._lmdb_path = _create_lmdb_with_system_ids( + f"{cls._tmpdir.name}/auto_prob.lmdb", + system_frames=[100, 200, 300], + natoms=6, + type_map=["O", "H"], + ) + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + def test_reader_system_groups(self): + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + self.assertEqual(reader.nsystems, 3) + self.assertEqual(reader.system_nframes, [100, 200, 300]) + self.assertEqual(len(reader.system_groups[0]), 100) + self.assertEqual(len(reader.system_groups[1]), 200) + self.assertEqual(len(reader.system_groups[2]), 300) + + def test_reader_no_system_ids_backward_compat(self): + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb(f"{tmpdir.name}/old.lmdb", nframes=10, natoms=6) + reader = LmdbDataReader(path, ["O", "H"]) + self.assertEqual(reader.nsystems, 1) + self.assertIsNone(reader.frame_system_ids) + tmpdir.cleanup() + + def test_compute_block_targets_equal_weight(self): + result = compute_block_targets( + "prob_sys_size;0:1:0.5;1:2:0.5", nsystems=2, system_nframes=[100, 100] + ) + self.assertEqual(result, []) + + def test_compute_block_targets_unequal(self): + result = compute_block_targets( + "prob_sys_size;0:1:0.5;1:2:0.5", nsystems=2, system_nframes=[100, 500] + ) + self.assertEqual(len(result), 2) + self.assertEqual(result[0], ([0], 500)) + self.assertEqual(result[1], ([1], 500)) + + def test_compute_block_targets_multi_sys_block(self): + result = compute_block_targets( + "prob_sys_size;0:2:0.5;2:3:0.5", + nsystems=3, + system_nframes=[100, 200, 300], + ) + self.assertEqual(result, []) + + def test_compute_block_targets_asymmetric(self): + result = compute_block_targets( + "prob_sys_size;0:2:0.5;2:3:0.5", + nsystems=3, + system_nframes=[50, 50, 400], + ) + self.assertEqual(len(result), 2) + self.assertEqual(result[0][0], [0, 1]) + self.assertEqual(result[0][1], 400) + + def test_expand_indices_basic(self): + frame_system_ids = [0] * 5 + [1] * 5 + block_targets = [([0], 25), ([1], 25)] + rng = np.random.default_rng(42) + expanded = _expand_indices_by_blocks( + list(range(10)), frame_system_ids, block_targets, rng + ) + sys0 = [i for i in expanded if frame_system_ids[i] == 0] + sys1 = [i for i in expanded if frame_system_ids[i] == 1] + self.assertEqual(len(sys0), 25) + self.assertEqual(len(sys1), 25) + + def test_expand_indices_no_expansion(self): + frame_system_ids = [0] * 5 + [1] * 5 + block_targets = [([0], 5), ([1], 5)] + rng = np.random.default_rng(42) + expanded = _expand_indices_by_blocks( + list(range(10)), frame_system_ids, block_targets, rng + ) + self.assertEqual(sorted(expanded), list(range(10))) + + def test_expand_indices_remainder_sampling(self): + from collections import ( + Counter, + ) + + frame_system_ids = [0] * 10 + block_targets = [([0], 23)] + rng = np.random.default_rng(42) + expanded = _expand_indices_by_blocks( + list(range(10)), frame_system_ids, block_targets, rng + ) + self.assertEqual(len(expanded), 23) + counts = Counter(expanded) + n_three = sum(1 for c in counts.values() if c == 3) + self.assertEqual(n_three, 3) + + def test_expand_epoch_diversity(self): + frame_system_ids = [0] * 10 + block_targets = [([0], 15)] + results = [] + for seed in range(5): + rng = np.random.default_rng(seed) + expanded = _expand_indices_by_blocks( + list(range(10)), frame_system_ids, block_targets, rng + ) + results.append(sorted(expanded[10:])) + unique = {tuple(r) for r in results} + self.assertGreater(len(unique), 1) + + def test_sampler_with_block_targets(self): + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + block_targets = compute_block_targets( + "prob_sys_size;0:1:0.5;1:3:0.5", + nsystems=3, + system_nframes=[100, 200, 300], + ) + sampler = SameNlocBatchSampler( + reader, shuffle=True, block_targets=block_targets + ) + all_indices = [i for batch in sampler for i in batch] + self.assertGreater(len(all_indices), 600) + self.assertEqual(len(set(all_indices)), 600) + + def test_sampler_without_block_targets(self): + reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) + sampler = SameNlocBatchSampler(reader, shuffle=False) + all_indices = [i for batch in sampler for i in batch] + self.assertEqual(sorted(all_indices), list(range(600))) + + +# ============================================================ +# Neighbor stat from LMDB tests +# ============================================================ + + +class TestLmdbNeighborStat(unittest.TestCase): + """Test make_neighbor_stat_data interface and sampling.""" + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + cls._lmdb_path = _create_grid_lmdb(f"{cls._tmpdir.name}/grid.lmdb", nframes=3) + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + def test_make_neighbor_stat_data_interface(self): + data = make_neighbor_stat_data(self._lmdb_path, ["TYPE", "NO_TYPE"]) + self.assertIsInstance(data.system_dirs, list) + self.assertGreater(len(data.system_dirs), 0) + self.assertEqual(data.get_ntypes(), 2) + data.get_batch() # no-op + sys0 = data.data_systems[0] + self.assertIsInstance(sys0.pbc, bool) + set_data = sys0._load_set(sys0.dirs[0]) + self.assertEqual(set_data["coord"].ndim, 2) + self.assertEqual(set_data["coord"].shape[1], sys0.get_natoms() * 3) + + def test_sampling_large_dataset(self): + tmpdir = tempfile.TemporaryDirectory() + path = _create_grid_lmdb(f"{tmpdir.name}/large.lmdb", nframes=50) + data = make_neighbor_stat_data(path, ["TYPE"], max_frames=10) + total = sum(s._load_set(s.dirs[0])["coord"].shape[0] for s in data.data_systems) + self.assertEqual(total, 10) + tmpdir.cleanup() + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/consistent/test_lmdb_data.py b/source/tests/consistent/test_lmdb_data.py index 168acf0d0d..342ddc63f5 100644 --- a/source/tests/consistent/test_lmdb_data.py +++ b/source/tests/consistent/test_lmdb_data.py @@ -3,7 +3,6 @@ Verifies that the framework-agnostic reader and the PyTorch wrapper produce identical outputs for the same LMDB data. -Also tests SameNlocBatchSampler and mixed_batch guards. """ import tempfile @@ -15,11 +14,6 @@ from deepmd.dpmodel.utils.lmdb_data import ( LmdbDataReader, - LmdbTestData, - SameNlocBatchSampler, - _expand_indices_by_blocks, - compute_block_targets, - is_lmdb, ) try: @@ -34,10 +28,7 @@ def _make_frame(natoms: int = 6, seed: int = 0) -> dict: - """Create a synthetic frame dict for testing. - - Generates atom_types with roughly 1/3 type-0 and 2/3 type-1. - """ + """Create a synthetic frame dict for testing.""" rng = np.random.RandomState(seed) n_type0 = max(1, natoms // 3) n_type1 = natoms - n_type0 @@ -108,21 +99,15 @@ def _create_lmdb(path: str, nframes: int = 10, natoms: int = 6) -> str: def _create_mixed_nloc_lmdb(path: str) -> str: - """Create an LMDB with frames of different atom counts. - - Frames 0-3: 6 atoms, Frames 4-7: 9 atoms, Frames 8-9: 12 atoms. - """ - frames_spec = [(6, 4), (9, 4), (12, 2)] # (natoms, count) + """Create an LMDB with frames of different atom counts.""" + frames_spec = [(6, 4), (9, 4), (12, 2)] total = sum(c for _, c in frames_spec) env = lmdb.open(path, map_size=10 * 1024 * 1024) with env.begin(write=True) as txn: meta = { "nframes": total, "frame_idx_fmt": "012d", - "system_info": { - "natoms": [2, 4], # first frame's type counts - "formula": "mixed", - }, + "system_info": {"natoms": [2, 4], "formula": "mixed"}, } txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) idx = 0 @@ -139,11 +124,7 @@ def _create_mixed_nloc_lmdb(path: str) -> str: return path -# ============================================================ -# Uniform nloc tests -# ============================================================ - - +@unittest.skipUnless(INSTALLED_PT, "PyTorch not available") class TestLmdbDataConsistency(unittest.TestCase): """Verify LmdbDataReader (dpmodel) and LmdbDataset (pt) produce identical outputs.""" @@ -161,13 +142,9 @@ def tearDownClass(cls): def test_same_len(self): reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(len(reader), 10) - if INSTALLED_PT: - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(len(ds), 10) - self.assertEqual(len(reader), len(ds)) + ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) + self.assertEqual(len(reader), len(ds)) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_same_frame_data(self): reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) @@ -185,13 +162,11 @@ def test_same_frame_data(self): else: self.assertEqual(dp_val, pt_val, msg=f"key={key}, frame={i}") - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_same_batch_size(self): reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size="auto") ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size="auto") self.assertEqual(reader.batch_size, ds.batch_size) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_same_properties(self): reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) @@ -199,7 +174,6 @@ def test_same_properties(self): self.assertEqual(reader.total_batch, ds.total_batch) self.assertEqual(reader.batch_sizes, ds.batch_sizes) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_data_requirement(self): req = [ { @@ -221,54 +195,10 @@ def test_data_requirement(self): np.testing.assert_array_equal(frame_dp["virial"], frame_pt["virial"]) self.assertEqual(frame_dp["find_virial"], frame_pt["find_virial"]) - def test_lmdb_test_data(self): - td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - result = td.get_test() - self.assertEqual(result["coord"].shape, (10, 18)) - self.assertEqual(result["box"].shape, (10, 9)) - self.assertEqual(result["type"].shape, (10, 6)) - self.assertEqual(result["energy"].shape, (10, 1)) - self.assertEqual(result["force"].shape, (10, 18)) - self.assertEqual(result["find_energy"], 1.0) - self.assertEqual(result["find_force"], 1.0) - - def test_is_lmdb(self): - self.assertTrue(is_lmdb(self._lmdb_path)) - self.assertTrue(is_lmdb("something.lmdb")) - self.assertFalse(is_lmdb("/some/npy/system")) - self.assertFalse(is_lmdb(["list", "of", "systems"])) - - def test_reader_standalone(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - frame = reader[0] - self.assertIn("coord", frame) - self.assertIn("energy", frame) - self.assertIn("force", frame) - self.assertIn("atype", frame) - self.assertIn("box", frame) - self.assertIn("natoms", frame) - self.assertIn("real_natoms_vec", frame) - self.assertIn("find_energy", frame) - self.assertEqual(frame["coord"].dtype, np.float64) - self.assertEqual(frame["atype"].dtype, np.int64) - - def test_uniform_nloc_single_group(self): - """Uniform-nloc LMDB has exactly one nloc group.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(len(reader.nloc_groups), 1) - self.assertIn(6, reader.nloc_groups) - self.assertEqual(len(reader.nloc_groups[6]), 10) - -# ============================================================ -# Mixed nloc tests -# ============================================================ - - -class TestMixedNloc(unittest.TestCase): - """Tests for mixed-nloc datasets and SameNlocBatchSampler.""" +@unittest.skipUnless(INSTALLED_PT, "PyTorch not available") +class TestMixedNlocConsistency(unittest.TestCase): + """Consistency tests for mixed-nloc LMDB: collate, LmdbDataset iteration.""" @classmethod def setUpClass(cls): @@ -280,143 +210,25 @@ def setUpClass(cls): def tearDownClass(cls): cls._tmpdir.cleanup() - def test_nloc_groups_detected(self): - """LmdbDataReader correctly groups frames by nloc.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(set(reader.nloc_groups.keys()), {6, 9, 12}) - self.assertEqual(len(reader.nloc_groups[6]), 4) - self.assertEqual(len(reader.nloc_groups[9]), 4) - self.assertEqual(len(reader.nloc_groups[12]), 2) - - def test_per_frame_natoms_vec(self): - """Each frame gets its own natoms_vec matching its actual atom count.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - frame0 = reader[0] # 6 atoms - frame4 = reader[4] # 9 atoms - frame8 = reader[8] # 12 atoms - self.assertEqual(frame0["natoms"][0], 6) - self.assertEqual(frame4["natoms"][0], 9) - self.assertEqual(frame8["natoms"][0], 12) - np.testing.assert_array_equal(frame0["real_natoms_vec"], frame0["natoms"]) - - def test_per_frame_shapes(self): - """coord/force/atype shapes match per-frame atom count.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - frame0 = reader[0] # 6 atoms - frame4 = reader[4] # 9 atoms - self.assertEqual(frame0["coord"].shape, (6, 3)) - self.assertEqual(frame0["force"].shape, (6, 3)) - self.assertEqual(frame0["atype"].shape, (6,)) - self.assertEqual(frame4["coord"].shape, (9, 3)) - self.assertEqual(frame4["force"].shape, (9, 3)) - self.assertEqual(frame4["atype"].shape, (9,)) - - def test_frame_nlocs(self): - """frame_nlocs returns correct per-frame atom counts.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - expected = [6, 6, 6, 6, 9, 9, 9, 9, 12, 12] - self.assertEqual(reader.frame_nlocs, expected) - - # --- SameNlocBatchSampler tests --- - - def test_sampler_all_batches_same_nloc(self): - """Every batch from SameNlocBatchSampler has frames with identical nloc.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - sampler = SameNlocBatchSampler(reader, shuffle=False, seed=42) - for batch_indices in sampler: - nlocs_in_batch = [reader.frame_nlocs[i] for i in batch_indices] - self.assertTrue( - all(n == nlocs_in_batch[0] for n in nlocs_in_batch), - f"Mixed nloc in batch: {nlocs_in_batch} (indices={batch_indices})", - ) - - def test_sampler_covers_all_frames(self): - """SameNlocBatchSampler yields every frame exactly once.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - sampler = SameNlocBatchSampler(reader, shuffle=False, seed=42) - all_indices = [] - for batch_indices in sampler: - all_indices.extend(batch_indices) - self.assertEqual(sorted(all_indices), list(range(10))) - - def test_sampler_auto_batch_size_per_nloc(self): - """Auto batch_size varies by nloc group.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size="auto") - bs_6 = reader.get_batch_size_for_nloc(6) - bs_9 = reader.get_batch_size_for_nloc(9) - bs_12 = reader.get_batch_size_for_nloc(12) - # Larger nloc → smaller batch_size - self.assertGreaterEqual(bs_6, bs_9) - self.assertGreaterEqual(bs_9, bs_12) - - def test_sampler_shuffle_deterministic(self): - """Same seed produces same batch order.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - s1 = SameNlocBatchSampler(reader, shuffle=True, seed=123) - s2 = SameNlocBatchSampler(reader, shuffle=True, seed=123) - batches1 = list(s1) - batches2 = list(s2) - self.assertEqual(batches1, batches2) - - def test_sampler_len(self): - """__len__ matches actual number of batches yielded.""" - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - sampler = SameNlocBatchSampler(reader, shuffle=False) - batches = list(sampler) - self.assertEqual(len(sampler), len(batches)) - - # --- Collate guard tests --- - - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_collate_mixed_nloc_raises(self): - """Collating frames with different nloc raises NotImplementedError.""" reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - frame_6 = reader[0] - frame_9 = reader[4] - with self.assertRaises(NotImplementedError) as ctx: - _collate_lmdb_batch([frame_6, frame_9]) - self.assertIn("mixed_batch", str(ctx.exception)) + with self.assertRaises(NotImplementedError): + _collate_lmdb_batch([reader[0], reader[4]]) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_collate_same_nloc_ok(self): - """Collating frames with same nloc works fine.""" reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - frame0 = reader[0] - frame1 = reader[1] - batch = _collate_lmdb_batch([frame0, frame1]) - self.assertIn("coord", batch) + batch = _collate_lmdb_batch([reader[0], reader[1]]) self.assertEqual(batch["coord"].shape[0], 2) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_mixed_batch_true_raises(self): - """LmdbDataset(mixed_batch=True) raises NotImplementedError.""" - with self.assertRaises(NotImplementedError) as ctx: + with self.assertRaises(NotImplementedError): LmdbDataset(self._lmdb_path, self._type_map, batch_size=2, mixed_batch=True) - self.assertIn("mixed_batch", str(ctx.exception)) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") - def test_pt_dataset_iterates_same_nloc_batches(self): - """LmdbDataset iteration produces only same-nloc batches.""" - import torch - - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - with torch.device("cpu"): - for batch in ds.dataloaders[0]: - atype = batch.get("atype") - if atype is not None: - # All frames in batch have same nloc - self.assertEqual(atype.shape[1], atype.shape[1]) - break # just check first batch - - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_pt_dataset_mixed_batch_flag(self): - """LmdbDataset exposes mixed_batch from reader.""" ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) self.assertFalse(ds.mixed_batch) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_pt_full_epoch_mixed_nloc(self): - """Full DataLoader epoch over mixed-nloc LMDB: all batches same-nloc, all frames covered.""" import torch ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) @@ -429,12 +241,9 @@ def test_pt_full_epoch_mixed_nloc(self): for i in range(atype.shape[0]): self.assertEqual(atype[i].shape[0], nloc) all_fids.extend(batch["fid"]) - # All 10 frames should be covered self.assertEqual(sorted(all_fids), list(range(10))) - @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") def test_pt_batch_shapes_consistent(self): - """Within each batch, coord/force/natoms shapes are consistent with atype.""" import torch ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=3) @@ -444,564 +253,109 @@ def test_pt_batch_shapes_consistent(self): nloc = batch["atype"].shape[1] self.assertEqual(batch["coord"].shape, (bs, nloc, 3)) self.assertEqual(batch["force"].shape, (bs, nloc, 3)) - self.assertEqual(batch["natoms"].shape, (bs, 4)) # ntypes=2 → 2+2=4 - # natoms_vec[0] should equal nloc for all frames - for i in range(bs): - self.assertEqual(batch["natoms"][i, 0].item(), nloc) - - # --- LmdbTestData mixed-nloc tests --- - - def test_test_data_nloc_groups(self): - """LmdbTestData detects nloc groups in mixed-nloc LMDB.""" - td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) - self.assertEqual(set(td.nloc_groups.keys()), {6, 9, 12}) - self.assertEqual(len(td.nloc_groups[6]), 4) - self.assertEqual(len(td.nloc_groups[9]), 4) - self.assertEqual(len(td.nloc_groups[12]), 2) - - def test_test_data_get_test_specific_nloc(self): - """get_test(nloc=N) returns only frames with that atom count.""" - td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - - result_6 = td.get_test(nloc=6) - self.assertEqual(result_6["coord"].shape, (4, 6 * 3)) - self.assertEqual(result_6["force"].shape, (4, 6 * 3)) - self.assertEqual(result_6["type"].shape, (4, 6)) - - result_9 = td.get_test(nloc=9) - self.assertEqual(result_9["coord"].shape, (4, 9 * 3)) - self.assertEqual(result_9["force"].shape, (4, 9 * 3)) - self.assertEqual(result_9["type"].shape, (4, 9)) - - result_12 = td.get_test(nloc=12) - self.assertEqual(result_12["coord"].shape, (2, 12 * 3)) - self.assertEqual(result_12["force"].shape, (2, 12 * 3)) - self.assertEqual(result_12["type"].shape, (2, 12)) - - def test_test_data_get_test_default_mixed(self): - """get_test() without nloc on mixed data returns largest group.""" - td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - # Largest groups are nloc=6 and nloc=9 (both 4 frames). - # max() picks the one with the largest nloc among tied groups. - result = td.get_test() - nframes = result["coord"].shape[0] - self.assertEqual(nframes, 4) - - def test_test_data_get_test_invalid_nloc(self): - """get_test(nloc=999) raises ValueError.""" - td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) - with self.assertRaises(ValueError): - td.get_test(nloc=999) - - def test_test_data_uniform_nloc_no_warning(self): - """Uniform-nloc LMDB: get_test() returns all frames without warning.""" - tmpdir = tempfile.TemporaryDirectory() - path = _create_lmdb(f"{tmpdir.name}/uniform.lmdb", nframes=5, natoms=6) - td = LmdbTestData(path, type_map=self._type_map, shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - result = td.get_test() - self.assertEqual(result["coord"].shape, (5, 18)) - tmpdir.cleanup() - - -def _create_lmdb_with_type_map( - path: str, - nframes: int = 6, - natoms: int = 6, - lmdb_type_map: list[str] | None = None, -) -> str: - """Create a test LMDB with type_map stored in metadata. - - Atom types: first 1/3 are type-0, rest are type-1. - """ - n_type0 = max(1, natoms // 3) - n_type1 = natoms - n_type0 - env = lmdb.open(path, map_size=10 * 1024 * 1024) - with env.begin(write=True) as txn: - meta = { - "nframes": nframes, - "frame_idx_fmt": "012d", - "system_info": { - "natoms": [n_type0, n_type1], - }, - } - if lmdb_type_map is not None: - meta["type_map"] = lmdb_type_map - txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) - for i in range(nframes): - key = format(i, "012d").encode() - frame = _make_frame(natoms=natoms, seed=i) - txn.put(key, msgpack.packb(frame, use_bin_type=True)) - env.close() - return path - - -# ============================================================ -# Type map remapping tests -# ============================================================ + self.assertEqual(batch["natoms"].shape, (bs, 4)) -class TestTypeMapRemapping(unittest.TestCase): - """Test type_map remapping in LmdbDataReader and LmdbTestData.""" +@unittest.skipUnless(INSTALLED_PT, "PyTorch not available") +class TestLmdbNeighborStatConsistency(unittest.TestCase): + """Test neighbor stat values from LMDB match expected geometry.""" @classmethod def setUpClass(cls): cls._tmpdir = tempfile.TemporaryDirectory() - # LMDB with type_map=["O","H"]: type-0 = O, type-1 = H - cls._lmdb_path = _create_lmdb_with_type_map( - f"{cls._tmpdir.name}/remap.lmdb", - nframes=6, - natoms=6, - lmdb_type_map=["O", "H"], - ) - - @classmethod - def tearDownClass(cls): - cls._tmpdir.cleanup() - - # --- LmdbDataReader tests --- - - def test_reader_no_remap_when_match(self): - """No remap when model type_map matches LMDB type_map.""" - reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) - self.assertIsNone(reader._type_remap) - frame = reader[0] - # type-0 = O, type-1 = H, unchanged - self.assertEqual(frame["atype"][0], 0) - - def test_reader_remap_when_reversed(self): - """Remap when model type_map=["H","O"] vs LMDB ["O","H"].""" - reader = LmdbDataReader(self._lmdb_path, ["H", "O"]) - self.assertIsNotNone(reader._type_remap) - np.testing.assert_array_equal(reader._type_remap, [1, 0]) - frame = reader[0] - # Original type-0 (O) -> 1, type-1 (H) -> 0 - atype = frame["atype"] - n_type0_orig = max(1, 6 // 3) # 2 atoms of original type-0 - # First n_type0_orig atoms were type-0 (O), now should be 1 - for i in range(n_type0_orig): - self.assertEqual(atype[i], 1) - # Remaining atoms were type-1 (H), now should be 0 - for i in range(n_type0_orig, 6): - self.assertEqual(atype[i], 0) - - def test_reader_remap_superset(self): - """Remap when model type_map has extra elements.""" - reader = LmdbDataReader(self._lmdb_path, ["C", "O", "H"]) - self.assertIsNotNone(reader._type_remap) - np.testing.assert_array_equal(reader._type_remap, [1, 2]) - frame = reader[0] - # O -> 1, H -> 2 - n_type0_orig = max(1, 6 // 3) - for i in range(n_type0_orig): - self.assertEqual(frame["atype"][i], 1) - for i in range(n_type0_orig, 6): - self.assertEqual(frame["atype"][i], 2) - - def test_reader_natoms_vec_after_remap(self): - """natoms_vec reflects remapped types.""" - reader = LmdbDataReader(self._lmdb_path, ["H", "O"]) - frame = reader[0] - natoms = frame["natoms"] - # model type_map=["H","O"], ntypes=2 - # Original: 2 O + 4 H. After remap: O->1, H->0 - # So count_H(idx=0)=4, count_O(idx=1)=2 - self.assertEqual(natoms[0], 6) # nloc - self.assertEqual(natoms[2], 4) # H count (model idx 0) - self.assertEqual(natoms[3], 2) # O count (model idx 1) - - def test_reader_missing_element_raises(self): - """ValueError when model type_map misses an LMDB element.""" - with self.assertRaises(ValueError): - LmdbDataReader(self._lmdb_path, ["O"]) # missing H - - def test_reader_no_type_map_in_metadata(self): - """Old LMDB without type_map -> no remap.""" - tmpdir = tempfile.TemporaryDirectory() - path = _create_lmdb_with_type_map( - f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None - ) - reader = LmdbDataReader(path, ["H", "O"]) - self.assertIsNone(reader._type_remap) - tmpdir.cleanup() - - # --- LmdbTestData tests --- - - def test_testdata_no_remap_when_match(self): - """LmdbTestData: no remap when type_maps match.""" - td = LmdbTestData(self._lmdb_path, type_map=["O", "H"], shuffle_test=False) - self.assertIsNone(td._type_remap) - data = td.get_test() - self.assertEqual(data["type"][0, 0], 0) - - def test_testdata_remap_when_reversed(self): - """LmdbTestData: remap when model ["H","O"] vs LMDB ["O","H"].""" - td = LmdbTestData(self._lmdb_path, type_map=["H", "O"], shuffle_test=False) - self.assertIsNotNone(td._type_remap) - data = td.get_test() - n_type0_orig = max(1, 6 // 3) - # Original type-0 (O) -> 1, type-1 (H) -> 0 - for i in range(n_type0_orig): - self.assertEqual(data["type"][0, i], 1) - for i in range(n_type0_orig, 6): - self.assertEqual(data["type"][0, i], 0) - - def test_testdata_remap_superset(self): - """LmdbTestData: remap with superset model type_map.""" - td = LmdbTestData(self._lmdb_path, type_map=["C", "O", "H"], shuffle_test=False) - self.assertIsNotNone(td._type_remap) - data = td.get_test() - n_type0_orig = max(1, 6 // 3) - # O -> 1, H -> 2 - for i in range(n_type0_orig): - self.assertEqual(data["type"][0, i], 1) - for i in range(n_type0_orig, 6): - self.assertEqual(data["type"][0, i], 2) - - def test_testdata_missing_element_raises(self): - """LmdbTestData: ValueError when model misses LMDB element.""" - with self.assertRaises(ValueError): - LmdbTestData(self._lmdb_path, type_map=["O"], shuffle_test=False) - - def test_testdata_no_type_map_in_metadata(self): - """LmdbTestData: old LMDB without type_map -> no remap.""" - tmpdir = tempfile.TemporaryDirectory() - path = _create_lmdb_with_type_map( - f"{tmpdir.name}/old.lmdb", nframes=3, natoms=6, lmdb_type_map=None - ) - td = LmdbTestData(path, type_map=["H", "O"], shuffle_test=False) - self.assertIsNone(td._type_remap) - tmpdir.cleanup() - - -# ============================================================ -# auto_prob / frame_system_ids tests -# ============================================================ - - -def _create_lmdb_with_system_ids( - path: str, - system_frames: list[int], - natoms: int = 6, - type_map: list[str] | None = None, -) -> str: - """Create a test LMDB with frame_system_ids in metadata. - - Parameters - ---------- - system_frames : list[int] - Number of frames per system, e.g. [100, 500] means sys0 has 100 - frames and sys1 has 500 frames. - """ - total = sum(system_frames) - n_type0 = max(1, natoms // 3) - n_type1 = natoms - n_type0 - frame_system_ids = [] - for sid, nf in enumerate(system_frames): - frame_system_ids.extend([sid] * nf) - - env = lmdb.open(path, map_size=50 * 1024 * 1024) - with env.begin(write=True) as txn: - meta = { - "nframes": total, - "frame_idx_fmt": "012d", - "system_info": {"natoms": [n_type0, n_type1]}, - "frame_system_ids": frame_system_ids, - "frame_nlocs": [natoms] * total, - } - if type_map is not None: - meta["type_map"] = type_map - txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) - for i in range(total): - key = format(i, "012d").encode() - frame = _make_frame(natoms=natoms, seed=i % 100) - txn.put(key, msgpack.packb(frame, use_bin_type=True)) - env.close() - return path - - -def _create_lmdb_with_system_ids_mixed_nloc( - path: str, - system_specs: list[tuple[int, int, int]], - type_map: list[str] | None = None, -) -> str: - """Create a test LMDB with frame_system_ids and mixed nloc. - - Parameters - ---------- - system_specs : list[tuple[int, int, int]] - Each tuple is (system_id, natoms, nframes). - """ - total = sum(nf for _, _, nf in system_specs) - frame_system_ids = [] - frame_nlocs = [] - frames_data = [] - for sid, natoms, nf in system_specs: - for _ in range(nf): - frame_system_ids.append(sid) - frame_nlocs.append(natoms) - frames_data.append(_make_frame(natoms=natoms, seed=len(frames_data) % 100)) - - env = lmdb.open(path, map_size=50 * 1024 * 1024) - with env.begin(write=True) as txn: - meta = { - "nframes": total, - "frame_idx_fmt": "012d", - "system_info": {"natoms": [2, 4]}, - "frame_system_ids": frame_system_ids, - "frame_nlocs": frame_nlocs, - } - if type_map is not None: - meta["type_map"] = type_map - txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) - for i, frame in enumerate(frames_data): - key = format(i, "012d").encode() - txn.put(key, msgpack.packb(frame, use_bin_type=True)) - env.close() - return path - - -class TestAutoProb(unittest.TestCase): - """Test auto_prob support: frame_system_ids, compute_block_targets, - _expand_indices_by_blocks, and SameNlocBatchSampler with block_targets. - """ - - @classmethod - def setUpClass(cls): - cls._tmpdir = tempfile.TemporaryDirectory() - # 3 systems: sys0=100 frames, sys1=200 frames, sys2=300 frames - cls._lmdb_path = _create_lmdb_with_system_ids( - f"{cls._tmpdir.name}/auto_prob.lmdb", - system_frames=[100, 200, 300], - natoms=6, - type_map=["O", "H"], - ) + X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T + natoms = 27 + cell = np.array([3.0, 0, 0, 0, 3.0, 0, 0, 0, 3.0], dtype=np.float64) + atype = np.zeros(natoms, dtype=np.int64) + path = f"{cls._tmpdir.name}/grid.lmdb" + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": 3, + "frame_idx_fmt": "012d", + "type_map": ["TYPE"], + "system_info": {"natoms": [natoms], "formula": "grid"}, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + for i in range(3): + frame = { + "atom_types": { + "type": " no expansion needed.""" - # sys0=100, sys1=100; prob 0.5 each - # ratio = 100/0.5 = 200 for both -> target = 100 each -> no expansion - result = compute_block_targets( - "prob_sys_size;0:1:0.5;1:2:0.5", - nsystems=2, - system_nframes=[100, 100], - ) - # No expansion needed (targets == actual) - self.assertEqual(result, []) - - def test_compute_block_targets_unequal(self): - """Unequal frames with equal weight -> expansion needed.""" - # sys0=100, sys1=500; prob 0.5 each - # ratio_A = 100/0.5 = 200, ratio_B = 500/0.5 = 1000 - # total_target = ceil(max(200, 1000)) = 1000 - # target_A = round(1000 * 0.5) = 500, target_B = round(1000 * 0.5) = 500 - result = compute_block_targets( - "prob_sys_size;0:1:0.5;1:2:0.5", - nsystems=2, - system_nframes=[100, 500], - ) - self.assertEqual(len(result), 2) - sys_ids_a, target_a = result[0] - sys_ids_b, target_b = result[1] - self.assertEqual(sys_ids_a, [0]) - self.assertEqual(sys_ids_b, [1]) - self.assertEqual(target_a, 500) - self.assertEqual(target_b, 500) - - def test_compute_block_targets_multi_sys_block(self): - """Block spanning multiple systems.""" - # sys0=100, sys1=200, sys2=300; block A=[0,1] prob=0.5, block B=[2] prob=0.5 - # block_A frames=300, block_B frames=300 - # ratio_A = 300/0.5 = 600, ratio_B = 300/0.5 = 600 - # total_target = 600, target_A = 300, target_B = 300 -> no expansion - result = compute_block_targets( - "prob_sys_size;0:2:0.5;2:3:0.5", - nsystems=3, - system_nframes=[100, 200, 300], - ) - self.assertEqual(result, []) - - def test_compute_block_targets_asymmetric(self): - """Asymmetric: small block needs expansion.""" - # sys0=50, sys1=50, sys2=400; block A=[0,1] prob=0.5, block B=[2] prob=0.5 - # block_A frames=100, block_B frames=400 - # ratio_A = 100/0.5 = 200, ratio_B = 400/0.5 = 800 - # total_target = 800, target_A = 400, target_B = 400 - result = compute_block_targets( - "prob_sys_size;0:2:0.5;2:3:0.5", - nsystems=3, - system_nframes=[50, 50, 400], + def test_neighbor_stat_values(self): + """Neighbor stat from LMDB matches expected values for grid geometry.""" + from deepmd.dpmodel.utils.lmdb_data import ( + make_neighbor_stat_data, ) - self.assertEqual(len(result), 2) - sys_ids_a, target_a = result[0] - sys_ids_b, target_b = result[1] - self.assertEqual(sys_ids_a, [0, 1]) - self.assertEqual(sys_ids_b, [2]) - self.assertEqual(target_a, 400) - self.assertEqual(target_b, 400) - - # --- _expand_indices_by_blocks --- - - def test_expand_indices_basic(self): - """Expand indices: block A needs 5x expansion.""" - # 10 frames: sys0=[0..4], sys1=[5..9] - frame_system_ids = [0] * 5 + [1] * 5 - block_targets = [([0], 25), ([1], 25)] - rng = np.random.default_rng(42) - indices = list(range(10)) - expanded = _expand_indices_by_blocks( - indices, - frame_system_ids, - block_targets, - rng, - ) - # Each block should have 25 indices - sys0_expanded = [i for i in expanded if frame_system_ids[i] == 0] - sys1_expanded = [i for i in expanded if frame_system_ids[i] == 1] - self.assertEqual(len(sys0_expanded), 25) - self.assertEqual(len(sys1_expanded), 25) - # All original indices present - self.assertTrue(set(range(5)).issubset(set(sys0_expanded))) - self.assertTrue(set(range(5, 10)).issubset(set(sys1_expanded))) - - def test_expand_indices_no_expansion(self): - """No expansion when target == actual.""" - frame_system_ids = [0] * 5 + [1] * 5 - block_targets = [([0], 5), ([1], 5)] - rng = np.random.default_rng(42) - indices = list(range(10)) - expanded = _expand_indices_by_blocks( - indices, - frame_system_ids, - block_targets, - rng, - ) - self.assertEqual(sorted(expanded), list(range(10))) - - def test_expand_indices_remainder_sampling(self): - """Remainder part uses without-replacement sampling.""" - # sys0=10 frames, target=23 -> 1 full copy + 3 remainder - frame_system_ids = [0] * 10 - block_targets = [([0], 23)] - rng = np.random.default_rng(42) - indices = list(range(10)) - expanded = _expand_indices_by_blocks( - indices, - frame_system_ids, - block_targets, - rng, - ) - self.assertEqual(len(expanded), 23) - # Each original index appears at least twice (1 original + 1 full copy) - from collections import ( - Counter, + from deepmd.pt.utils.neighbor_stat import ( + NeighborStat, ) - counts = Counter(expanded) - for i in range(10): - self.assertGreaterEqual(counts[i], 2) - # 3 indices appear 3 times (the remainder) - n_three = sum(1 for c in counts.values() if c == 3) - self.assertEqual(n_three, 3) - - def test_expand_epoch_diversity(self): - """Different RNG seeds produce different remainder samples.""" - frame_system_ids = [0] * 10 - block_targets = [([0], 15)] # 10 + 5 remainder - indices = list(range(10)) - results = [] - for seed in range(5): - rng = np.random.default_rng(seed) - expanded = _expand_indices_by_blocks( - indices, - frame_system_ids, - block_targets, - rng, - ) - # Sort the "extra" part (beyond the first 10) - results.append(sorted(expanded[10:])) - # At least 2 different remainder sets across 5 seeds - unique = {tuple(r) for r in results} - self.assertGreater(len(unique), 1) - - # --- SameNlocBatchSampler with block_targets --- - - def test_sampler_with_block_targets(self): - """SameNlocBatchSampler expands frames when block_targets provided.""" - reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) - # sys0=100, sys1=200, sys2=300; make block A=[0] prob=0.5, B=[1,2] prob=0.5 - # block_A frames=100, block_B frames=500 - # ratio_A=200, ratio_B=1000 -> total_target=1000 - # target_A=500, target_B=500 - block_targets = compute_block_targets( - "prob_sys_size;0:1:0.5;1:3:0.5", - nsystems=3, - system_nframes=[100, 200, 300], - ) - self.assertTrue(len(block_targets) > 0) + type_map = ["TYPE", "NO_THIS_TYPE"] + data = make_neighbor_stat_data(self._lmdb_path, type_map) + + for rcut in (1.0, 2.0, 4.0): + for mixed_type in (True, False): + with self.subTest(rcut=rcut, mixed_type=mixed_type): + rcut_eps = rcut + 1e-3 + nei = NeighborStat(len(type_map), rcut_eps, mixed_type=mixed_type) + min_nbor_dist, max_nbor_size = nei.get_stat(data) + + upper = int(np.ceil(rcut_eps)) + 1 + X, Y, Z = np.mgrid[-upper:upper, -upper:upper, -upper:upper] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T + distance = np.linalg.norm(positions, axis=1) + expected_neighbors = np.count_nonzero( + np.logical_and(distance > 0, distance <= rcut_eps) + ) - sampler = SameNlocBatchSampler( - reader, - shuffle=True, - block_targets=block_targets, - ) - all_indices = [] - for batch in sampler: - all_indices.extend(batch) - # Total should be ~1000 (500 from block A + 500 from block B) - self.assertGreater(len(all_indices), 600) # more than original 600 - # All original indices should appear at least once - self.assertEqual(len(set(all_indices)), 600) - - def test_sampler_without_block_targets(self): - """SameNlocBatchSampler without block_targets: no expansion.""" - reader = LmdbDataReader(self._lmdb_path, ["O", "H"]) - sampler = SameNlocBatchSampler(reader, shuffle=False) - all_indices = [] - for batch in sampler: - all_indices.extend(batch) - self.assertEqual(len(all_indices), 600) - self.assertEqual(sorted(all_indices), list(range(600))) + self.assertAlmostEqual(min_nbor_dist, 1.0, places=6) + expected = [expected_neighbors] + if not mixed_type: + expected.append(0) + np.testing.assert_array_equal(max_nbor_size, expected) if __name__ == "__main__": From 24b9060749607349254b57f4c5985aa0e52a2ca1 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:23:06 +0800 Subject: [PATCH 12/22] fix repeat keys --- deepmd/dpmodel/utils/lmdb_data.py | 143 +++--- source/tests/pt/test_lmdb_dataloader.py | 640 ++++++------------------ 2 files changed, 238 insertions(+), 545 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 4874fa30ed..7592611fd0 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -406,54 +406,75 @@ def __getitem__(self, index: int) -> dict[str, Any]: frame["natoms"] = fallback frame["real_natoms_vec"] = fallback - # Add find_* flags for known label keys - label_keys = [ - "energy", - "force", - "virial", - "atom_ener", - "atom_pref", - "drdq", - "atom_ener_coeff", - "hessian", - ] - for lk in label_keys: - frame[f"find_{lk}"] = np.float32(1.0) if lk in frame else np.float32(0.0) + # Add find_* flags for all data keys present in the frame. + # Core structural keys and metadata are excluded — only label-like + # and auxiliary data keys get find_* flags. + _structural_keys = frozenset( + { + "coord", + "box", + "atype", + "natoms", + "real_natoms_vec", + "fid", + } + ) + for fk in list(frame.keys()): + if fk.startswith("find_") or fk in _structural_keys: + continue + # Skip keys handled by data_requirements (processed below) + if fk in self._data_requirements: + continue + if f"find_{fk}" not in frame: + frame[f"find_{fk}"] = np.float32(1.0) - # Handle registered data requirements: fill defaults for missing keys + # Handle registered data requirements: fill defaults for missing keys, + # apply repeat, and cast dtype. for req_key, req_item in self._data_requirements.items(): + # Extract requirement fields (support both dict and object) + if isinstance(req_item, dict): + ndof = req_item["ndof"] + default = req_item["default"] + atomic = req_item["atomic"] + repeat = req_item.get("repeat", 1) + req_dtype = req_item.get("dtype") + if req_dtype is None: + req_dtype = ( + GLOBAL_ENER_FLOAT_PRECISION + if req_item.get("high_prec", False) + else GLOBAL_NP_FLOAT_PRECISION + ) + else: + ndof = req_item.ndof + default = req_item.default + atomic = req_item.atomic + repeat = getattr(req_item, "repeat", 1) + req_dtype = req_item.dtype + if req_dtype is None: + req_dtype = ( + GLOBAL_ENER_FLOAT_PRECISION + if req_item.high_prec + else GLOBAL_NP_FLOAT_PRECISION + ) + if req_key not in frame: frame[f"find_{req_key}"] = np.float32(0.0) - # Support both dict and DataRequirementItem object - if isinstance(req_item, dict): - ndof = req_item["ndof"] - default = req_item["default"] - atomic = req_item["atomic"] - req_dtype = req_item.get("dtype") - if req_dtype is None: - req_dtype = ( - GLOBAL_ENER_FLOAT_PRECISION - if req_item.get("high_prec", False) - else GLOBAL_NP_FLOAT_PRECISION - ) - else: - ndof = req_item.ndof - default = req_item.default - atomic = req_item.atomic - req_dtype = req_item.dtype - if req_dtype is None: - req_dtype = ( - GLOBAL_ENER_FLOAT_PRECISION - if req_item.high_prec - else GLOBAL_NP_FLOAT_PRECISION - ) if atomic: shape = (frame_natoms, ndof) else: shape = (ndof,) - frame[req_key] = np.full(shape, default, dtype=req_dtype) - elif f"find_{req_key}" not in frame: - frame[f"find_{req_key}"] = np.float32(1.0) + data = np.full(shape, default, dtype=req_dtype) + if repeat != 1: + data = np.repeat(data, repeat).reshape(-1) + frame[req_key] = data + else: + if f"find_{req_key}" not in frame: + frame[f"find_{req_key}"] = np.float32(1.0) + # Apply repeat to existing data (e.g. atom_pref repeat=3) + if repeat != 1 and isinstance(frame[req_key], np.ndarray): + frame[req_key] = ( + np.repeat(frame[req_key], repeat).reshape(-1).astype(req_dtype) + ) # Add find_* for fparam/aparam/spin if not already set for extra_key in ["fparam", "aparam", "spin"]: @@ -1268,22 +1289,17 @@ def _stack_frames( np.stack(atypes) if atypes else np.zeros((0, natoms), dtype=np.int64) ) - # Label keys and registered requirements + # Dynamically discover all data keys present in frames, plus + # any registered requirements. Structural keys (coord, box, type) + # are excluded — they are already handled above. + _structural_keys = frozenset({"coord", "box", "atype"}) all_keys: dict[str, dict[str, Any]] = {} - for key in [ - "energy", - "force", - "virial", - "atom_ener", - "atom_pref", - "force_mag", - "spin", - "fparam", - "aparam", - "hessian", - "efield", - ]: - all_keys[key] = {"ndof": None, "atomic": False, "default": 0.0} + for f in frames: + for fk in f: + if fk in _structural_keys or fk.startswith("find_"): + continue + if fk not in all_keys: + all_keys[fk] = {"ndof": None, "atomic": False, "default": 0.0} for key, req in self._requirements.items(): all_keys[key] = req @@ -1293,12 +1309,20 @@ def _stack_frames( ) result[f"find_{key}"] = 1.0 if has_key else 0.0 + # Get repeat factor from registered requirements + repeat = 1 + if key in self._requirements: + repeat = self._requirements[key].get("repeat", 1) + if has_key: arrays = [] for frame in frames: val = frame.get(key) if isinstance(val, np.ndarray): - arrays.append(val.astype(self._resolve_dtype(key)).ravel()) + arr = val.astype(self._resolve_dtype(key)).ravel() + if repeat != 1: + arr = np.repeat(arr, repeat) + arrays.append(arr) elif val is not None: arrays.append( np.array([float(val)], dtype=self._resolve_dtype(key)) @@ -1313,8 +1337,9 @@ def _stack_frames( None, ) if ref is not None: + size = ref.size * repeat if repeat != 1 else ref.size arrays.append( - np.zeros(ref.size, dtype=self._resolve_dtype(key)) + np.zeros(size, dtype=self._resolve_dtype(key)) ) else: arrays.append(np.zeros(1, dtype=self._resolve_dtype(key))) @@ -1324,9 +1349,9 @@ def _stack_frames( atomic = self._requirements[key]["atomic"] default = self._requirements[key]["default"] if atomic: - shape = (nframes, natoms * ndof) + shape = (nframes, natoms * ndof * repeat) else: - shape = (nframes, ndof) + shape = (nframes, ndof * repeat) result[key] = np.full(shape, default, dtype=self._resolve_dtype(key)) return result diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index fd7b4b2439..20704ba80b 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -1,5 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""Unit tests for LmdbDataset.""" +"""Unit tests for LmdbDataset (PyTorch wrapper) and related PT-specific features. + +Pure dpmodel tests (LmdbDataReader, LmdbTestData, SameNlocBatchSampler, type_map +remapping, auto_prob) live in source/tests/common/dpmodel/test_lmdb_data.py. +Consistency tests (dpmodel vs pt) live in source/tests/consistent/test_lmdb_data.py. +""" import lmdb import msgpack @@ -10,12 +15,10 @@ from deepmd.dpmodel.utils.lmdb_data import ( DistributedSameNlocBatchSampler, LmdbDataReader, - LmdbTestData, SameNlocBatchSampler, _decode_frame, _read_metadata, _remap_keys, - is_lmdb, merge_lmdb, ) from deepmd.pt.utils.lmdb_dataset import ( @@ -84,15 +87,19 @@ def lmdb_dir(tmp_path): return lmdb_path +# ============================================================ +# Internal helper functions +# ============================================================ + + class TestHelpers: - """Test helper functions.""" + """Test internal helper functions (dpmodel, but only tested here).""" def test_read_metadata(self, lmdb_dir): env = lmdb.open(lmdb_dir, readonly=True, lock=False) with env.begin() as txn: meta = _read_metadata(txn) assert meta["nframes"] == 10 - assert "system_info" in meta env.close() def test_read_metadata_missing(self, tmp_path): @@ -111,7 +118,6 @@ def test_decode_frame(self, lmdb_dir): raw = txn.get(format(0, "012d").encode()) frame = _decode_frame(raw) assert "coords" in frame - assert "forces" in frame assert isinstance(frame["coords"], np.ndarray) assert frame["coords"].shape == (6, 3) env.close() @@ -131,15 +137,13 @@ def test_remap_keys(self): assert "energy" in remapped assert "force" in remapped assert "atype" in remapped - assert "custom_key" in remapped # pass-through + assert "custom_key" in remapped assert "coords" not in remapped - def test_is_lmdb(self, lmdb_dir, tmp_path): - assert is_lmdb(lmdb_dir) is True - assert is_lmdb(str(tmp_path / "nonexistent.lmdb")) is True # ends with .lmdb - assert is_lmdb(str(tmp_path / "nope")) is False - assert is_lmdb(["a", "b"]) is False - assert is_lmdb(42) is False + +# ============================================================ +# LmdbDataset (PT wrapper) +# ============================================================ class TestLmdbDataset: @@ -152,24 +156,13 @@ def test_len(self, lmdb_dir): def test_getitem_keys(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) frame = ds[0] - # Required keys - assert "coord" in frame - assert "box" in frame - assert "energy" in frame - assert "force" in frame - assert "atype" in frame - assert "natoms" in frame - assert "fid" in frame - # find_* flags - assert "find_energy" in frame - assert "find_force" in frame + for key in ("coord", "box", "energy", "force", "atype", "natoms", "fid"): + assert key in frame assert frame["find_energy"] == 1.0 assert frame["find_force"] == 1.0 - assert frame["find_virial"] == 0.0 # Metadata keys removed - assert "atom_numbs" not in frame - assert "atom_names" not in frame - assert "orig" not in frame + for key in ("atom_numbs", "atom_names", "orig"): + assert key not in frame def test_getitem_shapes(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) @@ -179,15 +172,12 @@ def test_getitem_shapes(self, lmdb_dir): assert frame["energy"].shape == (1,) assert frame["force"].shape == (6, 3) assert frame["atype"].shape == (6,) - assert frame["natoms"].shape == (4,) # [natoms, natoms, nO, nH] + assert frame["natoms"].shape == (4,) def test_getitem_dtypes(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) frame = ds[0] assert frame["coord"].dtype == np.float64 - assert frame["box"].dtype == np.float64 - assert frame["energy"].dtype == np.float64 - assert frame["force"].dtype == np.float64 assert frame["atype"].dtype == np.int64 def test_getitem_out_of_range(self, lmdb_dir): @@ -197,27 +187,32 @@ def test_getitem_out_of_range(self, lmdb_dir): def test_natoms_vec(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - frame = ds[0] - natoms = frame["natoms"] - assert natoms[0] == 6 # total natoms - assert natoms[1] == 6 # total natoms (repeated) + natoms = ds[0]["natoms"] + assert natoms[0] == 6 assert natoms[2] == 3 # O count assert natoms[3] == 3 # H count def test_auto_batch_size(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size="auto") - # rule=32, natoms=6, 32//6=5, 5*6=30<32 → 6 assert ds.batch_size == 6 def test_auto_batch_size_with_rule(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size="auto:12") - # rule=12, natoms=6, 12//6=2, 2*6=12 → 2 assert ds.batch_size == 2 def test_int_batch_size(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=3) assert ds.batch_size == 3 + def test_mixed_type(self, lmdb_dir): + ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) + assert ds.mixed_type is True + + +# ============================================================ +# Trainer compatibility interface +# ============================================================ + class TestTrainerInterface: """Test Trainer compatibility interface.""" @@ -233,7 +228,7 @@ def test_dataloaders(self, lmdb_dir): def test_index(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - assert ds.index == [5] # 10 frames / 2 batch_size + assert ds.index == [5] def test_total_batch(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) @@ -249,31 +244,30 @@ def test_sampler_list(self, lmdb_dir): def test_add_data_requirement(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - req = [ - DataRequirementItem("virial", 9, atomic=False, must=False, default=0.0), - ] + req = [DataRequirementItem("virial", 9, atomic=False, must=False, default=0.0)] ds.add_data_requirement(req) frame = ds[0] assert frame["find_virial"] == 0.0 assert frame["virial"].shape == (9,) - assert np.allclose(frame["virial"], 0.0) def test_add_data_requirement_existing_key(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - req = [ - DataRequirementItem("energy", 1, atomic=False, must=True), - ] + req = [DataRequirementItem("energy", 1, atomic=False, must=True)] ds.add_data_requirement(req) - frame = ds[0] - assert frame["find_energy"] == 1.0 + assert ds[0]["find_energy"] == 1.0 def test_preload_noop(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - ds.preload_and_modify_all_data_torch() # should not raise + ds.preload_and_modify_all_data_torch() def test_set_noise_noop(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - ds.set_noise({}) # should not raise + ds.set_noise({}) + + +# ============================================================ +# DataLoader iteration +# ============================================================ class TestDataLoaderIteration: @@ -287,29 +281,19 @@ def test_batch_iteration(self, lmdb_dir): with torch.device("cpu"): dl = DataLoader( - ds, - batch_size=2, - shuffle=False, - collate_fn=_collate_lmdb_batch, + ds, batch_size=2, shuffle=False, collate_fn=_collate_lmdb_batch ) batch = next(iter(dl)) - assert "coord" in batch - assert "sid" in batch - assert batch["sid"] == 0 assert batch["coord"].shape == (2, 6, 3) assert batch["energy"].shape == (2, 1) - assert batch["force"].shape == (2, 6, 3) assert batch["atype"].shape == (2, 6) assert isinstance(batch["fid"], list) - assert len(batch["fid"]) == 2 - assert isinstance(batch["find_energy"], (float, np.floating)) + assert batch["sid"] == 0 def test_inner_dataloader(self, lmdb_dir): ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - dl = ds.dataloaders[0] with torch.device("cpu"): - batch = next(iter(dl)) - assert "coord" in batch + batch = next(iter(ds.dataloaders[0])) assert batch["coord"].shape[0] == 2 def test_full_epoch(self, lmdb_dir): @@ -320,18 +304,17 @@ def test_full_epoch(self, lmdb_dir): with torch.device("cpu"): dl = DataLoader( - ds, - batch_size=3, - shuffle=False, - collate_fn=_collate_lmdb_batch, + ds, batch_size=3, shuffle=False, collate_fn=_collate_lmdb_batch ) - total_frames = 0 - for batch in dl: - total_frames += batch["coord"].shape[0] - # 10 frames, batch_size=3 → 3+3+3+1 = 10 + total_frames = sum(batch["coord"].shape[0] for batch in dl) assert total_frames == 10 +# ============================================================ +# Collate function +# ============================================================ + + class TestCollate: """Test collate function.""" @@ -353,8 +336,6 @@ def test_collate_basic(self): ] batch = _collate_lmdb_batch(frames) assert batch["coord"].shape == (2, 4, 3) - assert batch["energy"].shape == (2, 1) - assert batch["find_energy"] == 1.0 assert batch["fid"] == [0, 1] assert batch["sid"] == 0 @@ -363,109 +344,19 @@ def test_collate_skips_type(self): {"coord": np.zeros((2, 3)), "type": np.array([0, 1])}, {"coord": np.zeros((2, 3)), "type": np.array([0, 1])}, ] - batch = _collate_lmdb_batch(frames) - assert "type" not in batch + assert "type" not in _collate_lmdb_batch(frames) def test_collate_none_values(self): frames = [ {"coord": np.zeros((2, 3)), "box": None}, {"coord": np.zeros((2, 3)), "box": None}, ] - batch = _collate_lmdb_batch(frames) - assert batch["box"] is None - - -class TestLmdbTestData: - """Test LmdbTestData for dp test support.""" - - def test_get_test_keys(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - td.add("virial", 9, atomic=False, must=False, high_prec=False) - result = td.get_test() - assert "coord" in result - assert "box" in result - assert "type" in result - assert "energy" in result - assert "force" in result - assert "find_energy" in result - assert "find_force" in result - assert "find_virial" in result - - def test_get_test_shapes(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - td.add("virial", 9, atomic=False, must=False, high_prec=False) - result = td.get_test() - nframes = 10 - natoms = 6 - assert result["coord"].shape == (nframes, natoms * 3) - assert result["box"].shape == (nframes, 9) - assert result["type"].shape == (nframes, natoms) - assert result["energy"].shape == (nframes, 1) - assert result["force"].shape == (nframes, natoms * 3) - - def test_get_test_dtypes(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - result = td.get_test() - assert result["coord"].dtype == np.float64 - assert result["box"].dtype == np.float64 - assert result["type"].dtype == np.int64 - assert result["energy"].dtype == np.float64 - assert result["force"].dtype == np.float64 - - def test_get_test_find_flags(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("energy", 1, atomic=False, must=False, high_prec=True) - td.add("force", 3, atomic=True, must=False, high_prec=False) - td.add("virial", 9, atomic=False, must=False, high_prec=False) - result = td.get_test() - assert result["find_energy"] == 1.0 - assert result["find_force"] == 1.0 - assert result["find_virial"] == 0.0 # not in test LMDB data - - def test_get_test_missing_key_default(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("virial", 9, atomic=False, must=False, high_prec=False, default=0.0) - result = td.get_test() - assert result["find_virial"] == 0.0 - assert result["virial"].shape == (10, 9) - assert np.allclose(result["virial"], 0.0) - - def test_get_test_missing_atomic_key(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td.add("atom_ener", 1, atomic=True, must=False, high_prec=False, default=0.0) - result = td.get_test() - assert result["find_atom_ener"] == 0.0 - assert result["atom_ener"].shape == (10, 6) # natoms=6 - assert np.allclose(result["atom_ener"], 0.0) - - def test_pbc(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - assert td.pbc is True + assert _collate_lmdb_batch(frames)["box"] is None - def test_mixed_type(self, lmdb_dir): - td = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - assert td.mixed_type is True - - def test_shuffle(self, lmdb_dir): - td1 = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - td2 = LmdbTestData(lmdb_dir, type_map=["O", "H"], shuffle_test=False) - r1 = td1.get_test() - r2 = td2.get_test() - # Without shuffle, results should be identical - np.testing.assert_array_equal(r1["coord"], r2["coord"]) - def test_type_map_global(self, lmdb_dir): - """Test with a larger global type_map than LMDB data.""" - td = LmdbTestData(lmdb_dir, type_map=["O", "H", "C"], shuffle_test=False) - result = td.get_test() - # type indices should still be 0 and 1 - assert result["type"].max() <= 1 +# ============================================================ +# Type map remapping (PT-specific: LmdbDataset) +# ============================================================ def _create_test_lmdb_with_type_map( @@ -480,26 +371,22 @@ def _create_test_lmdb_with_type_map( metadata = { "nframes": nframes, "frame_idx_fmt": fmt, - "system_info": { - "formula": f"O{natoms // 2}H{natoms // 2}", - "natoms": [natoms // 2, natoms // 2], - "nframes": nframes, - }, + "system_info": {"natoms": [natoms // 2, natoms // 2]}, } if lmdb_type_map is not None: metadata["type_map"] = lmdb_type_map with env.begin(write=True) as txn: txn.put(b"__metadata__", msgpack.packb(metadata, use_bin_type=True)) for i in range(nframes): - key = format(i, fmt).encode() - frame = _make_frame(natoms=natoms, seed=i) - txn.put(key, msgpack.packb(frame, use_bin_type=True)) + txn.put( + format(i, fmt).encode(), + msgpack.packb(_make_frame(natoms=natoms, seed=i), use_bin_type=True), + ) env.close() @pytest.fixture def lmdb_with_type_map(tmp_path): - """Create LMDB with type_map=["O","H"] in metadata.""" lmdb_path = str(tmp_path / "typed.lmdb") _create_test_lmdb_with_type_map( lmdb_path, nframes=10, natoms=6, lmdb_type_map=["O", "H"] @@ -507,170 +394,72 @@ def lmdb_with_type_map(tmp_path): return lmdb_path -@pytest.fixture -def lmdb_no_type_map(tmp_path): - """Create LMDB without type_map in metadata (old format).""" - lmdb_path = str(tmp_path / "old.lmdb") - _create_test_lmdb_with_type_map(lmdb_path, nframes=6, natoms=6, lmdb_type_map=None) - return lmdb_path - - -class TestTypeMapRemapping: - """Test type_map remapping in LmdbDataReader, LmdbDataset, and LmdbTestData.""" - - # --- LmdbDataReader --- - - def test_reader_no_remap_when_match(self, lmdb_with_type_map): - reader = LmdbDataReader(lmdb_with_type_map, ["O", "H"]) - assert reader._type_remap is None - frame = reader[0] - assert frame["atype"][0] == 0 # type-0 stays 0 - - def test_reader_remap_reversed(self, lmdb_with_type_map): - reader = LmdbDataReader(lmdb_with_type_map, ["H", "O"]) - assert reader._type_remap is not None - np.testing.assert_array_equal(reader._type_remap, [1, 0]) - frame = reader[0] - # Original: first 3 atoms type-0 (O), last 3 type-1 (H) - # After remap: O->1, H->0 - np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) - np.testing.assert_array_equal(frame["atype"][3:], [0, 0, 0]) - - def test_reader_remap_superset(self, lmdb_with_type_map): - reader = LmdbDataReader(lmdb_with_type_map, ["C", "O", "H"]) - assert reader._type_remap is not None - np.testing.assert_array_equal(reader._type_remap, [1, 2]) - frame = reader[0] - np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) - np.testing.assert_array_equal(frame["atype"][3:], [2, 2, 2]) - - def test_reader_natoms_vec_after_remap(self, lmdb_with_type_map): - reader = LmdbDataReader(lmdb_with_type_map, ["H", "O"]) - frame = reader[0] - natoms = frame["natoms"] - assert natoms[0] == 6 # nloc - assert natoms[2] == 3 # H count (model idx 0) - assert natoms[3] == 3 # O count (model idx 1) - - def test_reader_missing_element_raises(self, lmdb_with_type_map): - with pytest.raises(ValueError, match="not found in model type_map"): - LmdbDataReader(lmdb_with_type_map, ["O"]) - - def test_reader_no_remap_old_format(self, lmdb_no_type_map): - reader = LmdbDataReader(lmdb_no_type_map, ["H", "O"]) - assert reader._type_remap is None - - # --- LmdbDataset --- +class TestTypeMapRemappingDataset: + """Test type_map remapping in LmdbDataset (PT-specific).""" def test_dataset_remap_reversed(self, lmdb_with_type_map): ds = LmdbDataset(lmdb_with_type_map, type_map=["H", "O"], batch_size=2) frame = ds[0] - # O->1, H->0 np.testing.assert_array_equal(frame["atype"][:3], [1, 1, 1]) np.testing.assert_array_equal(frame["atype"][3:], [0, 0, 0]) def test_dataset_remap_batch(self, lmdb_with_type_map): ds = LmdbDataset(lmdb_with_type_map, type_map=["H", "O"], batch_size=2) with torch.device("cpu"): - dl = ds.dataloaders[0] - batch = next(iter(dl)) - # All frames in batch should have remapped types - atype = batch["atype"] - for i in range(atype.shape[0]): - np.testing.assert_array_equal(atype[i, :3].numpy(), [1, 1, 1]) - np.testing.assert_array_equal(atype[i, 3:].numpy(), [0, 0, 0]) + batch = next(iter(ds.dataloaders[0])) + for i in range(batch["atype"].shape[0]): + np.testing.assert_array_equal(batch["atype"][i, :3].numpy(), [1, 1, 1]) + np.testing.assert_array_equal(batch["atype"][i, 3:].numpy(), [0, 0, 0]) def test_dataset_no_remap_when_match(self, lmdb_with_type_map): ds = LmdbDataset(lmdb_with_type_map, type_map=["O", "H"], batch_size=2) - frame = ds[0] - np.testing.assert_array_equal(frame["atype"][:3], [0, 0, 0]) - np.testing.assert_array_equal(frame["atype"][3:], [1, 1, 1]) - - # --- LmdbTestData --- - - def test_testdata_no_remap_when_match(self, lmdb_with_type_map): - td = LmdbTestData(lmdb_with_type_map, type_map=["O", "H"], shuffle_test=False) - assert td._type_remap is None - data = td.get_test() - assert data["type"][0, 0] == 0 - - def test_testdata_remap_reversed(self, lmdb_with_type_map): - td = LmdbTestData(lmdb_with_type_map, type_map=["H", "O"], shuffle_test=False) - assert td._type_remap is not None - data = td.get_test() - # O->1, H->0 - np.testing.assert_array_equal(data["type"][0, :3], [1, 1, 1]) - np.testing.assert_array_equal(data["type"][0, 3:], [0, 0, 0]) - - def test_testdata_remap_superset(self, lmdb_with_type_map): - td = LmdbTestData( - lmdb_with_type_map, type_map=["C", "O", "H"], shuffle_test=False - ) - assert td._type_remap is not None - data = td.get_test() - # O->1, H->2 - np.testing.assert_array_equal(data["type"][0, :3], [1, 1, 1]) - np.testing.assert_array_equal(data["type"][0, 3:], [2, 2, 2]) + np.testing.assert_array_equal(ds[0]["atype"][:3], [0, 0, 0]) - def test_testdata_missing_element_raises(self, lmdb_with_type_map): - with pytest.raises(ValueError, match="not found in model type_map"): - LmdbTestData(lmdb_with_type_map, type_map=["O"], shuffle_test=False) - def test_testdata_no_remap_old_format(self, lmdb_no_type_map): - td = LmdbTestData(lmdb_no_type_map, type_map=["H", "O"], shuffle_test=False) - assert td._type_remap is None +# ============================================================ +# Distributed sampler +# ============================================================ def _create_multi_nloc_lmdb(path: str) -> None: """Create an LMDB with frames of varying nloc for distributed tests.""" env = lmdb.open(path, map_size=10 * 1024 * 1024) fmt = "012d" - # 30 frames: 10 with nloc=4, 10 with nloc=6, 10 with nloc=8 nframes = 30 frame_nlocs = [] with env.begin(write=True) as txn: idx = 0 for natoms in [4, 6, 8]: for i in range(10): - key = format(idx, fmt).encode() - frame = _make_frame(natoms=natoms, seed=idx * 100) - txn.put(key, msgpack.packb(frame, use_bin_type=True)) + txn.put( + format(idx, fmt).encode(), + msgpack.packb( + _make_frame(natoms=natoms, seed=idx * 100), use_bin_type=True + ), + ) frame_nlocs.append(natoms) idx += 1 - metadata = { - "nframes": nframes, - "frame_idx_fmt": fmt, - "frame_nlocs": frame_nlocs, - } - txn.put(b"__metadata__", msgpack.packb(metadata, use_bin_type=True)) + txn.put( + b"__metadata__", + msgpack.packb( + {"nframes": nframes, "frame_idx_fmt": fmt, "frame_nlocs": frame_nlocs}, + use_bin_type=True, + ), + ) env.close() @pytest.fixture def multi_nloc_lmdb(tmp_path): - """Create LMDB with multiple nloc groups for distributed tests.""" lmdb_path = str(tmp_path / "multi_nloc.lmdb") _create_multi_nloc_lmdb(lmdb_path) return lmdb_path -class TestMixedTypeProperty: - """Test mixed_type property on LMDB classes.""" - - def test_lmdb_data_reader_mixed_type(self, lmdb_dir): - reader = LmdbDataReader(lmdb_dir, type_map=["O", "H"], batch_size=2) - assert reader.mixed_type is True - - def test_lmdb_dataset_mixed_type(self, lmdb_dir): - ds = LmdbDataset(lmdb_dir, type_map=["O", "H"], batch_size=2) - assert ds.mixed_type is True - - class TestDistributedSameNlocBatchSampler: """Test DistributedSameNlocBatchSampler (pure logic, no torch.distributed).""" def test_disjoint_batches(self, multi_nloc_lmdb): - """Two ranks produce disjoint frame index sets.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) s0 = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 @@ -678,17 +467,11 @@ def test_disjoint_batches(self, multi_nloc_lmdb): s1 = DistributedSameNlocBatchSampler( reader, rank=1, world_size=2, shuffle=True, seed=42 ) - frames0 = set() - for batch in s0: - frames0.update(batch) - frames1 = set() - for batch in s1: - frames1.update(batch) - # No overlap + frames0 = {i for batch in s0 for i in batch} + frames1 = {i for batch in s1 for i in batch} assert frames0 & frames1 == set() def test_covers_all_frames(self, multi_nloc_lmdb): - """Union of all ranks covers all frames.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) s0 = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 @@ -696,27 +479,22 @@ def test_covers_all_frames(self, multi_nloc_lmdb): s1 = DistributedSameNlocBatchSampler( reader, rank=1, world_size=2, shuffle=True, seed=42 ) - all_frames = set() - for batch in s0: - all_frames.update(batch) - for batch in s1: - all_frames.update(batch) + all_frames = {i for batch in s0 for i in batch} | { + i for batch in s1 for i in batch + } assert all_frames == set(range(30)) def test_len(self, multi_nloc_lmdb): - """__len__ returns approximately total // world_size.""" + import math + reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) - single = SameNlocBatchSampler(reader, shuffle=False) - total = len(single) + total = len(SameNlocBatchSampler(reader, shuffle=False)) dist_s = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=False, seed=0 ) - import math - assert len(dist_s) == math.ceil(total / 2) def test_deterministic(self, multi_nloc_lmdb): - """Same parameters produce same batch sequence.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) s1 = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 @@ -724,79 +502,47 @@ def test_deterministic(self, multi_nloc_lmdb): s2 = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 ) - batches1 = list(s1) - batches2 = list(s2) - assert batches1 == batches2 + assert list(s1) == list(s2) def test_set_epoch_changes_order(self, multi_nloc_lmdb): - """Different epochs produce different batch orderings.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) s = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 ) s.set_epoch(0) - batches_e0 = list(s) + e0 = list(s) s.set_epoch(1) - batches_e1 = list(s) - # Should produce different orderings - assert batches_e0 != batches_e1 + e1 = list(s) + assert e0 != e1 def test_single_gpu_fallback(self, multi_nloc_lmdb): - """world_size=1 produces same frames as SameNlocBatchSampler.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) - single = SameNlocBatchSampler(reader, shuffle=True, seed=42) - dist_single = DistributedSameNlocBatchSampler( - reader, rank=0, world_size=1, shuffle=True, seed=42 - ) - frames_single = set() - for batch in single: - frames_single.update(batch) - frames_dist = set() - for batch in dist_single: - frames_dist.update(batch) - # Both should cover all frames - assert frames_single == frames_dist == set(range(30)) - - def test_partition_batches_overridable(self, multi_nloc_lmdb): - """Subclass can override _partition_batches for custom load balancing.""" - reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) - - class ReversePartition(DistributedSameNlocBatchSampler): - def _partition_batches(self, all_batches): - # Take the complementary slice - return all_batches[ - self._world_size - 1 - self._rank :: self._world_size - ] - - s_default = DistributedSameNlocBatchSampler( - reader, rank=0, world_size=2, shuffle=True, seed=42 - ) - s_custom = ReversePartition(reader, rank=0, world_size=2, shuffle=True, seed=42) - # Custom should get rank=1's batches (since it reverses) - s_rank1 = DistributedSameNlocBatchSampler( - reader, rank=1, world_size=2, shuffle=True, seed=42 - ) - frames_custom = set() - for batch in s_custom: - frames_custom.update(batch) - frames_rank1 = set() - for batch in s_rank1: - frames_rank1.update(batch) - assert frames_custom == frames_rank1 + single = { + i + for batch in SameNlocBatchSampler(reader, shuffle=True, seed=42) + for i in batch + } + dist = { + i + for batch in DistributedSameNlocBatchSampler( + reader, rank=0, world_size=1, shuffle=True, seed=42 + ) + for i in batch + } + assert single == dist == set(range(30)) def test_same_nloc_per_batch(self, multi_nloc_lmdb): - """Each batch from distributed sampler has frames with same nloc.""" reader = LmdbDataReader(multi_nloc_lmdb, type_map=["O", "H"], batch_size=2) s = DistributedSameNlocBatchSampler( reader, rank=0, world_size=2, shuffle=True, seed=42 ) for batch in s: - nlocs = [reader.frame_nlocs[idx] for idx in batch] - assert len(set(nlocs)) == 1, f"Mixed nloc in batch: {nlocs}" + nlocs = {reader.frame_nlocs[idx] for idx in batch} + assert len(nlocs) == 1 # ============================================================ -# auto_prob / merge_lmdb tests +# auto_prob / merge_lmdb (PT-specific: LmdbDataset integration) # ============================================================ @@ -806,21 +552,17 @@ def _create_lmdb_with_system_ids( natoms: int = 6, type_map: list[str] | None = None, ) -> str: - """Create a test LMDB with frame_system_ids in metadata.""" total = sum(system_frames) frame_system_ids = [] for sid, nf in enumerate(system_frames): frame_system_ids.extend([sid] * nf) - env = lmdb.open(path, map_size=50 * 1024 * 1024) fmt = "012d" with env.begin(write=True) as txn: meta = { "nframes": total, "frame_idx_fmt": fmt, - "system_info": { - "natoms": [natoms // 2, natoms // 2], - }, + "system_info": {"natoms": [natoms // 2, natoms // 2]}, "frame_system_ids": frame_system_ids, "frame_nlocs": [natoms] * total, } @@ -828,22 +570,21 @@ def _create_lmdb_with_system_ids( meta["type_map"] = type_map txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) for i in range(total): - key = format(i, fmt).encode() - frame = _make_frame(natoms=natoms, seed=i % 100) - txn.put(key, msgpack.packb(frame, use_bin_type=True)) + txn.put( + format(i, fmt).encode(), + msgpack.packb( + _make_frame(natoms=natoms, seed=i % 100), use_bin_type=True + ), + ) env.close() return path @pytest.fixture def auto_prob_lmdb(tmp_path): - """LMDB with 3 systems: 50, 100, 150 frames.""" path = str(tmp_path / "auto_prob.lmdb") _create_lmdb_with_system_ids( - path, - system_frames=[50, 100, 150], - natoms=6, - type_map=["O", "H"], + path, system_frames=[50, 100, 150], natoms=6, type_map=["O", "H"] ) return path @@ -852,7 +593,6 @@ class TestAutoProbDataset: """Test LmdbDataset with auto_prob_style.""" def test_dataset_auto_prob_passthrough(self, auto_prob_lmdb): - """LmdbDataset passes auto_prob_style to sampler.""" ds = LmdbDataset( auto_prob_lmdb, type_map=["O", "H"], @@ -860,114 +600,74 @@ def test_dataset_auto_prob_passthrough(self, auto_prob_lmdb): auto_prob_style="prob_sys_size;0:1:0.5;1:3:0.5", ) assert ds._block_targets is not None - assert len(ds._block_targets) > 0 def test_dataset_auto_prob_none(self, auto_prob_lmdb): - """LmdbDataset without auto_prob_style: no block_targets.""" - ds = LmdbDataset( - auto_prob_lmdb, - type_map=["O", "H"], - batch_size=4, - ) + ds = LmdbDataset(auto_prob_lmdb, type_map=["O", "H"], batch_size=4) assert ds._block_targets is None def test_dataset_auto_prob_no_system_ids(self, lmdb_dir): - """LmdbDataset with auto_prob but old LMDB (no system_ids): no crash.""" ds = LmdbDataset( lmdb_dir, type_map=["O", "H"], batch_size=4, auto_prob_style="prob_sys_size;0:1:1.0", ) - # No system_ids -> block_targets stays None assert ds._block_targets is None def test_dataset_auto_prob_iteration(self, auto_prob_lmdb): - """LmdbDataset with auto_prob can iterate and produce batches.""" ds = LmdbDataset( auto_prob_lmdb, type_map=["O", "H"], batch_size=4, auto_prob_style="prob_sys_size;0:1:0.5;1:3:0.5", ) - count = 0 - for batch in ds._batch_sampler: - assert len(batch) > 0 - count += len(batch) - # Should have more frames than original 300 due to expansion - assert count > 300 + count = sum(len(batch) for batch in ds._batch_sampler) + assert count > 300 # expanded class TestMergeLmdbSystemIds: """Test merge_lmdb propagates frame_system_ids.""" def test_merge_propagates_system_ids(self, tmp_path): - """Merged LMDB has correct frame_system_ids with offsets.""" - src1 = str(tmp_path / "src1.lmdb") - src2 = str(tmp_path / "src2.lmdb") + src1, src2 = str(tmp_path / "src1.lmdb"), str(tmp_path / "src2.lmdb") _create_lmdb_with_system_ids( - src1, - system_frames=[5, 10], - natoms=6, - type_map=["O", "H"], + src1, system_frames=[5, 10], natoms=6, type_map=["O", "H"] ) _create_lmdb_with_system_ids( - src2, - system_frames=[3, 7], - natoms=6, - type_map=["O", "H"], + src2, system_frames=[3, 7], natoms=6, type_map=["O", "H"] ) dst = str(tmp_path / "merged.lmdb") merge_lmdb([src1, src2], dst) - reader = LmdbDataReader(dst, ["O", "H"]) - assert reader.nframes == 25 # 15 + 10 - assert reader.nsystems == 4 # 2 from src1 + 2 from src2 - sids = reader.frame_system_ids - assert sids is not None - # src1: sys0(5 frames) -> 0, sys1(10 frames) -> 1 - # src2: sys0(3 frames) -> 2, sys1(7 frames) -> 3 + assert reader.nframes == 25 + assert reader.nsystems == 4 + sids = list(reader.frame_system_ids) assert sids[:5] == [0] * 5 assert sids[5:15] == [1] * 10 assert sids[15:18] == [2] * 3 assert sids[18:25] == [3] * 7 def test_merge_old_lmdb_no_system_ids(self, tmp_path): - """Merging old LMDBs without system_ids: each source gets one sys id.""" - src1 = str(tmp_path / "old1.lmdb") - src2 = str(tmp_path / "old2.lmdb") + src1, src2 = str(tmp_path / "old1.lmdb"), str(tmp_path / "old2.lmdb") _create_test_lmdb(src1, nframes=5, natoms=6) _create_test_lmdb(src2, nframes=3, natoms=6) dst = str(tmp_path / "merged_old.lmdb") merge_lmdb([src1, src2], dst) - reader = LmdbDataReader(dst, ["O", "H"]) - assert reader.nframes == 8 assert reader.nsystems == 2 - sids = reader.frame_system_ids - assert sids is not None - assert sids[:5] == [0] * 5 - assert sids[5:8] == [1] * 3 + assert list(reader.frame_system_ids[:5]) == [0] * 5 + assert list(reader.frame_system_ids[5:8]) == [1] * 3 def test_merge_preserves_type_map(self, tmp_path): - """Merged LMDB preserves type_map from first source.""" - src1 = str(tmp_path / "tm1.lmdb") - src2 = str(tmp_path / "tm2.lmdb") + src1, src2 = str(tmp_path / "tm1.lmdb"), str(tmp_path / "tm2.lmdb") _create_lmdb_with_system_ids( - src1, - system_frames=[5], - natoms=6, - type_map=["O", "H"], + src1, system_frames=[5], natoms=6, type_map=["O", "H"] ) _create_lmdb_with_system_ids( - src2, - system_frames=[5], - natoms=6, - type_map=["O", "H"], + src2, system_frames=[5], natoms=6, type_map=["O", "H"] ) dst = str(tmp_path / "merged_tm.lmdb") merge_lmdb([src1, src2], dst) - env = lmdb.open(dst, readonly=True, lock=False) with env.begin() as txn: meta = _read_metadata(txn) @@ -976,30 +676,21 @@ def test_merge_preserves_type_map(self, tmp_path): # ============================================================ -# Multitask LMDB training tests +# Multitask LMDB training # ============================================================ @pytest.fixture def multitask_lmdb_setup(tmp_path): """Create two LMDB datasets and a multitask training config.""" - lmdb_train1 = str(tmp_path / "task1_train.lmdb") - lmdb_train2 = str(tmp_path / "task2_train.lmdb") - lmdb_val1 = str(tmp_path / "task1_val.lmdb") - lmdb_val2 = str(tmp_path / "task2_val.lmdb") - - _create_test_lmdb_with_type_map( - lmdb_train1, nframes=20, natoms=6, lmdb_type_map=["O", "H"] - ) - _create_test_lmdb_with_type_map( - lmdb_train2, nframes=20, natoms=6, lmdb_type_map=["O", "H"] - ) - _create_test_lmdb_with_type_map( - lmdb_val1, nframes=10, natoms=6, lmdb_type_map=["O", "H"] - ) - _create_test_lmdb_with_type_map( - lmdb_val2, nframes=10, natoms=6, lmdb_type_map=["O", "H"] - ) + for name in ("task1_train", "task2_train", "task1_val", "task2_val"): + nf = 20 if "train" in name else 10 + _create_test_lmdb_with_type_map( + str(tmp_path / f"{name}.lmdb"), + nframes=nf, + natoms=6, + lmdb_type_map=["O", "H"], + ) config = { "model": { @@ -1018,11 +709,7 @@ def multitask_lmdb_setup(tmp_path): "attn_mask": False, "precision": "float64", }, - "my_fitting": { - "neuron": [16, 16], - "precision": "float64", - "seed": 1, - }, + "my_fitting": {"neuron": [16, 16], "precision": "float64", "seed": 1}, }, "model_dict": { "model_1": { @@ -1071,22 +758,22 @@ def multitask_lmdb_setup(tmp_path): "model_1": { "stat_file": str(tmp_path / "stat_model_1.hdf5"), "training_data": { - "systems": lmdb_train1, + "systems": str(tmp_path / "task1_train.lmdb"), "batch_size": 4, }, "validation_data": { - "systems": lmdb_val1, + "systems": str(tmp_path / "task1_val.lmdb"), "batch_size": 2, }, }, "model_2": { "stat_file": str(tmp_path / "stat_model_2.hdf5"), "training_data": { - "systems": lmdb_train2, + "systems": str(tmp_path / "task2_train.lmdb"), "batch_size": 4, }, "validation_data": { - "systems": lmdb_val2, + "systems": str(tmp_path / "task2_val.lmdb"), "batch_size": 2, }, }, @@ -1105,7 +792,6 @@ class TestMultitaskLmdbTraining: """Test multitask training with LMDB datasets.""" def test_multitask_lmdb_trainer_init(self, multitask_lmdb_setup, monkeypatch): - """Trainer initializes without error for multitask LMDB.""" from copy import ( deepcopy, ) @@ -1125,17 +811,14 @@ def test_multitask_lmdb_trainer_init(self, multitask_lmdb_setup, monkeypatch): config, tmp_path = multitask_lmdb_setup monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) config["model"], shared_links = preprocess_shared_params(config["model"]) config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) assert trainer.multi_task assert set(trainer.model_keys) == {"model_1", "model_2"} def test_multitask_lmdb_training_runs(self, multitask_lmdb_setup, monkeypatch): - """Multitask LMDB training runs for a few steps without errors.""" from copy import ( deepcopy, ) @@ -1155,20 +838,14 @@ def test_multitask_lmdb_training_runs(self, multitask_lmdb_setup, monkeypatch): config, tmp_path = multitask_lmdb_setup monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) config["model"], shared_links = preprocess_shared_params(config["model"]) config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) trainer.run() - - # Verify checkpoint was saved - ckpt_files = list(tmp_path.glob("model.ckpt*.pt")) - assert len(ckpt_files) > 0, "No checkpoint file was saved" + assert len(list(tmp_path.glob("model.ckpt*.pt"))) > 0 def test_multitask_lmdb_get_data(self, multitask_lmdb_setup, monkeypatch): - """Verify get_data returns proper dicts for each task with LMDB.""" from copy import ( deepcopy, ) @@ -1188,24 +865,18 @@ def test_multitask_lmdb_get_data(self, multitask_lmdb_setup, monkeypatch): config, tmp_path = multitask_lmdb_setup monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) config["model"], shared_links = preprocess_shared_params(config["model"]) config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) - for task_key in ["model_1", "model_2"]: input_dict, label_dict, log_dict = trainer.get_data( is_train=True, task_key=task_key ) assert "coord" in input_dict - assert "atype" in input_dict - assert "energy" in label_dict or "find_energy" in label_dict assert "sid" in log_dict def test_multitask_lmdb_shared_params(self, multitask_lmdb_setup, monkeypatch): - """Shared descriptor params are identical across tasks after init.""" from copy import ( deepcopy, ) @@ -1225,16 +896,13 @@ def test_multitask_lmdb_shared_params(self, multitask_lmdb_setup, monkeypatch): config, tmp_path = multitask_lmdb_setup monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) config["model"], shared_links = preprocess_shared_params(config["model"]) config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) - state_dict = trainer.wrapper.model.state_dict() for key in state_dict: if "model_1.atomic_model.descriptor" in key: key2 = key.replace("model_1", "model_2") - assert key2 in state_dict, f"Missing {key2}" + assert key2 in state_dict torch.testing.assert_close(state_dict[key], state_dict[key2]) From 78e2d5fc3049aee5f4d402a7e78db67bb7d710cc Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:39:38 +0800 Subject: [PATCH 13/22] add ut for extra keys --- deepmd/dpmodel/utils/lmdb_data.py | 6 +- source/tests/common/dpmodel/test_lmdb_data.py | 154 ++++++++++++++++++ 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 7592611fd0..629ca797cb 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -1289,13 +1289,13 @@ def _stack_frames( np.stack(atypes) if atypes else np.zeros((0, natoms), dtype=np.int64) ) - # Dynamically discover all data keys present in frames, plus + # Dynamically discover all data keys from the first frame, plus # any registered requirements. Structural keys (coord, box, type) # are excluded — they are already handled above. _structural_keys = frozenset({"coord", "box", "atype"}) all_keys: dict[str, dict[str, Any]] = {} - for f in frames: - for fk in f: + if frames: + for fk in frames[0]: if fk in _structural_keys or fk.startswith("find_"): continue if fk not in all_keys: diff --git a/source/tests/common/dpmodel/test_lmdb_data.py b/source/tests/common/dpmodel/test_lmdb_data.py index 76d337156c..106c550177 100644 --- a/source/tests/common/dpmodel/test_lmdb_data.py +++ b/source/tests/common/dpmodel/test_lmdb_data.py @@ -709,5 +709,159 @@ def test_sampling_large_dataset(self): tmpdir.cleanup() +def _create_lmdb_with_extra_keys( + path: str, nframes: int = 5, natoms: int = 6, extra_keys: dict | None = None +) -> str: + """Create a test LMDB with extra per-frame keys (e.g. atom_pref, fparam). + + Parameters + ---------- + extra_keys : dict + key -> (shape_fn, dtype) where shape_fn(natoms) returns the array shape. + Example: {"atom_pref": (lambda n: (n,), np.float64)} + """ + n_type0 = max(1, natoms // 3) + n_type1 = natoms - n_type0 + extra_keys = extra_keys or {} + env = lmdb.open(path, map_size=10 * 1024 * 1024) + with env.begin(write=True) as txn: + meta = { + "nframes": nframes, + "frame_idx_fmt": "012d", + "type_map": ["O", "H"], + "system_info": {"natoms": [n_type0, n_type1]}, + } + txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) + rng = np.random.RandomState(0) + for i in range(nframes): + frame = _make_frame(natoms=natoms, seed=i) + for ek, (shape_fn, dtype) in extra_keys.items(): + arr = rng.rand(*shape_fn(natoms)).astype(dtype) + frame[ek] = { + "type": str(arr.dtype), + "shape": list(arr.shape), + "data": arr.tobytes(), + } + txn.put( + format(i, "012d").encode(), + msgpack.packb(frame, use_bin_type=True), + ) + env.close() + return path + + +# ============================================================ +# Dynamic find_* and repeat tests +# ============================================================ + + +class TestDynamicKeysAndRepeat(unittest.TestCase): + """Test auto-discovery of find_* flags and repeat handling.""" + + @classmethod + def setUpClass(cls): + cls._tmpdir = tempfile.TemporaryDirectory() + cls._natoms = 6 + cls._nframes = 5 + cls._lmdb_path = _create_lmdb_with_extra_keys( + f"{cls._tmpdir.name}/extra.lmdb", + nframes=cls._nframes, + natoms=cls._natoms, + extra_keys={ + "atom_pref": (lambda n: (n,), np.float64), + "fparam": (lambda n: (3,), np.float64), + }, + ) + cls._type_map = ["O", "H"] + + @classmethod + def tearDownClass(cls): + cls._tmpdir.cleanup() + + # --- LmdbDataReader --- + + def test_reader_find_flags_auto_detected(self): + """Extra keys in frame get find_*=1.0 automatically.""" + reader = LmdbDataReader(self._lmdb_path, self._type_map) + frame = reader[0] + self.assertEqual(frame["find_atom_pref"], np.float32(1.0)) + self.assertEqual(frame["find_fparam"], np.float32(1.0)) + self.assertEqual(frame["find_energy"], np.float32(1.0)) + # Keys not in frame get find_*=0.0 + self.assertEqual(frame["find_aparam"], np.float32(0.0)) + self.assertEqual(frame["find_spin"], np.float32(0.0)) + + def test_reader_repeat_applied(self): + """DataRequirementItem with repeat=3 expands atom_pref from (natoms,) to (natoms*3,).""" + from deepmd.utils.data import ( + DataRequirementItem, + ) + + reader = LmdbDataReader(self._lmdb_path, self._type_map) + reader.add_data_requirement( + [ + DataRequirementItem( + "atom_pref", + ndof=1, + atomic=True, + must=False, + high_prec=False, + repeat=3, + ), + ] + ) + frame = reader[0] + self.assertEqual(frame["atom_pref"].shape, (self._natoms * 3,)) + + def test_reader_repeat_default_fill(self): + """Missing key with repeat fills correct shape.""" + from deepmd.utils.data import ( + DataRequirementItem, + ) + + reader = LmdbDataReader(self._lmdb_path, self._type_map) + reader.add_data_requirement( + [ + DataRequirementItem( + "drdq", ndof=6, atomic=True, must=False, high_prec=False, repeat=2 + ), + ] + ) + frame = reader[0] + self.assertEqual(frame["find_drdq"], np.float32(0.0)) + self.assertEqual(frame["drdq"].shape, (self._natoms * 6 * 2,)) + + # --- LmdbTestData --- + + def test_testdata_find_flags_auto_detected(self): + """LmdbTestData.get_test() discovers extra keys dynamically.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + result = td.get_test() + self.assertEqual(result["find_atom_pref"], 1.0) + self.assertEqual(result["find_fparam"], 1.0) + self.assertIn("atom_pref", result) + self.assertIn("fparam", result) + + def test_testdata_repeat_applied(self): + """LmdbTestData respects repeat=3 for atom_pref.""" + td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) + td.add("atom_pref", 1, atomic=True, must=False, high_prec=False, repeat=3) + result = td.get_test() + self.assertEqual( + result["atom_pref"].shape, + (self._nframes, self._natoms * 3), + ) + + def test_testdata_missing_key_not_found(self): + """Keys absent from LMDB frames get find_*=0.0 in get_test().""" + tmpdir = tempfile.TemporaryDirectory() + path = _create_lmdb(f"{tmpdir.name}/plain.lmdb", nframes=3, natoms=6) + td = LmdbTestData(path, type_map=["O", "H"], shuffle_test=False) + result = td.get_test() + # atom_pref is not in the plain LMDB + self.assertEqual(result.get("find_atom_pref", 0.0), 0.0) + tmpdir.cleanup() + + if __name__ == "__main__": unittest.main() From 2a0f1aa7324b3d4caf00128faf7a1fc5f343c6b6 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:21:28 +0800 Subject: [PATCH 14/22] add example data --- .../input_lmdb.json | 0 .../water_training.lmdb/data.mdb | Bin 0 -> 688128 bytes .../water_training.lmdb/lock.mdb | Bin 0 -> 8192 bytes .../water_validation.lmdb/data.mdb | Bin 0 -> 196608 bytes .../water_validation.lmdb/lock.mdb | Bin 0 -> 8192 bytes source/tests/common/test_examples.py | 1 + 6 files changed, 1 insertion(+) rename examples/{lmdb_data => lmdb_downsample_data}/input_lmdb.json (100%) create mode 100644 examples/lmdb_downsample_data/water_training.lmdb/data.mdb create mode 100644 examples/lmdb_downsample_data/water_training.lmdb/lock.mdb create mode 100644 examples/lmdb_downsample_data/water_validation.lmdb/data.mdb create mode 100644 examples/lmdb_downsample_data/water_validation.lmdb/lock.mdb diff --git a/examples/lmdb_data/input_lmdb.json b/examples/lmdb_downsample_data/input_lmdb.json similarity index 100% rename from examples/lmdb_data/input_lmdb.json rename to examples/lmdb_downsample_data/input_lmdb.json diff --git a/examples/lmdb_downsample_data/water_training.lmdb/data.mdb b/examples/lmdb_downsample_data/water_training.lmdb/data.mdb new file mode 100644 index 0000000000000000000000000000000000000000..2b3ecd3bde377155525c19fd2556299c88c758bd GIT binary patch literal 688128 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

bij99bmH^x<>?ox?XW*#^Uqk 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⋘H8FtR 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

+nj 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�_x6`<+ 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 literal 0 HcmV?d00001 diff --git a/examples/lmdb_downsample_data/water_training.lmdb/lock.mdb b/examples/lmdb_downsample_data/water_training.lmdb/lock.mdb new file mode 100644 index 0000000000000000000000000000000000000000..37d3108c8b7584fd9544fa671297417cd6533a17 GIT binary patch literal 8192 zcmeIu!3}^Q3;;lQ7}s(ICvgNh($ literal 0 HcmV?d00001 diff --git a/examples/lmdb_downsample_data/water_validation.lmdb/data.mdb b/examples/lmdb_downsample_data/water_validation.lmdb/data.mdb new file mode 100644 index 0000000000000000000000000000000000000000..abc9f9cd17eca98559744328552413e812853328 GIT binary patch literal 196608 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 a-k~)^1cnF<5f~ycL|}-(5P|=D1pXgkk*gd4 literal 0 HcmV?d00001 diff --git a/examples/lmdb_downsample_data/water_validation.lmdb/lock.mdb b/examples/lmdb_downsample_data/water_validation.lmdb/lock.mdb new file mode 100644 index 0000000000000000000000000000000000000000..adbfe89e9b0726dff90905acc18fa312d01061f1 GIT binary patch literal 8192 zcmeIu!3}^Q3;;lQ7$ Date: Mon, 30 Mar 2026 19:22:04 +0800 Subject: [PATCH 15/22] Create README.md --- examples/lmdb_downsample_data/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/lmdb_downsample_data/README.md diff --git a/examples/lmdb_downsample_data/README.md b/examples/lmdb_downsample_data/README.md new file mode 100644 index 0000000000..e38013ee19 --- /dev/null +++ b/examples/lmdb_downsample_data/README.md @@ -0,0 +1,18 @@ +# LMDB Example Data (Downsampled) + +**WARNING: This data is heavily downsampled and intended ONLY for testing +the LMDB data loading pipeline. Do NOT use it for accuracy benchmarks or +comparisons with the standard npy data format.** + +## Contents + +- `water_training.lmdb` - 80 frames downsampled from `water/data/data_0` +- `water_validation.lmdb` - 20 frames downsampled from `water/data/data_2` +- `input_lmdb.json` - Example training config using LMDB data + +## Usage + +```bash +cd examples/lmdb_downsample_data +dp --pt train input_lmdb.json +``` From 1f1e18ea588c8627e9e5b4b4d6ee820ed0664ac2 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:11:31 +0800 Subject: [PATCH 16/22] Update lmdb_data.py --- deepmd/dpmodel/utils/lmdb_data.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 629ca797cb..0f3fcba024 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -45,10 +45,27 @@ # (energy is set by Loss DataRequirementItem; reduce() also sets high_prec=True) _HIGH_PREC_KEYS = frozenset({"energy"}) +# Process-level cache: lmdb does not allow opening the same path twice in one +# process. Multiple LmdbDataReader / LmdbDataset instances that point to the +# same file must share a single Environment object. +_ENV_CACHE: dict[str, lmdb.Environment] = {} + def _open_lmdb(path: str) -> lmdb.Environment: - """Open LMDB environment readonly.""" - return lmdb.open(path, readonly=True, lock=False, readahead=False, meminit=False) + """Open (or reuse) an LMDB environment readonly. + + The python-lmdb binding raises ``lmdb.Error`` if the same path is opened + more than once in a single process. We keep a per-process cache keyed by + the *resolved* absolute path so that callers transparently share one + ``lmdb.Environment`` handle. + """ + resolved = str(Path(path).resolve()) + env = _ENV_CACHE.get(resolved) + if env is not None: + return env + env = lmdb.open(path, readonly=True, lock=False, readahead=False, meminit=False) + _ENV_CACHE[resolved] = env + return env def _read_metadata(txn: lmdb.Transaction) -> dict: From c77bc9ec178906fa711eaba61011778912e85fa6 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:06:41 +0800 Subject: [PATCH 17/22] fix ut --- deepmd/dpmodel/utils/lmdb_data.py | 54 ++++++++++--- source/tests/pt/test_lmdb_dataloader.py | 101 +++++------------------- 2 files changed, 62 insertions(+), 93 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 0f3fcba024..f3cf36008e 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -45,29 +45,49 @@ # (energy is set by Loss DataRequirementItem; reduce() also sets high_prec=True) _HIGH_PREC_KEYS = frozenset({"energy"}) -# Process-level cache: lmdb does not allow opening the same path twice in one -# process. Multiple LmdbDataReader / LmdbDataset instances that point to the -# same file must share a single Environment object. -_ENV_CACHE: dict[str, lmdb.Environment] = {} +# Process-level cache: python-lmdb does not allow opening the same path twice +# in one process. We ref-count so the Environment is closed (and freed from +# the cache) once every reader that shares it is garbage-collected. +_ENV_CACHE: dict[str, tuple[lmdb.Environment, int]] = {} def _open_lmdb(path: str) -> lmdb.Environment: - """Open (or reuse) an LMDB environment readonly. + """Open (or reuse) a readonly LMDB environment with reference counting. The python-lmdb binding raises ``lmdb.Error`` if the same path is opened - more than once in a single process. We keep a per-process cache keyed by - the *resolved* absolute path so that callers transparently share one - ``lmdb.Environment`` handle. + more than once in a single process. We cache by resolved absolute path + and bump a reference count. Call :func:`_close_lmdb` when done to + decrement the count; when it reaches zero the environment is closed and + removed from the cache. """ resolved = str(Path(path).resolve()) - env = _ENV_CACHE.get(resolved) - if env is not None: + entry = _ENV_CACHE.get(resolved) + if entry is not None: + env, refcount = entry + _ENV_CACHE[resolved] = (env, refcount + 1) return env env = lmdb.open(path, readonly=True, lock=False, readahead=False, meminit=False) - _ENV_CACHE[resolved] = env + _ENV_CACHE[resolved] = (env, 1) return env +def _close_lmdb(path: str) -> None: + """Decrement the ref-count for *path* and close the env when it hits zero.""" + resolved = str(Path(path).resolve()) + entry = _ENV_CACHE.get(resolved) + if entry is None: + return + env, refcount = entry + if refcount <= 1: + del _ENV_CACHE[resolved] + try: + env.close() + except Exception: + pass + else: + _ENV_CACHE[resolved] = (env, refcount - 1) + + def _read_metadata(txn: lmdb.Transaction) -> dict: """Read and decode __metadata__ from LMDB transaction.""" raw = txn.get(b"__metadata__") @@ -357,6 +377,12 @@ def _resolve_dtype(self, key: str) -> np.dtype: return GLOBAL_ENER_FLOAT_PRECISION return GLOBAL_NP_FLOAT_PRECISION + def __del__(self) -> None: + """Release the LMDB environment ref-count on garbage collection.""" + path = getattr(self, "lmdb_path", None) + if path is not None: + _close_lmdb(path) + def get_batch_size_for_nloc(self, nloc: int) -> int: """Get batch_size for a given nloc. Uses auto rule if configured.""" if self._auto_rule is not None: @@ -1188,6 +1214,12 @@ def __init__( self.mixed_type = True + def __del__(self) -> None: + """Release the LMDB environment ref-count on garbage collection.""" + path = getattr(self, "lmdb_path", None) + if path is not None: + _close_lmdb(path) + @property def nloc_groups(self) -> dict[int, list[int]]: """Nloc → list of frame indices in self._frames.""" diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index 20704ba80b..f32474e33f 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -789,36 +789,13 @@ def multitask_lmdb_setup(tmp_path): class TestMultitaskLmdbTraining: - """Test multitask training with LMDB datasets.""" + """Test multitask training with LMDB datasets. - def test_multitask_lmdb_trainer_init(self, multitask_lmdb_setup, monkeypatch): - from copy import ( - deepcopy, - ) - - from deepmd.pt.entrypoints.main import ( - get_trainer, - ) - from deepmd.pt.utils.multi_task import ( - preprocess_shared_params, - ) - from deepmd.utils.argcheck import ( - normalize, - ) - from deepmd.utils.compat import ( - update_deepmd_input, - ) - - config, tmp_path = multitask_lmdb_setup - monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) - config["model"], shared_links = preprocess_shared_params(config["model"]) - config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) - assert trainer.multi_task - assert set(trainer.model_keys) == {"model_1", "model_2"} + All assertions are in a single test to avoid creating multiple heavy + se_atten trainers which would OOM on CI runners (7 GB RAM limit). + """ - def test_multitask_lmdb_training_runs(self, multitask_lmdb_setup, monkeypatch): + def test_multitask_lmdb_end_to_end(self, multitask_lmdb_setup, monkeypatch): from copy import ( deepcopy, ) @@ -842,33 +819,20 @@ def test_multitask_lmdb_training_runs(self, multitask_lmdb_setup, monkeypatch): config["model"], shared_links = preprocess_shared_params(config["model"]) config = normalize(config, multi_task=True) trainer = get_trainer(config, shared_links=shared_links) - trainer.run() - assert len(list(tmp_path.glob("model.ckpt*.pt"))) > 0 - def test_multitask_lmdb_get_data(self, multitask_lmdb_setup, monkeypatch): - from copy import ( - deepcopy, - ) + # -- trainer init assertions -- + assert trainer.multi_task + assert set(trainer.model_keys) == {"model_1", "model_2"} - from deepmd.pt.entrypoints.main import ( - get_trainer, - ) - from deepmd.pt.utils.multi_task import ( - preprocess_shared_params, - ) - from deepmd.utils.argcheck import ( - normalize, - ) - from deepmd.utils.compat import ( - update_deepmd_input, - ) + # -- shared params assertions -- + state_dict = trainer.wrapper.model.state_dict() + for key in state_dict: + if "model_1.atomic_model.descriptor" in key: + key2 = key.replace("model_1", "model_2") + assert key2 in state_dict + torch.testing.assert_close(state_dict[key], state_dict[key2]) - config, tmp_path = multitask_lmdb_setup - monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) - config["model"], shared_links = preprocess_shared_params(config["model"]) - config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) + # -- get_data assertions -- for task_key in ["model_1", "model_2"]: input_dict, label_dict, log_dict = trainer.get_data( is_train=True, task_key=task_key @@ -876,33 +840,6 @@ def test_multitask_lmdb_get_data(self, multitask_lmdb_setup, monkeypatch): assert "coord" in input_dict assert "sid" in log_dict - def test_multitask_lmdb_shared_params(self, multitask_lmdb_setup, monkeypatch): - from copy import ( - deepcopy, - ) - - from deepmd.pt.entrypoints.main import ( - get_trainer, - ) - from deepmd.pt.utils.multi_task import ( - preprocess_shared_params, - ) - from deepmd.utils.argcheck import ( - normalize, - ) - from deepmd.utils.compat import ( - update_deepmd_input, - ) - - config, tmp_path = multitask_lmdb_setup - monkeypatch.chdir(tmp_path) - config = update_deepmd_input(deepcopy(config), warning=True) - config["model"], shared_links = preprocess_shared_params(config["model"]) - config = normalize(config, multi_task=True) - trainer = get_trainer(config, shared_links=shared_links) - state_dict = trainer.wrapper.model.state_dict() - for key in state_dict: - if "model_1.atomic_model.descriptor" in key: - key2 = key.replace("model_1", "model_2") - assert key2 in state_dict - torch.testing.assert_close(state_dict[key], state_dict[key2]) + # -- training run assertions -- + trainer.run() + assert len(list(tmp_path.glob("model.ckpt*.pt"))) > 0 From 7db11119044996a704fba6c3faa4b2cbb5e0a0cc Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:59:47 +0800 Subject: [PATCH 18/22] Update test_lmdb_data.py --- source/tests/consistent/test_lmdb_data.py | 38 ++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/source/tests/consistent/test_lmdb_data.py b/source/tests/consistent/test_lmdb_data.py index 342ddc63f5..58bdf4ccee 100644 --- a/source/tests/consistent/test_lmdb_data.py +++ b/source/tests/consistent/test_lmdb_data.py @@ -135,22 +135,21 @@ def setUpClass(cls): f"{cls._tmpdir.name}/test.lmdb", nframes=10, natoms=6 ) cls._type_map = ["O", "H"] + cls._reader = LmdbDataReader(cls._lmdb_path, cls._type_map, batch_size=2) + cls._ds = LmdbDataset(cls._lmdb_path, cls._type_map, batch_size=2) @classmethod def tearDownClass(cls): + del cls._ds, cls._reader cls._tmpdir.cleanup() def test_same_len(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(len(reader), len(ds)) + self.assertEqual(len(self._reader), len(self._ds)) def test_same_frame_data(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - for i in range(len(reader)): - frame_dp = reader[i] - frame_pt = ds[i] + for i in range(len(self._reader)): + frame_dp = self._reader[i] + frame_pt = self._ds[i] self.assertEqual(set(frame_dp.keys()), set(frame_pt.keys())) for key in frame_dp: dp_val = frame_dp[key] @@ -168,11 +167,9 @@ def test_same_batch_size(self): self.assertEqual(reader.batch_size, ds.batch_size) def test_same_properties(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - self.assertEqual(reader.index, ds.index) - self.assertEqual(reader.total_batch, ds.total_batch) - self.assertEqual(reader.batch_sizes, ds.batch_sizes) + self.assertEqual(self._reader.index, self._ds.index) + self.assertEqual(self._reader.total_batch, self._ds.total_batch) + self.assertEqual(self._reader.batch_sizes, self._ds.batch_sizes) def test_data_requirement(self): req = [ @@ -205,19 +202,20 @@ def setUpClass(cls): cls._tmpdir = tempfile.TemporaryDirectory() cls._lmdb_path = _create_mixed_nloc_lmdb(f"{cls._tmpdir.name}/mixed.lmdb") cls._type_map = ["O", "H"] + cls._reader = LmdbDataReader(cls._lmdb_path, cls._type_map, batch_size=2) + cls._ds = LmdbDataset(cls._lmdb_path, cls._type_map, batch_size=2) @classmethod def tearDownClass(cls): + del cls._ds, cls._reader cls._tmpdir.cleanup() def test_collate_mixed_nloc_raises(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) with self.assertRaises(NotImplementedError): - _collate_lmdb_batch([reader[0], reader[4]]) + _collate_lmdb_batch([self._reader[0], self._reader[4]]) def test_collate_same_nloc_ok(self): - reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size=2) - batch = _collate_lmdb_batch([reader[0], reader[1]]) + batch = _collate_lmdb_batch([self._reader[0], self._reader[1]]) self.assertEqual(batch["coord"].shape[0], 2) def test_mixed_batch_true_raises(self): @@ -225,16 +223,14 @@ def test_mixed_batch_true_raises(self): LmdbDataset(self._lmdb_path, self._type_map, batch_size=2, mixed_batch=True) def test_pt_dataset_mixed_batch_flag(self): - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) - self.assertFalse(ds.mixed_batch) + self.assertFalse(self._ds.mixed_batch) def test_pt_full_epoch_mixed_nloc(self): import torch - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=2) all_fids = [] with torch.device("cpu"): - for dl in ds.dataloaders: + for dl in self._ds.dataloaders: for batch in dl: atype = batch["atype"] nloc = atype.shape[1] From 22fde3da4e01232dc6258b7cd4c0093eac0fdd47 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:40:25 +0800 Subject: [PATCH 19/22] Update test_lmdb_dataloader.py --- source/tests/pt/test_lmdb_dataloader.py | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/source/tests/pt/test_lmdb_dataloader.py b/source/tests/pt/test_lmdb_dataloader.py index f32474e33f..ebb505706d 100644 --- a/source/tests/pt/test_lmdb_dataloader.py +++ b/source/tests/pt/test_lmdb_dataloader.py @@ -50,10 +50,10 @@ def _encode_array(arr: np.ndarray) -> dict: np.array([0] * (natoms // 2) + [1] * (natoms // 2), dtype=np.int64) ), "orig": _encode_array(np.zeros(3, dtype=np.float64)), - "cells": _encode_array(rng.randn(3, 3).astype(np.float32)), - "coords": _encode_array(rng.randn(natoms, 3).astype(np.float32)), - "energies": _encode_array(np.array(rng.randn(), dtype=np.float32)), - "forces": _encode_array(rng.randn(natoms, 3).astype(np.float32)), + "cells": _encode_array((np.eye(3) * 10.0).astype(np.float64)), + "coords": _encode_array((rng.rand(natoms, 3) * 10.0).astype(np.float64)), + "energies": _encode_array(np.array(rng.randn(), dtype=np.float64)), + "forces": _encode_array(rng.randn(natoms, 3).astype(np.float64)), } @@ -697,19 +697,15 @@ def multitask_lmdb_setup(tmp_path): "shared_dict": { "type_map_all": ["O", "H"], "my_descriptor": { - "type": "se_atten", - "sel": 40, + "type": "se_e2_a", + "sel": [4, 4], "rcut_smth": 0.5, "rcut": 4.0, - "neuron": [4, 8, 16], + "neuron": [4, 8], "axis_neuron": 4, - "attn": 4, - "attn_layer": 2, - "attn_dotr": True, - "attn_mask": False, "precision": "float64", }, - "my_fitting": {"neuron": [16, 16], "precision": "float64", "seed": 1}, + "my_fitting": {"neuron": [8, 8], "precision": "float64", "seed": 1}, }, "model_dict": { "model_1": { @@ -791,8 +787,8 @@ def multitask_lmdb_setup(tmp_path): class TestMultitaskLmdbTraining: """Test multitask training with LMDB datasets. - All assertions are in a single test to avoid creating multiple heavy - se_atten trainers which would OOM on CI runners (7 GB RAM limit). + Uses se_e2_a (not se_atten) to keep memory usage low on CI runners (~7 GB). + All assertions are in a single test to avoid creating multiple trainers. """ def test_multitask_lmdb_end_to_end(self, multitask_lmdb_setup, monkeypatch): @@ -843,3 +839,9 @@ def test_multitask_lmdb_end_to_end(self, multitask_lmdb_setup, monkeypatch): # -- training run assertions -- trainer.run() assert len(list(tmp_path.glob("model.ckpt*.pt"))) > 0 + + # Explicit cleanup to free memory on CI + import gc + + del trainer + gc.collect() From 3e2e77b43721023b0aff9a84666fd8df8bea3958 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:52:37 +0800 Subject: [PATCH 20/22] Update test_lmdb_data.py --- source/tests/consistent/test_lmdb_data.py | 215 +++++----------------- 1 file changed, 42 insertions(+), 173 deletions(-) diff --git a/source/tests/consistent/test_lmdb_data.py b/source/tests/consistent/test_lmdb_data.py index 58bdf4ccee..6e9cecee52 100644 --- a/source/tests/consistent/test_lmdb_data.py +++ b/source/tests/consistent/test_lmdb_data.py @@ -19,7 +19,6 @@ try: from deepmd.pt.utils.lmdb_dataset import ( LmdbDataset, - _collate_lmdb_batch, ) INSTALLED_PT = True @@ -124,6 +123,24 @@ def _create_mixed_nloc_lmdb(path: str) -> str: return path +def _assert_frames_equal(test_case, frame_dp, frame_pt, frame_idx): + """Assert two frames (from reader and dataset) are identical.""" + test_case.assertEqual( + set(frame_dp.keys()), + set(frame_pt.keys()), + msg=f"frame={frame_idx}", + ) + for key in frame_dp: + dp_val = frame_dp[key] + pt_val = frame_pt[key] + if isinstance(dp_val, np.ndarray): + np.testing.assert_array_equal( + dp_val, pt_val, err_msg=f"key={key}, frame={frame_idx}" + ) + else: + test_case.assertEqual(dp_val, pt_val, msg=f"key={key}, frame={frame_idx}") + + @unittest.skipUnless(INSTALLED_PT, "PyTorch not available") class TestLmdbDataConsistency(unittest.TestCase): """Verify LmdbDataReader (dpmodel) and LmdbDataset (pt) produce identical outputs.""" @@ -148,18 +165,7 @@ def test_same_len(self): def test_same_frame_data(self): for i in range(len(self._reader)): - frame_dp = self._reader[i] - frame_pt = self._ds[i] - self.assertEqual(set(frame_dp.keys()), set(frame_pt.keys())) - for key in frame_dp: - dp_val = frame_dp[key] - pt_val = frame_pt[key] - if isinstance(dp_val, np.ndarray): - np.testing.assert_array_equal( - dp_val, pt_val, err_msg=f"key={key}, frame={i}" - ) - else: - self.assertEqual(dp_val, pt_val, msg=f"key={key}, frame={i}") + _assert_frames_equal(self, self._reader[i], self._ds[i], i) def test_same_batch_size(self): reader = LmdbDataReader(self._lmdb_path, self._type_map, batch_size="auto") @@ -170,6 +176,9 @@ def test_same_properties(self): self.assertEqual(self._reader.index, self._ds.index) self.assertEqual(self._reader.total_batch, self._ds.total_batch) self.assertEqual(self._reader.batch_sizes, self._ds.batch_sizes) + self.assertEqual(self._reader.nframes, self._ds.nframes) + self.assertEqual(self._reader.mixed_type, self._ds.mixed_type) + self.assertEqual(self._reader.mixed_batch, self._ds.mixed_batch) def test_data_requirement(self): req = [ @@ -192,166 +201,26 @@ def test_data_requirement(self): np.testing.assert_array_equal(frame_dp["virial"], frame_pt["virial"]) self.assertEqual(frame_dp["find_virial"], frame_pt["find_virial"]) - -@unittest.skipUnless(INSTALLED_PT, "PyTorch not available") -class TestMixedNlocConsistency(unittest.TestCase): - """Consistency tests for mixed-nloc LMDB: collate, LmdbDataset iteration.""" - - @classmethod - def setUpClass(cls): - cls._tmpdir = tempfile.TemporaryDirectory() - cls._lmdb_path = _create_mixed_nloc_lmdb(f"{cls._tmpdir.name}/mixed.lmdb") - cls._type_map = ["O", "H"] - cls._reader = LmdbDataReader(cls._lmdb_path, cls._type_map, batch_size=2) - cls._ds = LmdbDataset(cls._lmdb_path, cls._type_map, batch_size=2) - - @classmethod - def tearDownClass(cls): - del cls._ds, cls._reader - cls._tmpdir.cleanup() - - def test_collate_mixed_nloc_raises(self): - with self.assertRaises(NotImplementedError): - _collate_lmdb_batch([self._reader[0], self._reader[4]]) - - def test_collate_same_nloc_ok(self): - batch = _collate_lmdb_batch([self._reader[0], self._reader[1]]) - self.assertEqual(batch["coord"].shape[0], 2) - - def test_mixed_batch_true_raises(self): - with self.assertRaises(NotImplementedError): - LmdbDataset(self._lmdb_path, self._type_map, batch_size=2, mixed_batch=True) - - def test_pt_dataset_mixed_batch_flag(self): - self.assertFalse(self._ds.mixed_batch) - - def test_pt_full_epoch_mixed_nloc(self): - import torch - - all_fids = [] - with torch.device("cpu"): - for dl in self._ds.dataloaders: - for batch in dl: - atype = batch["atype"] - nloc = atype.shape[1] - for i in range(atype.shape[0]): - self.assertEqual(atype[i].shape[0], nloc) - all_fids.extend(batch["fid"]) - self.assertEqual(sorted(all_fids), list(range(10))) - - def test_pt_batch_shapes_consistent(self): - import torch - - ds = LmdbDataset(self._lmdb_path, self._type_map, batch_size=3) - with torch.device("cpu"): - for batch in ds.dataloaders[0]: - bs = batch["atype"].shape[0] - nloc = batch["atype"].shape[1] - self.assertEqual(batch["coord"].shape, (bs, nloc, 3)) - self.assertEqual(batch["force"].shape, (bs, nloc, 3)) - self.assertEqual(batch["natoms"].shape, (bs, 4)) - - -@unittest.skipUnless(INSTALLED_PT, "PyTorch not available") -class TestLmdbNeighborStatConsistency(unittest.TestCase): - """Test neighbor stat values from LMDB match expected geometry.""" - - @classmethod - def setUpClass(cls): - cls._tmpdir = tempfile.TemporaryDirectory() - X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] - positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T - natoms = 27 - cell = np.array([3.0, 0, 0, 0, 3.0, 0, 0, 0, 3.0], dtype=np.float64) - atype = np.zeros(natoms, dtype=np.int64) - path = f"{cls._tmpdir.name}/grid.lmdb" - env = lmdb.open(path, map_size=10 * 1024 * 1024) - with env.begin(write=True) as txn: - meta = { - "nframes": 3, - "frame_idx_fmt": "012d", - "type_map": ["TYPE"], - "system_info": {"natoms": [natoms], "formula": "grid"}, - } - txn.put(b"__metadata__", msgpack.packb(meta, use_bin_type=True)) - for i in range(3): - frame = { - "atom_types": { - "type": " 0, distance <= rcut_eps) - ) - - self.assertAlmostEqual(min_nbor_dist, 1.0, places=6) - expected = [expected_neighbors] - if not mixed_type: - expected.append(0) - np.testing.assert_array_equal(max_nbor_size, expected) + def test_mixed_nloc_same_frame_data(self): + """Reader and dataset produce identical frames for mixed atom counts.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = _create_mixed_nloc_lmdb(f"{tmpdir}/mixed.lmdb") + reader = LmdbDataReader(path, self._type_map, batch_size=2) + ds = LmdbDataset(path, self._type_map, batch_size=2) + self.assertEqual(len(reader), len(ds)) + for i in range(len(reader)): + _assert_frames_equal(self, reader[i], ds[i], i) + + def test_mixed_nloc_same_properties(self): + """Reader and dataset agree on properties for mixed-nloc LMDB.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = _create_mixed_nloc_lmdb(f"{tmpdir}/mixed.lmdb") + reader = LmdbDataReader(path, self._type_map, batch_size=2) + ds = LmdbDataset(path, self._type_map, batch_size=2) + self.assertEqual(reader.nframes, ds.nframes) + self.assertEqual(reader.batch_sizes, ds.batch_sizes) + self.assertEqual(reader.mixed_batch, ds.mixed_batch) + self.assertFalse(reader.mixed_batch) if __name__ == "__main__": From fb73297d1ae972d65bec81a3aacc24506160058b Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:35:20 +0800 Subject: [PATCH 21/22] resolve comments --- deepmd/dpmodel/utils/lmdb_data.py | 6 +-- deepmd/entrypoints/test.py | 20 ++----- deepmd/pt/entrypoints/main.py | 54 ++++++++----------- deepmd/utils/data_system.py | 9 ++++ source/tests/common/dpmodel/test_lmdb_data.py | 1 - 5 files changed, 39 insertions(+), 51 deletions(-) diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index f3cf36008e..243d4f525d 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -146,11 +146,9 @@ def _remap_keys(frame: dict[str, Any]) -> dict[str, Any]: return out -def is_lmdb(systems: Any) -> bool: +def is_lmdb(systems: str) -> bool: """Check if systems points to an LMDB dataset.""" - if not isinstance(systems, str): - return False - return systems.endswith(".lmdb") or Path(systems, "data.mdb").exists() + return systems.endswith(".lmdb") or Path(systems, "data.mdb").is_file() def _parse_metadata(meta: dict) -> tuple[int, str, list[int]]: diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index bd8cb09734..9a5f2f3f79 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -13,7 +13,6 @@ import numpy as np from deepmd.common import ( - expand_sys_str, j_loader, ) from deepmd.dpmodel.utils.lmdb_data import ( @@ -148,11 +147,8 @@ def test( systems = str((root / Path(systems)).resolve()) else: systems = [str((root / Path(ss)).resolve()) for ss in systems] - if is_lmdb(systems): - all_sys = [systems] - else: - patterns = data_params.get("rglob_patterns", None) - all_sys = process_systems(systems, patterns=patterns) + patterns = data_params.get("rglob_patterns", None) + all_sys = process_systems(systems, patterns=patterns) elif valid_json is not None: jdata = j_loader(valid_json) jdata = update_deepmd_input(jdata) @@ -165,19 +161,13 @@ def test( systems = str((root / Path(systems)).resolve()) else: systems = [str((root / Path(ss)).resolve()) for ss in systems] - if is_lmdb(systems): - all_sys = [systems] - else: - patterns = data_params.get("rglob_patterns", None) - all_sys = process_systems(systems, patterns=patterns) + patterns = data_params.get("rglob_patterns", None) + all_sys = process_systems(systems, patterns=patterns) elif datafile is not None: with open(datafile) as datalist: all_sys = datalist.read().splitlines() elif system is not None: - if is_lmdb(system): - all_sys = [system] - else: - all_sys = expand_sys_str(system) + all_sys = process_systems(system) else: raise RuntimeError("No data source specified for testing") diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index c13c292be3..c60d781d88 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -147,6 +147,23 @@ def prepare_trainer_input_single( Path(stat_file_path_single).mkdir() stat_file_path_single = DPPath(stat_file_path_single, "a") + rank_seed = [rank, seed % (2**32)] if seed is not None else None + + def _make_dp_loader_set( + systems: str | list[str], + dataset_params: dict[str, Any], + ) -> DpLoaderSet: + """Create a DpLoaderSet from systems with pattern expansion.""" + patterns = dataset_params.get("rglob_patterns", None) + systems = process_systems(systems, patterns=patterns) + return DpLoaderSet( + systems, + dataset_params["batch_size"], + model_params_single["type_map"], + seed=rank_seed, + modifier=modifier, + ) + # LMDB path: single string → LmdbDataset if is_lmdb(training_systems): auto_prob = training_dataset_params.get("auto_prob", None) @@ -163,46 +180,21 @@ def prepare_trainer_input_single( validation_dataset_params["batch_size"], ) elif validation_systems is not None: - val_patterns = validation_dataset_params.get("rglob_patterns", None) - validation_systems = process_systems(validation_systems, val_patterns) - rank_seed = [rank, seed % (2**32)] if seed is not None else None - validation_data_single = DpLoaderSet( - validation_systems, - validation_dataset_params["batch_size"], - model_params_single["type_map"], - seed=rank_seed, - modifier=modifier, + validation_data_single = _make_dp_loader_set( + validation_systems, validation_dataset_params ) else: validation_data_single = None else: # Standard npy path - trn_patterns = training_dataset_params.get("rglob_patterns", None) - training_systems = process_systems(training_systems, patterns=trn_patterns) - if validation_systems is not None: - val_patterns = validation_dataset_params.get("rglob_patterns", None) - validation_systems = process_systems(validation_systems, val_patterns) - - # avoid the same batch sequence among devices - rank_seed = [rank, seed % (2**32)] if seed is not None else None + train_data_single = _make_dp_loader_set( + training_systems, training_dataset_params + ) validation_data_single = ( - DpLoaderSet( - validation_systems, - validation_dataset_params["batch_size"], - model_params_single["type_map"], - seed=rank_seed, - modifier=modifier, - ) + _make_dp_loader_set(validation_systems, validation_dataset_params) if validation_systems else None ) - train_data_single = DpLoaderSet( - training_systems, - training_dataset_params["batch_size"], - model_params_single["type_map"], - seed=rank_seed, - modifier=modifier, - ) return ( train_data_single, validation_data_single, diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index dd60d9a7e0..05e9ae60dc 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -791,6 +791,7 @@ def process_systems( If it is a single directory, search for all the systems in the directory. If it is a list, each item in the list is treated as a directory to search. + If it is a single LMDB path, return it directly without expansion. Check if the systems are valid. Parameters @@ -805,6 +806,14 @@ def process_systems( result_systems: list of str The valid systems """ + from deepmd.dpmodel.utils.lmdb_data import ( + is_lmdb, + ) + + # LMDB path: return directly without expansion + if isinstance(systems, str) and is_lmdb(systems): + return [systems] + # Normalize input to a list of paths to search if isinstance(systems, str): search_paths = [systems] diff --git a/source/tests/common/dpmodel/test_lmdb_data.py b/source/tests/common/dpmodel/test_lmdb_data.py index 106c550177..ac096633c2 100644 --- a/source/tests/common/dpmodel/test_lmdb_data.py +++ b/source/tests/common/dpmodel/test_lmdb_data.py @@ -309,7 +309,6 @@ def test_is_lmdb(self): self.assertTrue(is_lmdb(self._lmdb_path)) self.assertTrue(is_lmdb("something.lmdb")) self.assertFalse(is_lmdb("/some/npy/system")) - self.assertFalse(is_lmdb(["list", "of", "systems"])) def test_lmdb_test_data(self): td = LmdbTestData(self._lmdb_path, type_map=self._type_map, shuffle_test=False) From 72736f76e20804a4ae3e016fc7aafbc9e03ec853 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:51:03 +0800 Subject: [PATCH 22/22] fix ut --- deepmd/pt/entrypoints/main.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index c60d781d88..7b45c46333 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -165,7 +165,7 @@ def _make_dp_loader_set( ) # LMDB path: single string → LmdbDataset - if is_lmdb(training_systems): + if isinstance(training_systems, str) and is_lmdb(training_systems): auto_prob = training_dataset_params.get("auto_prob", None) train_data_single = LmdbDataset( training_systems, @@ -173,7 +173,11 @@ def _make_dp_loader_set( training_dataset_params["batch_size"], auto_prob_style=auto_prob, ) - if validation_systems is not None and is_lmdb(validation_systems): + if ( + validation_systems is not None + and isinstance(validation_systems, str) + and is_lmdb(validation_systems) + ): validation_data_single = LmdbDataset( validation_systems, model_params_single["type_map"], @@ -365,7 +369,11 @@ def train( if not multi_task: type_map = config["model"].get("type_map") training_systems = config["training"]["training_data"].get("systems") - if training_systems is not None and is_lmdb(training_systems): + if ( + training_systems is not None + and isinstance(training_systems, str) + and is_lmdb(training_systems) + ): from deepmd.dpmodel.utils.lmdb_data import ( make_neighbor_stat_data, ) @@ -385,7 +393,11 @@ def train( training_systems = config["training"]["data_dict"][model_item][ "training_data" ].get("systems") - if training_systems is not None and is_lmdb(training_systems): + if ( + training_systems is not None + and isinstance(training_systems, str) + and is_lmdb(training_systems) + ): from deepmd.dpmodel.utils.lmdb_data import ( make_neighbor_stat_data, )