Filesystem and Metadata Structure
A Sedpack dataset is stored in a dedicated root directory containing a
top-level dataset_info.json file and one subdirectory per populated split
(train, test, holdout). Both metadata and data files are organized as a
tree to scale to many examples without overwhelming the underlying filesystem
or requiring all metadata to be loaded into memory at once.
Directory Layout
Section titled “Directory Layout”File systems degrade in performance when a single directory contains too many
entries. When writing shards, PathGenerator (with defaults levels=4,
max_branching=1_000, name_length=10) creates 3 nested subdirectory levels of
10-character hexadecimal names (at most 1,000 entries per directory) and a
32-character hexadecimal shard filename at the leaf (when using
write_multiprocessing, each worker process also writes under its own
32-character UUID subdirectory inside the split):
tiny_aes_sedpack/├── dataset_info.json├── train/│ ├── shards_list.json│ └── 8a745b3853/│ ├── shards_list.json│ └── 412a5873e4/│ ├── shards_list.json│ └── 20fd9888f6/│ ├── shards_list.json│ ├── 062203904722443e82655d5b65650f68.fb│ └── 62b497f95ff84c1184fc7f85316967e8.fb├── test/│ └── ...└── holdout/ └── ...All file paths stored inside JSON metadata files are relative to the dataset
root directory and are validated to prevent directory traversal (..).
Metadata Hierarchy
Section titled “Metadata Hierarchy”Sedpack separates dataset-wide configuration from per-directory shard listings
using Pydantic models serialized as JSON. Custom
JSON-serializable metadata (custom_metadata) can be attached at three levels:
the dataset (Metadata), individual attributes (Attribute), and individual
shards (ShardInfo).
1. Root Configuration (dataset_info.json)
Section titled “1. Root Configuration (dataset_info.json)”The root dataset_info.json file (represented by DatasetInfo) is the entry
point when loading a dataset and always serializes three top-level sections:
metadata(Metadata): General information about the dataset:description: Human-readable description (default"").dataset_license: License string or URL.dataset_version: Semantic version of the dataset (default"1.0.0").download_from: Source URL for downloading the dataset (default"").custom_metadata: Arbitrary JSON-serializable dictionary (for example, citation info or target implementation in the TinyAES dataset).sedpack_version: Version of Sedpack used to create the dataset (checked on load to ensure the reader is not older than the dataset).
dataset_structure(DatasetStructure): Schema and storage parameters:saved_data_description: List ofAttributedefinitions (name,dtype,shape, andcustom_metadatasuch as global"min"and"max"trace values in TinyAES). Whendtypeis"bytes"or"str"andshapeis(), the attribute has variable size (returned asbytesorstr). Whendtypeis"bytes"andshapeis a 1D tuple(N,), the attribute has fixed lengthNbytes (can be written asbytesof lengthNor a 1D integer array/sequence of shape(N,)with values in[0, 255], and is returned asbytesof lengthN). Whenshard_file_typeis"fb", attributes are stored in this exact order.compression: Compression algorithm applied to shard files ("","BZ2","GZIP","LZMA","LZ4","ZIP","ZLIB","ZSTD").examples_per_shard: Maximum number of examples written to a single shard file before opening a new one (default256).shard_file_type: Storage format and file extension for shards ("fb","npz", or"tfrec").hash_checksum_algorithms: Tuple of hash algorithms (default("sha256",), also supportingmd5,sha1,sha224,sha384,sha512,sha3_*,xxh32,xxh64,xxh128) computed for every file.
splits(dict[SplitT, ShardListInfo]): Maps each populated split (train,test,holdout) to aShardListInfopointer referencing<split>/shards_list.json.
Example dataset_info.json (abbreviated from the TinyAES tutorial dataset):
{ "metadata": { "description": "SCAAML AES side-channel attacks tutorial", "dataset_license": "Apache License 2.0", "dataset_version": "1.0.0", "download_from": "", "custom_metadata": { "implementation": "TinyAES" }, "sedpack_version": "0.1.4" }, "dataset_structure": { "saved_data_description": [ { "name": "trace1", "dtype": "float16", "shape": [80000], "custom_metadata": {"min": -0.42, "max": 0.39} }, { "name": "key", "dtype": "uint8", "shape": [16], "custom_metadata": {} } ], "compression": "LZ4", "examples_per_shard": 256, "shard_file_type": "fb", "hash_checksum_algorithms": ["sha256"] }, "splits": { "train": { "shard_list_info_file": { "file_path": "train/shards_list.json", "hash_checksums": ["201a27e3..."] }, "number_of_examples": 512, "number_of_shards": 2 } }}2. Shard List Pointers (ShardListInfo)
Section titled “2. Shard List Pointers (ShardListInfo)”A ShardListInfo references a shards_list.json file without requiring it to
be read immediately:
shard_list_info_file(FileInfo): Relativefile_path(always namedshards_list.json) and itshash_checksums.number_of_examples: Total number of examples in the subtree rooted at thatshards_list.json.number_of_shards: Total number of shard files in the subtree.
Storing number_of_examples and number_of_shards in the pointer allows fast
counting and indexing without traversing or downloading child metadata files.
3. Directory Shard Lists (shards_list.json)
Section titled “3. Directory Shard Lists (shards_list.json)”Each directory in a split’s hierarchy contains a shards_list.json file
(represented by ShardsList and serialized with exclude_defaults=True, so
empty fields are omitted):
relative_path_self: Path to thisshards_list.jsonrelative to the dataset root.number_of_examples: Sum of examples across all shards in this directory and all its subdirectories.shard_files: List ofShardInfoobjects for shard files located directly in this directory (omitted in intermediate directories that only contain subdirectories).children_shard_lists: List ofShardListInfopointers toshards_list.jsonfiles in immediate subdirectories (omitted in leaf directories).
Example intermediate train/shards_list.json:
{ "relative_path_self": "train/shards_list.json", "number_of_examples": 512, "children_shard_lists": [ { "shard_list_info_file": { "file_path": "train/8a745b3853/shards_list.json", "hash_checksums": ["c909e2cc..."] }, "number_of_examples": 512, "number_of_shards": 2 } ]}When writing examples concurrently or across multiple processes, each writer
writes into its own directory subtree (<split>/<relative_path_from_split>/...)
and merges its local shards_list.json files up to
<split>/<relative_path_from_split>/shards_list.json on exit. Calling
dataset.write_config(updated_infos=...) on the coordinator then runs
merge_shard_infos to merge each worker subtree up to
<split>/shards_list.json and dataset_info.json (see
Parallel and Distributed Writing below).
4. Individual Shard Metadata (ShardInfo)
Section titled “4. Individual Shard Metadata (ShardInfo)”Each ShardInfo entry in a leaf shards_list.json describes one shard:
file_infos: Tuple ofFileInfoobjects (file_pathrelative to the dataset root andhash_checksums) for the shard file(s).number_of_examples: Number of examples stored in this shard.custom_metadata: Optional JSON-serializable dictionary attached to the shard (omitted when empty). Passing a differentcustom_metadatatowrite_exampleautomatically closes the current shard and opens a new one. In TinyAES, each shard stores 256 traces with the same AES key and records{"key": [...]}incustom_metadata, enablingshard_filterand balanced shard iteration without reading the shard files themselves.
Example leaf train/8a745b3853/412a5873e4/20fd9888f6/shards_list.json:
{ "relative_path_self": "train/8a745b3853/412a5873e4/20fd9888f6/shards_list.json", "number_of_examples": 512, "shard_files": [ { "file_infos": [ { "file_path": "train/8a745b3853/412a5873e4/20fd9888f6/062203904722443e82655d5b65650f68.fb", "hash_checksums": ["00fb1da6..."] } ], "number_of_examples": 256, "custom_metadata": { "key": [43, 126, 21, 22, 40, 174, 210, 166, 171, 247, 21, 136, 9, 207, 79, 60] } } ]}Parallel and Distributed Writing
Section titled “Parallel and Distributed Writing”DatasetFiller decouples in-memory example buffering from shard serialization,
compression, hashing, and metadata merging:
-
Single-coordinator writing (
dataset.filler(concurrency=4)): Each full shard is serialized, compressed, and hashed asynchronously in aProcessPoolExecutor(concurrency=4worker processes by default) across all shard formats ("fb","npz","tfrec"). Becauserelative_path_from_splitdefaults toPath(".")(empty), exiting thewithblock writes the leafshards_list.jsonfiles and automatically callsdataset.write_config(...)to update all the way up to<split>/shards_list.jsonanddataset_info.json. -
Built-in multi-process writing (
dataset.write_multiprocessing): When generating examples in parallel across multiple CPU processes on one machine,dataset.write_multiprocessingassigns each worker aDatasetFillerwith a unique non-emptyrelative_path_from_split(Path(uuid.uuid4().hex)). The user-suppliedfeed_writercallable receivesdataset_filler: DatasetFilleras its first positional argument, followed by arbitrary*argsand**kwargsunpacked from that worker’s entry incustom_argumentsandcustom_kwarguments. On exit, each worker updates metadata up to its own<split>/<worker_uuid>/shards_list.json, andwrite_multiprocessingthen merges all workerShardListInfoobjects into<split>/shards_list.jsonanddataset_info.json:def feed_worker(dataset_filler: DatasetFiller,chunk: list[ExampleT],split: SplitT = "train",) -> int:# First argument is `dataset_filler`; remaining `*args` and `**kwargs`# are unpacked from `custom_arguments` and `custom_kwarguments`.with dataset_filler:for values in chunk:dataset_filler.write_example(values=values, split=split)return len(chunk)counts = dataset.write_multiprocessing(feed_writer=feed_worker,custom_arguments=[(chunk_0,), (chunk_1,)],custom_kwarguments=[{"split": "train"}, {"split": "test"}],) -
Custom multi-worker / distributed writing (
DatasetFillerwith non-emptyrelative_path_from_split): When running custom worker processes or distributed jobs, pass a unique, non-emptyrelative_path_from_split(e.g.,Path(f"worker_{worker_id}")) to each worker’sDatasetFiller. Exiting thewithblock merges and hashes that worker’s shard lists up to<split>/<relative_path_from_split>/shards_list.jsonwithout touching<split>/shards_list.jsonordataset_info.json. Once all workers finish, collectfiller.get_updated_infos()on the coordinator and calldataset.write_config(updated_infos=...):def run_worker(dataset_path: Path,worker_id: int,items: list[ExampleT],) -> list[ShardListInfo]:ds = Dataset(dataset_path)filler = DatasetFiller(dataset=ds,relative_path_from_split=Path(f"worker_{worker_id}"),)with filler:for item in items:filler.write_example(values=item, split="train")return filler.get_updated_infos()# Coordinator: merge all worker ShardListInfo updates into the dataset.updated_infos: list[ShardListInfo] = []for worker_id, chunk in enumerate(chunks):updated_infos.extend(run_worker(dataset.path, worker_id, chunk))dataset.write_config(updated_infos=updated_infos)Caveats for custom multi-worker / distributed writing:
relative_path_from_splitmust be non-empty for every concurrent worker even when each worker writes to a different split (e.g., one worker writing only"train"and another writing only"test"). Ifrelative_path_from_splitis left empty (Path(".")), exiting thewithblock callsdataset.write_config(...)and overwrites the shared rootdataset_info.json, racing with other workers and losing their split metadata.- Concurrent workers must use disjoint
relative_path_from_splitpaths where neither path is a parent prefix of another (e.g., do not runPath("a")andPath("a/b")concurrently, since thePath("a")worker writes<split>/a/shards_list.jsonon exit). Sibling paths such asPath("job_1/worker_0")andPath("job_1/worker_1")are safe. - When
relative_path_from_splitis non-empty, the written shards are not visible in<split>/shards_list.jsonordataset_info.jsonuntil the coordinator callsdataset.write_config(updated_infos=...).
Integrity and Merkle Tree
Section titled “Integrity and Merkle Tree”Every FileInfo stores the hash checksums configured in
dataset_structure.hash_checksum_algorithms:
- Each shard file’s checksum is stored in its leaf
shards_list.json. - Each child
shards_list.jsonchecksum is stored in its parent’sshards_list.json. - Each split’s root
shards_list.jsonchecksum is stored indataset_info.json.
This forms a Merkle tree over all
metadata and data files in the dataset. Recording
expected = dataset.current_metadata_checksums() and calling
dataset.check(hash_checksums_values=expected) verifies dataset_info.json,
every shards_list.json in the tree, and every shard file on disk.
Shard File Formats
Section titled “Shard File Formats”Each shard file stores up to examples_per_shard examples compressed with
dataset_structure.compression in one of three formats:
"fb"(FlatBuffers): Default format. Provides zero-copy-friendly binary serialization and high-throughput concurrent iteration via the Rust backend."npz"(NumPy): Standard NumPy archive format."tfrec"(TFRecord): Protocol Buffer format used by TensorFlow (requires TensorFlow to be installed, e.g.,pip install "sedpack[tf]").