Retry‑safe writes with np.memmap: unresolved durability guarantees
0 reputation · 28 Jan 2024, 23:57 UTC
0 reputation · 28 Jan 2024, 23:57 UTC
When persisting large arrays to disk using np.memmap or np.lib.format.open_memmap, a common design goal is to enable retry‑safe writes that overwrite the same file region instead of creating duplicate data. The current documented workflow is to open the file in 'r+' or 'w+' mode, modify the array in place, and invoke flush() to push pages to the OS page cache before the process can fail or be restarted.
However, NumPy’s documentation does not specify whether flush() guarantees that the data has been physically written to stable storage, nor does it describe any atomicity guarantees for the write operation itself. Consequently, it is unclear whether a retry that re‑opens the same file will see the partial data, or whether a crash between flush() and the next write could leave the file in an inconsistent state.
Given these gaps, a typical retry pattern is to write to a temporary file and then atomically rename it onto the target, but this approach still relies on external file‑system semantics. The key unresolved question is:
np.memmap.flush() provide durability equivalent to fsync(), or must callers explicitly invoke OS‑level sync mechanisms to guarantee that a retried write will not duplicate or corrupt data?31275 reputation · 29 Jan 2024, 02:40 UTC
No, np.memmap.flush() does not provide durability equivalent to fsync(). It ensures that modified pages are pushed from the process memory to the OS page cache, but it does not guarantee that the data has been physically written to stable storage. To achieve durability, you must explicitly invoke OS-level synchronization on the file descriptor.
NumPy's memmap is a wrapper around the operating system's mmap implementation. When you call flush(), NumPy delegates the operation to the underlying mmap.flush(). In most POSIX and Windows environments, this tells the kernel that the pages are dirty and should be scheduled for writing, but the call typically returns before the disk controller has confirmed the write.
It is critical to distinguish between durability (will the data survive a power loss?) and atomicity (will the write be all-or-nothing?).
flush() protects against process crashes (because the OS still holds the data in the page cache), but not against kernel panics or power failures.flush() nor fsync() provides atomicity. If a crash occurs during a large write, you may encounter "torn writes," where the file contains a mixture of old and new data.To guarantee that a retried write will not observe corrupted or missing data, follow these steps:
mm.flush() to move data to the kernel cache.os.fsync() on the file descriptor. Since np.memmap does not expose the file descriptor directly in its public API, you must either maintain your own file handle or access the internal mapping: os.fsync(mm._mmap.fileno()).fsync the temp file, and use os.replace() to move it to the target destination.You can verify that flush() does not trigger a hardware sync by using strace (on Linux) to monitor system calls:
# Run your python script and filter for fsync or msync calls
strace -e trace=fsync,fdatasync,msync python your_script.py
If flush() were providing full durability, you would see an fsync or msync syscall associated with the file descriptor; in standard NumPy usage, you will not.
This analysis assumes a standard POSIX-compliant filesystem or NTFS. Note that network filesystems (NFS, SMB) often have weaker fsync and rename semantics, which may render the "write-rename" pattern non-atomic. Additionally, accessing mm._mmap relies on a private attribute that may change in future NumPy versions.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.