Skip to content

Commit ad27f75

Browse files
committed
Begin investigation for sharded ingestion
1 parent 73ab646 commit ad27f75

9 files changed

Lines changed: 768 additions & 17 deletions

File tree

docs/data_models/chunk_grids.md

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717

1818
The variables in MDIO data model can represent different types of chunk grids.
1919
These grids are essential for managing multi-dimensional data arrays efficiently.
20-
In this breakdown, we will explore four distinct data models within the MDIO schema,
20+
In this breakdown, we will explore the chunk grid data models within the MDIO schema,
2121
each serving a specific purpose in data handling and organization.
2222

2323
MDIO implements data models following the guidelines of the Zarr v3 spec and ZEPs:
2424

2525
- [Zarr core specification (version 3)](https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html)
2626
- [ZEP 1 — Zarr specification version 3](https://zarr.dev/zeps/accepted/ZEP0001.html)
27+
- [ZEP 2 — Sharding codec](https://zarr.dev/zeps/accepted/ZEP0002.html)
2728
- [ZEP 3 — Variable chunking](https://zarr.dev/zeps/draft/ZEP0003.html)
2829

2930
## Regular Grid
@@ -126,6 +127,100 @@ conceptual and visually not to scale.
126127
└──────────┴─────┴───────┴───┘
127128
```
128129

130+
## Sharded Grid
131+
132+
The [ShardedChunkGrid](ShardedChunkGrid) model implements Zarr v3 sharding, which
133+
organizes data in a two-level hierarchy for improved I/O performance:
134+
135+
- **Shards**: Outer containers stored as individual files/objects
136+
- **Chunks**: Inner units within each shard for compression and fine-grained access
137+
138+
Sharding is particularly beneficial for cloud storage where reducing the number of
139+
objects improves performance while still maintaining efficient partial reads.
140+
141+
```{eval-rst}
142+
.. autosummary::
143+
ShardedChunkGrid
144+
ShardedChunkShape
145+
```
146+
147+
:::{note}
148+
Sharding is only supported with Zarr v3 format. The `shardShape` must be evenly
149+
divisible by `chunkShape` along all dimensions.
150+
:::
151+
152+
:::{important}
153+
**Non-shardable dtype limitation**: Sharding is only applied to simple scalar-type
154+
variables (e.g., `amplitude`, `cdp_x`, `cdp_y`). Variables with structured dtypes
155+
(e.g., `headers`) or void/bytes types (e.g., `raw_headers`) will automatically use
156+
regular chunking instead, as Zarr's sharding codec does not support these dtypes.
157+
However, these variables will use the **shard shape as their chunk shape** to maintain
158+
consistent I/O patterns across all variables.
159+
:::
160+
161+
For a 2D array with shape `rows, cols = (256, 512)`{l=python}, we can configure
162+
sharding with shard size `(128, 128)` and chunk size `(32, 32)`:
163+
164+
`{ "name": "sharded", "configuration": { "shardShape": [128, 128], "chunkShape": [32, 32] } }`{l=json}
165+
166+
This creates a two-level structure where each shard contains multiple chunks:
167+
168+
```bash
169+
←────────── 128 ──────────→ ←────────── 128 ──────────→ ...
170+
┌─────────────────────────────────────────────────────────┐
171+
│ ┌─────┬─────┬─────┬─────┐ ┌─────┬─────┬─────┬─────┐ │ ↑
172+
│ │ 32 │ 32 │ 32 │ 32 │ │ 32 │ 32 │ 32 │ 32 │ │ │
173+
│ ├─────┼─────┼─────┼─────┤ ├─────┼─────┼─────┼─────┤ │ │
174+
│ │ 32 │ 32 │ 32 │ 32 │ │ 32 │ 32 │ 32 │ 32 │ │ 128
175+
│ ├─────┼─────┼─────┼─────┤ ├─────┼─────┼─────┼─────┤ │ │
176+
│ │ 32 │ 32 │ 32 │ 32 │ │ 32 │ 32 │ 32 │ 32 │ │ │
177+
│ ├─────┼─────┼─────┼─────┤ ├─────┼─────┼─────┼─────┤ │ │
178+
│ │ 32 │ 32 │ 32 │ 32 │ │ 32 │ 32 │ 32 │ 32 │ │ ↓
179+
│ └─────┴─────┴─────┴─────┘ └─────┴─────┴─────┴─────┘ │
180+
│ Shard (0,0) Shard (0,1) │
181+
├─────────────────────────────────────────────────────────┤
182+
│ Shard (1,0) Shard (1,1) ... │
183+
└─────────────────────────────────────────────────────────┘
184+
```
185+
186+
### Using Sharding with Templates
187+
188+
By default, MDIO templates do not use sharding. To enable sharding on a template,
189+
set both `full_chunk_shape` and `full_shard_shape`:
190+
191+
```python
192+
from mdio.builder.templates.seismic_3d_poststack import Seismic3DPostStackTemplate
193+
194+
# Create template with default chunking (no sharding)
195+
template = Seismic3DPostStackTemplate(data_domain='depth')
196+
print(template.full_shard_shape) # None - sharding disabled
197+
198+
# Enable sharding by setting both chunk and shard shapes
199+
template.full_chunk_shape = (32, 32, 128) # Inner chunk sizes
200+
template.full_shard_shape = (128, 128, 512) # Outer shard sizes
201+
202+
# Build dataset - will use ShardedChunkGrid for scalar variables (amplitude, coordinates)
203+
# Note: headers array will use RegularChunkGrid (structured dtypes don't support sharding)
204+
dataset = template.build_dataset('Seismic3D', sizes=(256, 512, 1024))
205+
```
206+
207+
### Ingestion with Sharding
208+
209+
When sharding is enabled, the SEG-Y ingestion process automatically iterates at the
210+
shard level instead of the chunk level. This reduces the number of I/O operations
211+
and improves performance, especially for cloud storage backends.
212+
213+
- **With sharding**: Ingestion iterates over shards (larger units)
214+
- **Without sharding**: Ingestion iterates over chunks (default behavior)
215+
216+
During ingestion, sharding is applied only to simple scalar-type arrays (e.g., `amplitude`).
217+
Arrays with non-shardable dtypes use regular chunking with the shard shape as their chunk
218+
size. For example, with `full_chunk_shape=(32, 32, 32)` and `full_shard_shape=(256, 256, 256)`:
219+
220+
- `amplitude` array: chunks=(32, 32, 32), shards=(256, 256, 256)
221+
- `headers` array (structured dtype): chunks=(256, 256), no sharding
222+
- `raw_headers` array (void/bytes dtype): chunks=(256, 256), no sharding
223+
129224
## Model Reference
130225

131226
:::{dropdown} RegularChunkGrid
@@ -152,3 +247,15 @@ conceptual and visually not to scale.
152247
```
153248

154249
:::
250+
:::{dropdown} ShardedChunkGrid
251+
:animate: fade-in-slide-down
252+
253+
```{eval-rst}
254+
.. autopydantic_model:: ShardedChunkGrid
255+
256+
----------
257+
258+
.. autopydantic_model:: ShardedChunkShape
259+
```
260+
261+
:::

src/mdio/builder/schemas/chunk_grid.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from pydantic import Field
6+
from pydantic import model_validator
67

78
from mdio.builder.schemas.core import CamelCaseStrictModel
89

@@ -22,6 +23,53 @@ class RectilinearChunkShape(CamelCaseStrictModel):
2223
)
2324

2425

26+
class ShardedChunkShape(CamelCaseStrictModel):
27+
"""Represents sharded chunk configuration for Zarr v3.
28+
29+
In Zarr v3 sharding, data is organized in a two-level hierarchy:
30+
- Shards: Outer containers that are stored as individual files/objects
31+
- Chunks: Inner units within each shard for compression and access
32+
33+
The shard_shape must be evenly divisible by chunk_shape along each dimension.
34+
"""
35+
36+
shard_shape: tuple[int, ...] = Field(
37+
...,
38+
description="Lengths of the shard (outer container) along each dimension of the array.",
39+
)
40+
41+
chunk_shape: tuple[int, ...] = Field(
42+
...,
43+
description="Lengths of the chunk (inner unit) along each dimension within each shard.",
44+
)
45+
46+
@model_validator(mode="after")
47+
def validate_shapes(self) -> ShardedChunkShape:
48+
"""Validate that shard_shape is divisible by chunk_shape."""
49+
if len(self.shard_shape) != len(self.chunk_shape):
50+
msg = (
51+
f"shard_shape and chunk_shape must have the same number of dimensions. "
52+
f"Got shard_shape={self.shard_shape} and chunk_shape={self.chunk_shape}"
53+
)
54+
raise ValueError(msg)
55+
56+
for i, (shard_dim, chunk_dim) in enumerate(zip(self.shard_shape, self.chunk_shape, strict=True)):
57+
if shard_dim % chunk_dim != 0:
58+
msg = (
59+
f"shard_shape must be evenly divisible by chunk_shape along all dimensions. "
60+
f"Dimension {i}: shard_shape[{i}]={shard_dim} is not divisible by chunk_shape[{i}]={chunk_dim}"
61+
)
62+
raise ValueError(msg)
63+
if shard_dim < chunk_dim:
64+
msg = (
65+
f"shard_shape must be >= chunk_shape along all dimensions. "
66+
f"Dimension {i}: shard_shape[{i}]={shard_dim} < chunk_shape[{i}]={chunk_dim}"
67+
)
68+
raise ValueError(msg)
69+
70+
return self
71+
72+
2573
class RegularChunkGrid(CamelCaseStrictModel):
2674
"""Represents a rectangular and regularly spaced chunk grid."""
2775

@@ -36,3 +84,16 @@ class RectilinearChunkGrid(CamelCaseStrictModel):
3684
name: str = Field(default="rectilinear", description="The name of the chunk grid.")
3785

3886
configuration: RectilinearChunkShape = Field(..., description="Configuration of the irregular chunk grid.")
87+
88+
89+
class ShardedChunkGrid(CamelCaseStrictModel):
90+
"""Represents a sharded chunk grid for Zarr v3.
91+
92+
Sharding enables efficient storage and access patterns by grouping multiple
93+
chunks into larger shards. Each shard is stored as a single object/file,
94+
reducing the number of I/O operations for sequential access patterns.
95+
"""
96+
97+
name: str = Field(default="sharded", description="The name of the chunk grid.")
98+
99+
configuration: ShardedChunkShape = Field(..., description="Configuration of the sharded chunk grid.")

src/mdio/builder/schemas/v1/variable.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from mdio.builder.schemas.base import NamedArray
1919
from mdio.builder.schemas.chunk_grid import RectilinearChunkGrid
2020
from mdio.builder.schemas.chunk_grid import RegularChunkGrid
21+
from mdio.builder.schemas.chunk_grid import ShardedChunkGrid
2122
from mdio.builder.schemas.core import CamelCaseStrictModel
2223
from mdio.builder.schemas.dtype import ScalarType
2324
from mdio.builder.schemas.v1.stats import SummaryStatistics
@@ -34,7 +35,7 @@ class CoordinateMetadata(CamelCaseStrictModel):
3435
class VariableMetadata(CoordinateMetadata):
3536
"""Complete Metadata for Variables and complex or large Coordinates."""
3637

37-
chunk_grid: RegularChunkGrid | RectilinearChunkGrid | None = Field(
38+
chunk_grid: RegularChunkGrid | RectilinearChunkGrid | ShardedChunkGrid | None = Field(
3839
default=None,
3940
description="Chunk grid specification for the array.",
4041
)

src/mdio/builder/templates/base.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from mdio.builder.schemas import compressors
1414
from mdio.builder.schemas.chunk_grid import RegularChunkGrid
1515
from mdio.builder.schemas.chunk_grid import RegularChunkShape
16+
from mdio.builder.schemas.chunk_grid import ShardedChunkGrid
17+
from mdio.builder.schemas.chunk_grid import ShardedChunkShape
1618
from mdio.builder.schemas.dtype import ScalarType
1719
from mdio.builder.schemas.dtype import StructuredType
1820
from mdio.builder.schemas.v1.units import AllUnitModel
@@ -43,6 +45,7 @@ def __init__(self, data_domain: SeismicDataDomain) -> None:
4345
self._physical_coord_names: tuple[str, ...] = ()
4446
self._logical_coord_names: tuple[str, ...] = ()
4547
self._var_chunk_shape: tuple[int, ...] = ()
48+
self._var_shard_shape: tuple[int, ...] | None = None # Default to no sharding
4649

4750
self._builder: MDIODatasetBuilder | None = None
4851
self._dim_sizes: tuple[int, ...] = ()
@@ -59,6 +62,7 @@ def __repr__(self) -> str:
5962
f"physical_coord_names={self._physical_coord_names}, "
6063
f"logical_coord_names={self._logical_coord_names}, "
6164
f"var_chunk_shape={self._var_chunk_shape}, "
65+
f"var_shard_shape={self._var_shard_shape}, "
6266
f"dim_sizes={self._dim_sizes}, "
6367
f"units={self._units})"
6468
)
@@ -196,6 +200,41 @@ def full_chunk_shape(self, shape: tuple[int, ...]) -> None:
196200

197201
self._var_chunk_shape = shape
198202

203+
@property
204+
def full_shard_shape(self) -> tuple[int, ...] | None:
205+
"""Returns the shard shape for the variables, or None if sharding is disabled."""
206+
if self._var_shard_shape is None:
207+
return None
208+
209+
# If dimension sizes are not set yet, return the stored shape as-is
210+
if len(self._dim_sizes) != len(self._dim_names):
211+
return self._var_shard_shape
212+
213+
# Expand -1 values to full dimension sizes
214+
return tuple(
215+
dim_size if shard_size == -1 else shard_size
216+
for shard_size, dim_size in zip(self._var_shard_shape, self._dim_sizes, strict=False)
217+
)
218+
219+
@full_shard_shape.setter
220+
def full_shard_shape(self, shape: tuple[int, ...] | None) -> None:
221+
"""Sets the shard shape for the variables, or None to disable sharding."""
222+
if shape is None:
223+
self._var_shard_shape = None
224+
return
225+
226+
if len(shape) != len(self._dim_names):
227+
msg = f"Shard shape {shape} has {len(shape)} dimensions, expected {len(self._dim_names)}"
228+
raise ValueError(msg)
229+
230+
# Validate that all values are positive integers or -1
231+
for shard_size in shape:
232+
if shard_size != -1 and shard_size <= 0:
233+
msg = f"Shard size must be positive integer or -1, got {shard_size}"
234+
raise ValueError(msg)
235+
236+
self._var_shard_shape = shape
237+
199238
@property
200239
@abstractmethod
201240
def _name(self) -> str:
@@ -274,6 +313,27 @@ def _add_coordinates(self) -> None:
274313
if "same name twice" not in str(exc):
275314
raise
276315

316+
def _create_chunk_grid(self, exclude_vertical: bool = False) -> RegularChunkGrid | ShardedChunkGrid:
317+
"""Create the appropriate chunk grid based on sharding configuration.
318+
319+
Args:
320+
exclude_vertical: If True, excludes the vertical (last) dimension from shapes.
321+
Used for headers and other non-sample variables.
322+
323+
Returns:
324+
RegularChunkGrid if sharding is disabled, ShardedChunkGrid if enabled.
325+
"""
326+
chunk_shape = self.full_chunk_shape[:-1] if exclude_vertical else self.full_chunk_shape
327+
shard_shape = self.full_shard_shape
328+
329+
if shard_shape is None:
330+
# No sharding - use regular chunk grid
331+
return RegularChunkGrid(configuration=RegularChunkShape(chunk_shape=chunk_shape))
332+
333+
# Sharding enabled - create sharded chunk grid
334+
shard_shape = shard_shape[:-1] if exclude_vertical else shard_shape
335+
return ShardedChunkGrid(configuration=ShardedChunkShape(shard_shape=shard_shape, chunk_shape=chunk_shape))
336+
277337
def _add_trace_mask(self) -> None:
278338
"""Add trace mask variables."""
279339
self._builder.add_variable(
@@ -286,7 +346,7 @@ def _add_trace_mask(self) -> None:
286346

287347
def _add_trace_headers(self, header_dtype: StructuredType) -> None:
288348
"""Add trace mask variables."""
289-
chunk_grid = RegularChunkGrid(configuration=RegularChunkShape(chunk_shape=self.full_chunk_shape[:-1]))
349+
chunk_grid = self._create_chunk_grid(exclude_vertical=True)
290350
self._builder.add_variable(
291351
name="headers",
292352
dimensions=self.spatial_dimension_names,
@@ -302,7 +362,7 @@ def _add_variables(self) -> None:
302362
A virtual method that can be overwritten by subclasses to add custom variables.
303363
Uses the class field 'builder' to add variables to the dataset.
304364
"""
305-
chunk_grid = RegularChunkGrid(configuration=RegularChunkShape(chunk_shape=self.full_chunk_shape))
365+
chunk_grid = self._create_chunk_grid(exclude_vertical=False)
306366
unit = self.get_unit_by_key(self._default_variable_name)
307367
self._builder.add_variable(
308368
name=self.default_variable_name,

0 commit comments

Comments
 (0)