From fa03b8cfff1d794428309fe54b1af48edb1ce2ae Mon Sep 17 00:00:00 2001 From: Tyagi Date: Wed, 2 Sep 2026 10:54:54 +1000 Subject: [PATCH 1/2] Keep snapshot bulk filenames compact Use deterministic mesh_0000-style identifiers for PETSc-HDF5 snapshot bulk files instead of expanding a loaded mesh's complete source pathname. Preserve the original mesh name in wrapper metadata for exact restore matching. Add a regression proving long source names do not enter bulk filenames. --- src/underworld3/checkpoint/disk_snapshot.py | 13 ++++++++---- tests/test_0010_snapshot_disk_format.py | 22 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/underworld3/checkpoint/disk_snapshot.py b/src/underworld3/checkpoint/disk_snapshot.py index 7d30d2000..61a98a397 100644 --- a/src/underworld3/checkpoint/disk_snapshot.py +++ b/src/underworld3/checkpoint/disk_snapshot.py @@ -247,8 +247,8 @@ def read_snapshot_metadata(path: str) -> dict: # # /path/to/run.snap.h5 wrapper (metadata, h5py-readable) # /path/to/run.snap.bulk/ companion directory (one per snapshot) -# {mesh_safe}.mesh.00000.h5 mesh DM dump (PETSc HDF5) -# {mesh_safe}.{var_clean}.00000.h5 per-variable section + vec (PETSc HDF5) +# mesh_0000.mesh.00000.h5 mesh DM dump (PETSc HDF5) +# mesh_0000.{var_clean}.00000.h5 per-variable section + vec (PETSc HDF5) # ... one set per (mesh, var) ... # # The bulk-dir path is derived from the wrapper path by convention, so a @@ -306,8 +306,13 @@ def write_snapshot(model, path: str) -> str: # bulk directory. write_checkpoint is collective (PETSc HDF5 # viewer), so all ranks must participate. mesh_records: list[dict] = [] - for mesh in list(model._meshes.values()): - mesh_safe = _sanitise(mesh.name) + for mesh_index, mesh in enumerate(list(model._meshes.values())): + # Loaded meshes commonly use their complete source path as ``name``. + # Embedding that path in every PETSc-HDF5 bulk filename can exceed + # practical MPI-I/O pathname limits even when the wrapper path itself + # is valid. The wrapper preserves the original name for exact restore + # matching, so bulk files only need a compact deterministic identifier. + mesh_safe = f"mesh_{mesh_index:04d}" mesh_vars = list(mesh.vars.values()) # Filter to allocated variables — same skip rule as the in-memory # path: lazy-allocated vars with _gvec == None have no data. diff --git a/tests/test_0010_snapshot_disk_format.py b/tests/test_0010_snapshot_disk_format.py index 56609dfee..a116c5d36 100644 --- a/tests/test_0010_snapshot_disk_format.py +++ b/tests/test_0010_snapshot_disk_format.py @@ -214,6 +214,28 @@ def test_write_snapshot_produces_wrapper_and_bulk_dir(tmp_path): assert any("V.00000.h5" in f for f in files) +def test_snapshot_bulk_filenames_do_not_expand_loaded_mesh_name(tmp_path): + """A source pathname remains metadata, not a PETSc bulk filename.""" + import h5py + import os + + uw, model, mesh, T, V = _fresh_model_mesh_and_vars() + mesh.name = "/g/data/project/user/" + "nested_directory/" * 12 + "mesh.msh.h5" + + path = str(tmp_path / "compact.snap.h5") + model.save_state(file=path) + + bulk = str(tmp_path / "compact.snap.bulk") + files = sorted(os.listdir(bulk)) + assert files + assert all(filename.startswith("mesh_0000.") for filename in files) + assert max(map(len, files)) < 64 + + with h5py.File(path, "r") as h5: + assert list(h5["meshes"]) == ["mesh_0000"] + assert h5["meshes"]["mesh_0000"].attrs["name"] == mesh.name + + def test_write_snapshot_populates_wrapper_layout(tmp_path): """The wrapper carries the per-mesh + per-variable metadata that makes 'what's in this snapshot?' answerable from h5py alone.""" From 10957a5112eead945242fd3937d3a92c2884f481 Mon Sep 17 00:00:00 2001 From: Tyagi Date: Wed, 2 Sep 2026 11:15:04 +1000 Subject: [PATCH 2/2] Use short relative paths for snapshot PETSc I/O Keep snapshot artifacts beside their wrapper while changing into the artifact parent directory for native PETSc/HDF5 reads and writes. This prevents valid but long user output roots from crossing MPI-I/O pathname limits even after bulk filenames have been compacted. Apply the same short-path handling to exact checkpoint restore and selective variable extraction, and add a focused regression that verifies the helper restores the process working directory. --- src/underworld3/checkpoint/disk_snapshot.py | 58 +++++++++++++++------ tests/test_0010_snapshot_disk_format.py | 17 ++++++ 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/underworld3/checkpoint/disk_snapshot.py b/src/underworld3/checkpoint/disk_snapshot.py index 61a98a397..c0cdac286 100644 --- a/src/underworld3/checkpoint/disk_snapshot.py +++ b/src/underworld3/checkpoint/disk_snapshot.py @@ -40,6 +40,7 @@ import json import os import warnings +from contextlib import contextmanager from typing import Any, Optional import numpy as np @@ -262,6 +263,23 @@ def _bulk_dir_for(wrapper_path: str) -> str: return base + ".bulk" +@contextmanager +def _short_io_path(path: str): + """Expose ``path`` to native I/O as a basename from its parent directory. + + Some parallel PETSc/HDF5 stacks fail on valid absolute paths well below + ``PATH_MAX``. Snapshot artifacts retain their normal locations, while the + native reader or writer receives only the final path component. + """ + absolute_path = os.path.abspath(path) + previous_directory = os.getcwd() + os.chdir(os.path.dirname(absolute_path)) + try: + yield os.path.basename(absolute_path) + finally: + os.chdir(previous_directory) + + def _sanitise(name: str) -> str: """Sanitise a mesh / variable name for use as a filename component. @@ -324,14 +342,15 @@ def write_snapshot(model, path: str) -> str: # (consumed by the reload path below). Suppress the FutureWarning for # this internal call rather than spam every snapshot. (Migrating the # snapshot to write_timestep is tracked in #252.) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - mesh.write_checkpoint( - mesh_safe, - outputPath=bulk_dir, - meshVars=mesh_vars, - index=0, - ) + with _short_io_path(bulk_dir) as short_bulk_dir: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + mesh.write_checkpoint( + mesh_safe, + outputPath=short_bulk_dir, + meshVars=mesh_vars, + index=0, + ) mesh_records.append({ "name": mesh.name, @@ -501,10 +520,13 @@ def read_snapshot(model, path: str) -> None: f"snapshot variable {var_name!r} not registered on " f"mesh {mesh_name!r}" ) - var.read_checkpoint( - os.path.join(bulk_dir, external_file), - data_name=var_name, - ) + with _short_io_path( + os.path.join(bulk_dir, external_file) + ) as short_external_file: + var.read_checkpoint( + short_external_file, + data_name=var_name, + ) # Phase 3a: restore state-bearer dataclasses. if _GROUP_PYTHON_STATE in f: @@ -954,13 +976,17 @@ def extract_var_via_bridge(wrapper_path: str, var_name: str): # Rebuild a transient source mesh + variable to read DOFs into. # We deliberately don't register them with the live model — these # are throwaway and exit scope on return. - src_mesh = uw.discretisation.Mesh(os.path.join(bulk_dir, mesh_file_rel)) + with _short_io_path( + os.path.join(bulk_dir, mesh_file_rel) + ) as short_mesh_file: + src_mesh = uw.discretisation.Mesh(short_mesh_file) src_var = uw.discretisation.MeshVariable( var_name, src_mesh, components, degree=degree, continuous=continuous, ) - src_var.read_checkpoint( - os.path.join(bulk_dir, var_file_rel), data_name=var_name - ) + with _short_io_path( + os.path.join(bulk_dir, var_file_rel) + ) as short_var_file: + src_var.read_checkpoint(short_var_file, data_name=var_name) coords = np.asarray(src_var.coords).copy() values = np.asarray(src_var.array[...]).reshape( diff --git a/tests/test_0010_snapshot_disk_format.py b/tests/test_0010_snapshot_disk_format.py index a116c5d36..012cce0a5 100644 --- a/tests/test_0010_snapshot_disk_format.py +++ b/tests/test_0010_snapshot_disk_format.py @@ -236,6 +236,23 @@ def test_snapshot_bulk_filenames_do_not_expand_loaded_mesh_name(tmp_path): assert h5["meshes"]["mesh_0000"].attrs["name"] == mesh.name +def test_short_io_path_uses_basename_and_restores_cwd(tmp_path): + """Native PETSc/HDF5 calls do not receive the full user output path.""" + import os + from underworld3.checkpoint.disk_snapshot import _short_io_path + + original_directory = os.getcwd() + target_directory = tmp_path / "nested" / "snapshot.bulk" + target_directory.mkdir(parents=True) + target_file = target_directory / "mesh_0000.T.00000.h5" + + with _short_io_path(str(target_file)) as short_path: + assert short_path == target_file.name + assert os.getcwd() == str(target_directory) + + assert os.getcwd() == original_directory + + def test_write_snapshot_populates_wrapper_layout(tmp_path): """The wrapper carries the per-mesh + per-variable metadata that makes 'what's in this snapshot?' answerable from h5py alone."""