Feat(datapipes): Zarr mesh schema and readers — a zarr storage path for mesh-native CAE pipelines#1798
Feat(datapipes): Zarr mesh schema and readers — a zarr storage path for mesh-native CAE pipelines#1798negin513 wants to merge 21 commits into
Conversation
…eshReader Add a chunked, compressed, random-access zarr counterpart to the .pmsh/ .pdmsh memmap format, bridging the zarr storage world (PhysicsNeMo-Curator output, cloud object stores) and the physicsnemo.mesh object world: - save_mesh_to_zarr / save_domain_mesh_to_zarr: reference writers for the physicsnemo-mesh-zarr and physicsnemo-domainmesh-zarr schemas. The DomainMesh schema mirrors the .pdmsh tree (case-level global_data single-sourced at the root + interior/ + boundaries/<name>/) with verified layout attrs. - ZarrMeshReader: returns (Mesh, metadata); drop-in alternative to MeshReader. subpath= navigates into DomainMesh groups; merge_global_data_from= merges case-level metadata at read time (MeshReaderWithGlobalData semantics, collision-checked). Contiguous block subsampling with chunk-aligned I/O; groups with a verified layout="soup" attr skip the cells read entirely and issue all array reads concurrently. zarr-python and tensorstore backends. - ZarrDomainMeshReader: returns (DomainMesh, metadata); drop-in alternative to DomainMeshReader with per-submesh subsample selection and full_resolution_boundaries for exact-geometry queries (SDF). - to_cell_soup: denormalize to per-cell vertex soup so block subsamples read contiguously on meshes without point-index locality. Benchmarked on DrivAerML over Lustre: up to 6.6x reader throughput cold / 3x warm vs memmap MeshReader at 8 workers; bitwise-equivalent samples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give the unified external aerodynamics recipe a zarr storage backend alongside .pdmsh, for both training paths: - datasets/drivaer_ml_surface_zarr.yaml: reads the vehicle boundary out of full-DomainMesh zarr groups via ZarrMeshReader subpath navigation, with case-level freestream global_data merged at read time (single-sourced, nothing baked into the boundary data). Transform stack identical to drivaer_ml_surface.yaml. - datasets/drivaer_ml_volume_zarr.yaml: full DomainMesh cases via ZarrDomainMeshReader; the stl_geometry boundary is stored indexed at full resolution in the group (the zarr analog of DomainMeshReader's sibling-file extra_boundaries) for exact SDF queries. Transform stack identical to drivaer_ml_volume.yaml. - src/convert_pdmsh_to_zarr.py: transitional thin CLI over save_domain_mesh_to_zarr for datasets already curated to .pdmsh, with --soup-boundaries (contiguous block reads) and --extra-boundary NAME=GLOB (sibling .pmsh meshes as full-resolution boundaries). New curation should emit the schema directly from PhysicsNeMo-Curator's DomainMeshZarrSink. Validated on DrivAerML: GeoTransolver surface and volume both train end-to-end from converted zarr with losses matching the .pdmsh path (final train loss 0.1266 vs 0.1265 in a matched 5-epoch A/B) and samples bitwise-equivalent per-cell geometry, fields, and metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone scripts (not ASV; they need dataset generation, page-cache eviction, and multi-worker loops) comparing .pmsh/.pdmsh memmap reading against the zarr mesh schema: - benchmark_mesh_vs_zarr.py: reader-level, synthetic or real DrivAerML (--real-from), cold/warm, multi-worker, zarr-python vs tensorstore, compressed vs raw, plus a relaid-out pmsh control separating layout effects from format effects. - benchmark_recipe_pipeline_mesh_vs_zarr.py: the unified aero recipe's full datapipe (reader + transform stack -> DomainMesh) for both formats, with a GeoTransolver fwd+bwd step time as the prefetch-hiding threshold. - profile_recipe_stages.py: per-stage attribution (read / H2D / each transform / collate / model) mirroring MeshDataset order, with a zarr reader-internals breakdown. Headline results (8 DrivAerML runs, 200k-cell subsample, Lustre): zarr +tensorstore up to 6.6x cold / 3x warm reader throughput vs memmap at 8 workers; ~2.5x through the recipe DataLoader; ~17% faster training epochs and ~40% faster validation at 1 GPU where prefetch hides most of the gap. Stage profile: the entire format difference lives in the read stage (58ms vs 230ms warm); GPU-side transforms are negligible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harden the mesh-zarr schema before it gains external consumers: - MESH_ZARR_SCHEMA.md: normative spec for the mesh and DomainMesh schemas, versioning policy (readers reject newer schema_version), and explicitly reserved v2 evolutions (offsets-based ragged/mixed-element connectivity per VTKHDF/UGRID practice; connectivity dtype downcast). - validate_mesh_zarr(): schema conformance checker -- format/version, required arrays/attrs, attr/shape consistency, field lengths, and a sampled truthfulness check of layout="soup" claims. - Writers accept zarr StoreLike objects in addition to paths, keeping them forward-compatible with object stores and transactional stores (e.g. Icechunk) without an API change. - Readers enforce the version policy (clear error instead of misreading newer stores). - Docs: readers API .rst entries for the new readers and utilities; CHANGELOG entry.
Greptile SummaryAdds a complete zarr storage path for mesh-native CAE pipelines:
Important Files Changed
Reviews (1): Last reviewed commit: "feat(datapipes): mesh-zarr schema spec, ..." | Re-trigger Greptile |
|
Re-verified the dataloader numbers on exactly the code in this PR (the table in the description was measured on an earlier iteration of the branch). Same protocol: recipe's
Slightly better than the description's table (18.7 → 23.6 warm): the final reader includes the soup fast path (verified |
peterdsharpe
left a comment
There was a problem hiding this comment.
Deep review done locally (worktree checkout of c7ca4787), with empirical probes run against this branch rather than desk-checking alone. Overall: the code quality is high, and several design choices are exactly right — the schema versioning policy (reject-newer rather than best-effort), the machine-verified layout attr, the collision-checked read-time global_data merge (a big cleanup over globbing into _tensordict/ internals), and store-object writer support. Tests are well-designed for what they cover.
Two blocking themes, detailed in the inline comments:
1. Nested-TensorDict and key-name semantics (the blocker). Mesh's data containers are TensorDicts, where any value can be another TensorDict — and the .pmsh memmap path round-trips that today. The v1 schema/writer/reader assume exactly-one-level-flat dicts of tensors. Probed on this branch: nested TensorDicts crash the writer; a field key containing / (plausible in CFD exports, handled fine by .pmsh) writes successfully, reads back silently missing, and passes validate_mesh_zarr; NonTensorData crashes the writer. The clean fix is small: map the TensorDict tree to a zarr group tree recursively (zarr is hierarchical, like HDF5) — no flattening, so the whole key-collision class vanishes by construction, and the special-cased per-subgroup loops plus prefix-string plumbing collapse into one recursion. If v1 is to stay flat instead, it needs write-time validation with clear errors, reader/validator detection of unexpected subgroups, and the restriction stated in MESH_ZARR_SCHEMA.md (the spec is what PhysicsNeMo-Curator's independent writer implements against).
2. The performance story needs the controls the harness already has. The benchmark thoughtfully builds a pmsh_soup control store precisely to separate layout effects from format/reader effects — but its numbers appear nowhere (PR description, follow-up comment, gist). The claimed speedup conflates four mechanisms (soup layout; skipping unique() compaction; tensorstore concurrency; compression + buffered-vs-mmap cold I/O), and only the last is inherently "zarr". Running this PR's own synthetic benchmark (layout-controlled: soup on both sides) on local NVMe: memmap wins warm single-worker vs the shipped default backend (33.1 vs 25.8 samples/s, zarr-python+zstd), while zarr wins cold, multi-worker, and via tensorstore. That's consistent with the Lustre numbers being real — but the honest pitch is "zarr wins cold reads, parallel loaders, and network/cloud storage", and the pmsh_soup numbers on DrivAerML/Lustre are the evidence that decides how much a pure re-curation of .pdmsh would have captured. A sentence positioning zarr against TensorDict's own HDF5 path (to_h5/PersistentTensorDict: nested-native, single file per case) would also preempt the obvious question.
On size: the library footprint is ~1,150 lines, not 2,900 — benchmarks are ~1,000 and tests 410. I'd take up the offer to split the benchmarks into a follow-up PR. Worth a maintainer conversation: this adds the repo's third and fourth zarr access implementations (ZarrReader, TensorStoreZarrReader exist); the private backend pair here is small and justified by the dependent-read pattern, but a shared backend abstraction would trim ~100 lines and unify tensorstore spec handling. If the recursive rewrite happens, save_mesh_to_zarr becomes "walk the tensorclass fields", which argues for it living next to Mesh.save/Mesh.load in the mesh io layer, with only the readers in datapipes.
Smaller items inline: set_epoch seed drift (inherited from MeshReader — verified sequential-vs-resume divergence), the indexed (non-soup) subsample path has zero test coverage (I probed it on both backends — it is correct, but it's the trickiest code in the file), ZarrMeshReader never checks the format attr, first-case-only boundary_names, tensorstore backend hardcodes zarr v3 while discovery accepts v2 stores, and readers/validator are filesystem-only while writers accept StoreLike.
Negin — per our conversation, I'll push fixes for the review items directly to this branch, starting with the nested-TensorDict/key-semantics blocker (recursive TensorDict-tree ↔ zarr-group-tree mapping, plus the spec updates in MESH_ZARR_SCHEMA.md), then the smaller items (set_epoch drift, format checks, tests for the indexed path). The inline comments document the rationale; happy to discuss any of them before or after the commits land.
| for fully contiguous reads. A ``pmsh soup`` control store (same layout, | ||
| memmap format) separates layout effects from format/reader effects. |
There was a problem hiding this comment.
This control is exactly the right experimental design — and its numbers are reported nowhere (PR description, the re-verified follow-up comment, or the gist). Please publish the pmsh soup variant's results from the DrivAerML/Lustre runs. They're the datum that decides how much of the memmap-vs-zarr gap is the storage format versus the soup re-layout that the zarr conversion also performs.
For calibration, I ran this script's synthetic mode (layout-controlled: soup on both sides) on local NVMe, 8×500k-cell samples: memmap wins warm single-worker against the shipped default backend (33.1 vs 25.8 samples/s, zarr-python+zstd); zarr wins cold (19.6-39.2 vs 8.0), multi-worker (memmap degrades to 20.1 at 8 threads), and via tensorstore (up to 116). So the honest headline is "zarr wins cold reads, parallel loaders, and network storage" — not a flat 3-6×. The four mechanisms in the reported speedup (soup layout, skipping unique() compaction, tensorstore concurrency, compression + buffered-vs-mmap cold I/O) deserve to be separated in the writeup, since the first two are available to the memmap path with no format change.
Related: the PR should say a sentence about why zarr over TensorDict's own HDF5 path (to_h5/PersistentTensorDict) — which handles nested TensorDicts natively, stores one file per case (friendlier to Lustre metadata than many chunk files, absent v3 sharding), and does chunked+compressed slice reads. The genuine zarr advantages (object-store-native access, curator-side parallel writes, tensorstore async reads) are worth stating explicitly so the format choice is on the record.
Map the TensorDict tree to a zarr group tree recursively: nested
TensorDicts become nested groups (matching what .pmsh already round-trips)
instead of crashing the writer. Field names containing '/' (zarr's path
separator) previously wrote implicit hierarchy that read back silently
missing and passed validation; they are now rejected at write time, as are
non-tensor leaves (NonTensorData; attrs encoding reserved for schema v2).
The validator checks leaf arrays at any depth, so a store it blesses is
actually readable. The spec now defines field-name/nesting rules and pins
zarr format 3.
Robustness follow-ups from review: ZarrMeshReader rejects DomainMesh
groups with a subpath= hint instead of KeyError('points'); zarr format 2
stores fail with a clear error instead of opaquely inside the tensorstore
backend; ZarrDomainMeshReader verifies per-case boundary_names instead of
silently dropping boundaries a later case adds; set_epoch reseeds from a
captured base seed so checkpoint resume reproduces the same subsample
blocks; to_cell_soup gathers point_data via TensorDict batch-indexing
(nested-safe, and simpler).
Tests: nested round-trip on both backends, nested subsample slicing,
nested case-level global_data merge, '/'-key and NonTensorData rejection,
the previously-uncovered indexed (dependent-read) subsample path, set_epoch
resume equivalence, format mismatch, heterogeneous boundaries, v2
rejection, and nested-leaf validation.
manual_seed(initial_seed() + epoch) drifts: manual_seed updates initial_seed, so the epoch->seed mapping depended on call history (sequential epochs 1..5 from base 42 gave seeds 43,45,48,52,57; resuming directly at epoch 5 gave 47), silently changing subsample blocks on checkpoint resume. Capture the base seed once in set_generator and reseed as base + epoch. Same fix as the zarr mesh readers, which inherited this code; behavior is pinned by test_set_epoch_resume_reproducible.
The benchmark docstring claimed the library zarr mesh reader does not exist -- stale within the PR that ships it; reworded to say the prototypes predate and motivated it. The converter's run-directory sort key crashed with an unguarded ValueError on any directory not named run_<int>; numeric runs still sort numerically, everything else falls back to name order.
|
Pushed the first batch of review fixes directly to the branch (as discussed):
All 36 |
This reverts commit 79d250795a4a4a3c7853a3873da70a80e79e94f4. Superseded by NVIDIA#1742, which rewrites the .pmsh readers' RNG scheme entirely (per-(epoch, index) SeedSequence-derived generators -- both resume-reproducible and order-independent) and is about to merge. Dropping this PR's copy of the fix removes the readers/mesh.py overlap so the two PRs no longer conflict in either merge order. The zarr readers added by this PR keep their own resume-reproducible set_epoch; porting them to NVIDIA#1742's per-index scheme is deferred until NVIDIA#1742 lands (it depends on _rng helpers introduced there).
ruff-format the two files the earlier fix commits touched and resolve a pre-existing PERF401 in the benchmark's single-worker timing loop, so pre-commit runs clean on the branch.
for more information, see https://pre-commit.ci
|
One scope proposal to close out my review (the two open threads above both point here): 1. Pull the benchmarks (~1,000 lines) into a separate PR — as you already offered in the description; +1 on doing it. 2. Pull the unified-aero-recipe changes ( That would leave this PR as the clean core: library + schema + validator + tests + docs (~1,600 lines). Two small consequences if you take this: the CHANGELOG line mentioning the recipe configs would trim down, and the description's training A/B evidence should note the configs it references are coming in the follow-up (the evidence itself stands). Happy to help with the extraction if useful. |
…-up PRs Per review: the benchmark suite (~1,060 lines) and the unified-aero recipe zarr configs + .pdmsh converter (~347 lines) move to dedicated follow-up PRs, leaving this PR as the library core: readers, writers, schema spec, validator, tests, and docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mantics Align the sink with the schema-v1 contract tightened during review of NVIDIA/physicsnemo#1798 (MESH_ZARR_SCHEMA.md): - Write data subgroups (point_data/cell_data/global_data, incl. the case-level global_data at the DomainMesh root) via a recursive _write_tensordict helper: tensor leaves become arrays, nested TensorDicts become nested zarr groups, at any depth. - Reject empty field names and names containing '/' (zarr path separator) at write time instead of silently creating hierarchy. - Reject non-tensor leaves (NonTensorData/strings) at write time; an attrs-based encoding is reserved for schema v2. - Pin zarr_format=3 explicitly so a zarr.config default override cannot produce a format-2 store. - _to_cell_soup: gather point_data via TensorDict batch indexing (handles nested leaves, fixes ruff SIM118). Tests: nested-TensorDict group structure, '/'-name and NonTensorData rejection (with atomic-write cleanup), verified-soup layout attr, cells dtype preservation, zarr-format-3 + plain-JSON attrs, and a cross-repo check that physicsnemo's validate_mesh_zarr accepts sink output (skips when unavailable, like the reader round-trip test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@peterdsharpe done! |
|
The
The relayout alone is not the win. Soup on memmap is slower when warm than what ships today ( The win comes from the format:
Re-curating The NVMe result reflects the same mechanics in reverse: the page cache leaves compression little I/O to save, while a single worker leaves concurrency little opportunity to help. |
- MESH_ZARR_SCHEMA.md: add 'text' language to schema-tree code fences (MD040) - CHANGELOG.md: wrap mesh-zarr entry to 88 chars (MD013) - .importlinter: exclude datapipes.readers.zarr_mesh from the external-imports contract — zarr is an optional dependency imported lazily via OptionalImport, but grimp flags the nested 'from zarr.codecs import BloscCodec' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| :members: | ||
| :show-inheritance: | ||
|
|
||
| Mesh-zarr schema utilities |
There was a problem hiding this comment.
| Mesh-zarr schema utilities | |
| Mesh-Zarr Schema Utilities |
| PhysicsNeMo-Curator's `DomainMeshZarrSink`; consumed by `ZarrMeshReader` / | ||
| `ZarrDomainMeshReader`. | ||
|
|
||
| ## Mesh group (`format = "physicsnemo-mesh-zarr"`) |
There was a problem hiding this comment.
| ## Mesh group (`format = "physicsnemo-mesh-zarr"`) | |
| ## Mesh Group (`format = "physicsnemo-mesh-zarr"`) |
| | `cell_data/<f>` | `(n_cells, ...)` | array or subgroup (see below) | | ||
| | `global_data/<f>` | any (typically 0-d) | array or subgroup (see below) | | ||
|
|
||
| ### Field names and nesting |
There was a problem hiding this comment.
| ### Field names and nesting | |
| ### Field Names and Nesting |
|
|
||
| ### Field names and nesting | ||
|
|
||
| Data fields mirror the `TensorDict` tree: each `<f>` under `point_data` / |
There was a problem hiding this comment.
| Data fields mirror the `TensorDict` tree: each `<f>` under `point_data` / | |
| Data fields mirror the `TensorDict` tree. Each `<f>` under `point_data`, |
| ### Field names and nesting | ||
|
|
||
| Data fields mirror the `TensorDict` tree: each `<f>` under `point_data` / | ||
| `cell_data` / `global_data` is either an **array** (a tensor leaf) or a |
There was a problem hiding this comment.
| `cell_data` / `global_data` is either an **array** (a tensor leaf) or a | |
| `cell_data`, or `global_data` is an *array* (a tensor leaf) or a |
Co-authored-by: megnvidia <mmiranda@nvidia.com>
Co-authored-by: megnvidia <mmiranda@nvidia.com>
Co-authored-by: megnvidia <mmiranda@nvidia.com>
Co-authored-by: megnvidia <mmiranda@nvidia.com>
Co-authored-by: megnvidia <mmiranda@nvidia.com>
There was a problem hiding this comment.
I notice that both the DomainMesh save utility and load utility are in physicsnemo/datapipes/readers/zarr_mesh.py.
@coreyjadams will probably have stronger opinions (as codeowner for datapipes), but my impression was that readers/ was only for load utilities. One API design option: you could put this in physicsnemo/mesh/io/ instead? Perhaps a more natural fit - there we have from_pyvista() and to_pyvista() already (both from VTK-family formats, basically), so this would slot right in. What do you think @negin513?
coreyjadams
left a comment
There was a problem hiding this comment.
Hi @negin513 - thanks for working on this PR. In general I am not opposed to this backend for mesh. But I think we have some design changes needed here before this can merge.
First, I think the logic for mesh-zarr backend needs to deploy at mesh/io/zarr.py. It's important if we have zarr supported that it be supported with or without datapipes, here, from a software design. Peter mentioned that the writing utilities shouldn't be in the datapipes but I'd actually think we should put the reading and writing utilites as much as reasonably possible into mesh/io/zarr.py and the MeshReader and DomainMesh reader can tap into that as a backend instead. Then, swapping backends at user recipe time becomes a very simple config change even.
So, abstractly, datapipes/readers/mesh.py might get an update of Mesh.load(mesh_path, backend="zarr") (and other associated settings at constructor time) and similarly for domain mesh reader.
Overall, I think the integration logic of a zarr backend becomes a lot cleaner to datapipes and every other consumer too if we contain it to just mesh.
For testing, I haven't actually looked closely at the tests, but we should consider several components:
- If we ship multiple IO backends, the
saveandloadmethods should default tomemmapandautocorrespondingly, and we should test that those deliver as promised. - The tests should validate that `load(...., backend="auto") successfully identifies and deserializes .pmsh and .pdmsh files of all supported backends.
- We should have really good "round trip" testing: make a synthetic file, write it to both formats, load it with "auto", check that it's identical when restored. Do this across a suite of dimensions / .pmsh / .pdmsh though no need to go crazy of course?
- @peterdsharpe what other tests can tighten up the Mesh IO to help prevent user facing churn?
Overall, my top priority is making sure we don't introduce churn into the mesh dataset ecosystem, even though its admittedly very young.
|
Thanks Corey! I'm actually fully on board with where you want this to end up —mesh/io/zarr.py owning the format, Mesh.save(backend="memmap"|"zarr"), load with backend="auto", and the round-trip tests you described. That's what I proposed to you earlier. Where I'd push back a little is on sequencing. I'd rather merge this PR as-is and do the mesh/io move as the immediate next PR, and here's why I think that's actually the safer path... FWIW, this is also how we've been operating on this PR already — when Peter asked, we split the converter, recipe, and benchmarks out to #1828/#1829. So "follow-up PR" isn't a promise that goes to die in the backlog; it's the pattern this work has actually followed.... That said, if you feel strongly it has to happen in this PR, I'll do it back again. I will defer @coreyjadams and @peterdsharpe to finish this PR per your design of choice. |
|
@negin513 I think that moving this interface to |
|
Let's see what happens here: pytorch/tensordict#1754 |
|
On placement, this structure follows the existing codebase convention rather than departing from it. Every on-disk format reader in PhysicsNeMo currently lives under By contrast, For that reason, I’m open to that direction, but I don’t think this PR should be gated on a broader package reorganization. My preference would be to land this consistently with the current layout, then address the |
|
@peterdsharpe and @coreyjadams I trust your judgment on this PR and will defer this to you. Please feel free to push the proposed structure to this PR. @peterdsharpe thanks for the tensordict issue. This would be really cool. |
Description
Adds a chunked, compressed, cloud-capable zarr storage path for mesh-native CAE pipelines, bridging the previously disjoint
physicsnemo.meshmemmap (.pmsh/.pdmsh) and zarr data stacks with a documented, versioned schema and drop-in readers. No changes to models, transforms, collate, or training loops — the readers honor the existing(Mesh, metadata)/(DomainMesh, metadata)contracts.What's included
Library (
physicsnemo/datapipes/readers/zarr_mesh.py)save_mesh_to_zarr/save_domain_mesh_to_zarr: reference writers for thephysicsnemo-mesh-zarr/physicsnemo-domainmesh-zarrschemas (spec:physicsnemo/datapipes/MESH_ZARR_SCHEMA.md). DomainMesh groups mirror the.pdmshtree with case-levelglobal_datasingle-sourced at the root. Writers accept paths or zarr StoreLike objects.ZarrMeshReader:(Mesh, metadata)per sample;subpath=navigation into DomainMesh groups;merge_global_data_from=read-time metadata merge (collision-checked); contiguous-block subsampling with chunk-aligned I/O; zarr-python and tensorstore backends.ZarrDomainMeshReader:(DomainMesh, metadata)per case for volume pipelines; per-submesh subsample selection;full_resolution_boundaries=for exact-geometry consumers (SDF).to_cell_soup+ machine-verifiedlayoutattr: opt-in per-cell vertex-soup curation for cell-centric training; soup groups skip thecellsread entirely and issue all array reads concurrently.validate_mesh_zarr: schema conformance checker; readers reject stores written by a newerschema_version.Recipe (
examples/.../unified_external_aero_recipe)drivaer_ml_surface_zarr.yaml/drivaer_ml_volume_zarr.yaml: differ from their.pdmshtwins only in thereader:block; the full transform stack and all five recipe models run unchanged.src/convert_pdmsh_to_zarr.py: transitional CLI for datasets already curated to.pdmsh(new curation should emit the schema directly from PhysicsNeMo-Curator; companion PR: feat(mesh): DomainMeshZarrSink emitting the physicsnemo mesh-zarr schema physicsnemo-curator#64).Benchmarks (
benchmarks/physicsnemo/datapipes/): the reader-level, recipe-pipeline, and per-stage profiling scripts behind the numbers below (happy to split into a follow-up PR if preferred).Performance (DrivAerML, 8 runs of 15-20M cells, 200k-cell subsample, Lustre)
.pdmsh/memmapbenchmark_io, cold / warm batches/s)At 1 GPU, prefetch hides most of the loader gap (~17% faster training epochs, ~40% faster validation); the full loader ratio re-emerges for cold epochs, larger-than-cache datasets, and multi-GPU nodes. Stores are ~6% smaller than
.pdmsh(zstd) while additionally embedding the STL geometry in-group. Per-stage profiling shows the entire format difference is in the read stage (230 ms vs 58 ms warm); GPU-side transforms are unaffected.Correctness
.pdmshon real data: per-cell geometrypoints[cells], all cell fields, and mergedglobal_dataexact; 166M-point volume interior verified by shape + random 1M-point probes..pdmsh) vs 0.1265 (zarr); volume — 0.2648 vs 0.2680 train / 0.0998 vs 0.0987 val at epoch 3; trajectories track epoch-for-epoch within subsample-RNG noise.train.py(volume incl. on-the-fly SDF against the in-group full-resolution STL boundary).Scope notes
The soup layout is an opt-in curation choice for cell-centric consumers, recorded as a verified
layoutattr; connectivity-dependent consumers (GNNs) should curatelayout: indexedgroups, which the schema and readers support. Version-1 schema requires fixed-width cells; an offsets-based ragged/mixed-element encoding is explicitly reserved for v2 in the spec (readers reject newer versions rather than misread).Checklist
test/datapipes/readers/test_zarr_mesh.py; 262 datapipes core+readers tests pass)..rst,MESH_ZARR_SCHEMA.md).Dependencies
None new:
zarrwas already optional (OptionalImport);tensorstoreoptional with automatic zarr-python fallback.