Formats: turning a bucket into training examples¶
A CachedDataset does two things: it figures out what your training examples
are, and it serves their bytes fast through the peer cache (local hit → peer
hit → S3, with write-back). The first half — how a pile of objects in your
bucket becomes a list of training examples — is the job of a Format.
The split of responsibility is deliberate:
| Layer | Owns | Knows about |
|---|---|---|
Format (RAMJET) |
which object keys make up one example | your storage layout (where images live, where labels live, how they pair) |
| Your training code | decode bytes, parse labels into targets, model, loss | your task (classification / detection / …) |
Format is framework-agnostic: it never fetches bytes, never builds target
tensors, never touches the cache. You either use the built-in GenericFormat
or write your own subclass — it's one method.
The contract¶
A Format is the only extension point of CachedDataset. Subclass
ramjetio.Format and implement exactly one method:
class Format(ABC):
def list_examples(self, client) -> List[Dict[str, Optional[str]]]:
"""Ordered list of training examples — a *list of dicts of strings*.
Each example is a {role: object_key} group: the keys that together
form one training example. A single-file example is a one-role group
({'data': key}); an image+label example is two roles
({'image': key, 'label': key}). A role whose value is None is served
as empty bytes (use it for an optional sibling, e.g. a missing label).
Order must be deterministic across processes so DDP ranks shard the
same set of examples.
"""
That's the whole contract. list_examples returns the addresses (object
keys); CachedDataset does everything else — it fetches the bytes for every
key (local hit → peer hit → S3, with write-back, keyed by the object path),
DDP-shards the examples, and hands the caller {role: bytes} for each group.
The client is the data-source client built from your dashboard Data Source
(S3Client / LocalClient); it exposes list_files(prefix='', suffix='').
The split (train / val) is the Path Prefix field on the dashboard — it
is folded into the client, so the keys you get back are already scoped to it.
The built-in: GenericFormat — each file is one example¶
GenericFormat lists every object in the data source and returns one example
per file: [{'data': key}, ...]. cached[i] yields {'data': bytes}; your
code decodes (cv2, decord, parquet, …). Use it when each file stands alone (one
tensor, one record, one video). Option: suffix restricts the scan to one
extension (e.g. '.mp4').
⚠️
GenericFormatdoes not pair files. If an example is split across several objects (an image plus a sidecar label, say),GenericFormatwill treat the label as its own example too. Write a customFormat(below).
Writing your own format¶
Pairing files is a property of your storage layout, so the subclass lives in
your training script — you name it, you define it. You only implement
list_examples; the cache, DDP sharding and cold→warm behaviour come for free.
Example — pair each image with its sidecar label
(images/<stem>.<ext> ↔ labels/<stem>.txt):
import ramjetio
class ImageLabelFormat(ramjetio.Format):
def list_examples(self, client):
all_keys = set(client.list_files())
examples = []
for key in sorted(all_keys):
parent, _, name = key.rpartition('/')
if parent.rpartition('/')[2] != 'images':
continue # skip the label files
stem = name.rpartition('.')[0] or name
label = f"{parent[:-len('images')]}labels/{stem}.txt"
examples.append({'image': key,
'label': label if label in all_keys else None})
return examples
ds = ramjetio.CachedDataset(format=ImageLabelFormat(), dataset_name="my-data")
Then your Dataset reads ds[i]['image'] / ds[i]['label'] as bytes and
turns them into a tensor and a target. A worked end-to-end version (YOLO label
→ class id, DDP) ships in the repo at ramjet-training/train.py.