ML training
One warm dataset for every node, job, and cluster.
Keep your streaming loader. Archil puts your bucket behind one shared SSD cache that every node mounts as a disk, so back-to-back jobs and sweeps on any cluster read warm data, even on TPU VMs and other nodes with no local SSD to cache on.
Each node's loader fills its own cache from the bucket while it trains. The next job, or the next cluster, starts with empty caches and downloads again.
Runs where your accelerators are
Mount a disk on any Linux node, with an IAM role on AWS or a disk token anywhere else. Public regions run on AWS and Google Cloud, and BYOC runs Archil in your own cloud or on bare metal. How BYOC works.
What changes when the dataset is a disk.
A good streaming loader already starts training without a copy. Archil changes where the bytes live between jobs, and what a node needs to cache them.
No local disk required
Accelerators with no Local SSD, like TPU VMs, have nowhere fast to put a loader's cache. Archil's client caches in memory, and a 4 KiB read that misses it comes back from the shared SSD cache in 0.5 ms at the median, so the node needs no disk of its own.
- Before
- no local SSD → cache on the boot disk
- After
- mount → read the shared cache
Warm once for the whole sweep
The first job pulls each shard from the bucket into one shared cache, and every node, job, and cluster that mounts the disk reads it from there. Data stays cached for up to an hour after its last read, so back-to-back runs, sweeps, and restarts start warm.
- Before
- new job → empty caches → download again
- After
- new job → the cache is already warm
The whole workspace, as files
Shards, raw files your code seeks into, tokenizers, eval sets, and checkpoints sit on one disk, as ordinary files and as plain objects in your bucket. Nothing is locked into one loader's format, and a checkpoint is durable when fsync() returns.
- Before
- samples in shards, everything else elsewhere
- After
- one disk for data, code, and checkpoints
A local cache in every region
Training in more than one region? Put a disk in each one over the same bucket. Each disk pulls a shard across the globe once, then serves every node in its region from a cache next door, so no job streams samples from a bucket on another continent.
- Before
- far region → every node reads across the globe
- After
- far region → one pull, then a local cache
What streaming loaders leave to you.
MosaicML Streaming, WebDataset, and Hugging Face streaming solved the staging problem: shards stream from the bucket while the GPUs train, with shuffling and sharding across nodes, and Streaming adds deterministic, elastic resumption. What they don't decide is where the bytes live.
Every job starts cold
Each node's cache lives on that node's disk. A new cluster or a replaced node starts empty, and Streaming's own FAQ notes that with ephemeral storage you'll likely download shards again.
No local SSD to cache on
TPU VMs have no Local SSD, and a multi-host slice's attached disks are read-only. Where GPU VMs have Local SSD, Google warns its data may be lost when the VM stops or is preempted.
Samples, not files
Shard formats trade file access for sequential reads. MDS needs a conversion step, and Hugging Face's docs say a streamed dataset isn't for random access. WebDataset's tars stay readable by standard tools, but still as shards.
Everything else needs a home
Checkpoints, tokenizers, eval sets, configs, and notebooks aren't samples. They get pushed to the bucket and pulled back down, or copied between clusters by hand.
Already using a streaming loader? Keep it.
Put the shards on the Archil disk and give the loader the mount as a local path. Both loaders document this mode. The loader keeps its shuffling, sharding, and resumption, and the shared cache replaces each node's private copy.
from streaming import MDSWriter, StreamingDatasetfrom torch.utils.data import DataLoader # Once: write MDS shards onto the disk, uncompressed,# so every node reads them in placewith MDSWriter(out="/data/mds/train", columns={"tokens": "ndarray"}) as out: for sample in samples(): out.write(sample) # Every node, every job: local only, no remote and no# cache_limit, so nothing is downloaded or evicteddataset = StreamingDataset(local="/data/mds/train", shuffle=True, batch_size=8)loader = DataLoader(dataset, batch_size=8, num_workers=8)import webdataset as wds # Tar shards on the disk, opened like local files.# No cache_dir: the disk is already the shared cache.urls = "/data/wds/train-{000000..000999}.tar" dataset = ( wds.WebDataset(urls, shardshuffle=1000, nodesplitter=wds.split_by_node) .shuffle(1000) .decode("pil") .to_tuple("png", "json"))Point local at the shards, with no remote
Streaming's local cache is per node: it tracks what's cached in each node's shared memory and evicts by deleting files. So don't aim several nodes' caches at one shared directory. Leave remote and cache_limit unset, and read the shards in place.
Streaming: shard retrievalWrite the shards uncompressed
With compressed shards, Streaming decompresses each one into local before reading it, which on a shared disk means every node writing the same files. Uncompressed shards are read where they are.
Write through the disk
Point MDSWriter or your tar writer at the mount, or at Archil's S3 API. Objects written straight to the bucket can take minutes to appear on the disk.
DALI, Ray Data, and Hugging Face
DALI's readers take a directory; its docs suggest dont_use_mmap on network file systems. Ray Data reads shared files mounted at the same path on every node. Hugging Face streams local files without conversion.
Ray Data: loading dataOne disk in every training pod.
Point an Archil disk at the bucket that holds your data, and mount it ReadWriteMany into every training pod with the CSI driver. Loaders and training code keep opening and reading files.
CSI driver docs# One Archil disk over your training bucketapiVersion: v1kind: PersistentVolumemetadata: name: datasetsspec: capacity: storage: 1000Gi # Archil disks are elastic accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain csi: driver: csi.archil.com volumeHandle: "dsk-XXXXXXXXXXXX" volumeAttributes: region: "aws-us-east-1"---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: datasetsspec: accessModes: - ReadWriteMany resources: requests: storage: 1000Gi volumeName: datasets# Every training pod mounts the same diskapiVersion: v1kind: Podmetadata: name: trainer-0spec: containers: - name: trainer image: "your-training-image" resources: limits: nvidia.com/gpu: 8 volumeMounts: - name: datasets mountPath: /data volumes: - name: datasets persistentVolumeClaim: claimName: datasetsEvery pod reads and writes
ReadWriteMany mounts each pod in conditional mode, so any pod can read the dataset and write its checkpoints.
Scale-out reads
Each client reads from many storage servers in parallel, the way Lustre scales out. Scale-out reads launched at 8 GiB/s per client.
No read cap per disk
Reads per disk are unlimited. The highest one-minute average seen on one disk in production is 5.9 GB/s.
Any S3-compatible bucket
Amazon S3, Google Cloud Storage, Cloudflare R2, or another S3-compatible store.
import numpy as npimport torchfrom torch.utils.data import DataLoader, Dataset class TokenWindows(Dataset): """Random windows from one raw token file on the mount.""" def __init__(self, path="/data/tokens/train.bin", ctx=2048): self.tokens = np.memmap(path, dtype=np.uint16, mode="r") self.ctx = ctx def __len__(self): return (len(self.tokens) - 1) // self.ctx def __getitem__(self, i): start = i * self.ctx window = self.tokens[start : start + self.ctx + 1] window = torch.from_numpy(window.astype(np.int64)) return window[:-1], window[1:] loader = DataLoader(TokenWindows(), batch_size=8, shuffle=True, num_workers=8)# A GPU or TPU VM outside Kubernetes: mount, and warm# the tokenizer and validation set into this node's memoryarchil mount <disk-name> /mnt/data --region aws-us-east-1 \ --conditional \ --max-cache-mb 65536 \ --read-ahead 16777216 \ --pre-fetch tokenizer,tokens/val # Later, warm the files the next epoch starts witharchil prefetch /mnt/data tokens/train/part-0000 tokens/train/part-0001Warm what's next
archil prefetch, or --pre-fetch at mount, reads files and directories into a node's in-memory cache in the background, so the first read is already local.
Tune read-ahead
The client reads up to 4 MiB ahead of sequential reads. Raise --read-ahead for large shards, and size --max-cache-mb for the working set.
Checkpoints that land once.
Write model checkpoints to the disk like any file. A write is durable on Archil once fsync() returns, stored redundantly across Availability Zones, and any node that mounts the disk can resume from it.
Snapshot the disk in 13 ms
A disk checkpoint freezes the dataset, configs, and model checkpoints together, in 13 ms at the median, whatever the disk's size.
Branch every experiment in 33 ms
Fork an independent, writable branch from any disk checkpoint in 33 ms at the median. Give each sweep or dataset version its own branch, and mount it on any node.
Evals and RL rollouts
Fork a task environment per rollout, grade each run, and collect the graded trajectories on a disk.
Evals and RL environments guide
Checkpoints and branches run on Archil-managed disks in the AWS regions. Keep the raw corpus in your bucket, and put what you need to reproduce a run on a managed disk: the dataset manifests, configs, and model checkpoints. Branches and checkpoints. Writes reach up to 1 GB/s per disk today. If that limits how often you can save, tell us; most limits can be raised. Limits.
import os import torch def save(state, step, root="/data/checkpoints/run-42"): path = f"{root}/step-{step:07d}.pt" with open(path + ".tmp", "wb") as f: torch.save(state, f) f.flush() os.fsync(f.fileno()) # durable on Archil when this returns os.rename(path + ".tmp", path)# Freeze the run, then fork two experiments from itarchil checkpoints create /data before-lr-sweep archil branches --region aws-us-east-1 create dsk-XXXXXXXXXXXX \ lr-3e-4 --from-checkpoint before-lr-sweeparchil branches --region aws-us-east-1 create dsk-XXXXXXXXXXXX \ lr-1e-4 --from-checkpoint before-lr-sweep # Each branch mounts anywhere, like any diskarchil mount <disk-name> /data --region aws-us-east-1 --branch lr-3e-4Bytes pulled from the bucket.
Set the shape of a sweep and compare what per-node loader caches download against filling one shared cache. Every input is an assumption you can change.
Your assumptions
A cache on every node
45 TBpulled from the bucket
- Each cold job
- 10 TB
- Jobs starting cold
- 4.5
- Times the dataset
- 4.5×
A job starts cold on a new cluster, on replaced or preempted nodes, or where node storage doesn't outlive the job.
One shared cache
20 TBpulled from the bucket
- Each fill
- 10 TB
- Times it fills
- 2
- Times the dataset
- 2×
After each break of over an hour, the cache refills from the bucket as the next job reads.
A simplified model, not a benchmark. It counts bytes read out of the bucket, not what they cost, which depends on your provider and region, and not throughput or latency. It assumes every job reads the same samples, that node caches are large enough not to evict (a cache_limit set too low evicts and downloads shards again), and that the shared cache holds the working set: Archil's cache grows to fit it and keeps data for up to an hour after its last read, per its published performance notes. It leaves out that reads from the shared cache cross the network, where a node's own cache reads from its local disk.
How the options compare.
Local NVMe is the fastest place to read data that's already there. A streaming loader removes the staging step and brings shuffling and resumption. A parallel file system serves tightly coupled jobs. Archil sits under a loader, for data that has to be in many places, on nodes with or without a disk to cache on.
| Property | Local NVMe staging | Streaming data loader (MosaicML Streaming, WebDataset) | Parallel file system | FUSE over a bucket | Archil |
|---|---|---|---|---|---|
| Reads, once data is in place | Fastest: never leaves the box | From the node's own cache, after each shard downloads | High aggregate throughput for tightly coupled jobs | Each node's own reads from the bucket | A shared SSD cache, with scale-out reads per client |
| Before the first step | Copy the dataset to every node | Nothing: shards download as training reaches them | Load the dataset in, or link it to the bucket | Mount | Mount |
| Disk on each node | Room for the whole dataset | Room for its cache; cache_limit bounds it | None | None, unless you turn on its file cache | None: the client caches in memory |
| Data format | Any files | Its own shards: MDS, or tar for WebDataset | Any files | Your objects, as files | Any files, kept as plain objects in your bucket |
| File semantics | Full POSIX | None: samples through the loader | Full POSIX | Partial: no locks or renames on Mountpoint | Standard POSIX operations |
| Shuffle and resume | Up to your loader | Built in: shard and buffer shuffles; Streaming adds elastic, deterministic resumption | Up to your loader | Up to your loader | Up to your loader, so keep the streaming one |
| A new job or cluster | Copies everything again | Reuses a cache only on the same node's disk | Usually its own file system, in one cloud | Downloads again, node by node | Mounts the same disk, warm for an hour after its last read |
| Checkpoints | Copied off by hand | Not its job: usually through the bucket | Shared within the file system | Through the bucket | Durable at fsync(), readable from any node |
Local NVMe staging
- Reads, once data is in place
- Fastest: never leaves the box
- Before the first step
- Copy the dataset to every node
- Disk on each node
- Room for the whole dataset
- Data format
- Any files
- File semantics
- Full POSIX
- Shuffle and resume
- Up to your loader
- A new job or cluster
- Copies everything again
- Checkpoints
- Copied off by hand
Streaming data loader (MosaicML Streaming, WebDataset)
- Reads, once data is in place
- From the node's own cache, after each shard downloads
- Before the first step
- Nothing: shards download as training reaches them
- Disk on each node
- Room for its cache; cache_limit bounds it
- Data format
- Its own shards: MDS, or tar for WebDataset
- File semantics
- None: samples through the loader
- Shuffle and resume
- Built in: shard and buffer shuffles; Streaming adds elastic, deterministic resumption
- A new job or cluster
- Reuses a cache only on the same node's disk
- Checkpoints
- Not its job: usually through the bucket
Parallel file system
- Reads, once data is in place
- High aggregate throughput for tightly coupled jobs
- Before the first step
- Load the dataset in, or link it to the bucket
- Disk on each node
- None
- Data format
- Any files
- File semantics
- Full POSIX
- Shuffle and resume
- Up to your loader
- A new job or cluster
- Usually its own file system, in one cloud
- Checkpoints
- Shared within the file system
FUSE over a bucket
- Reads, once data is in place
- Each node's own reads from the bucket
- Before the first step
- Mount
- Disk on each node
- None, unless you turn on its file cache
- Data format
- Your objects, as files
- File semantics
- Partial: no locks or renames on Mountpoint
- Shuffle and resume
- Up to your loader
- A new job or cluster
- Downloads again, node by node
- Checkpoints
- Through the bucket
Archil
- Reads, once data is in place
- A shared SSD cache, with scale-out reads per client
- Before the first step
- Mount
- Disk on each node
- None: the client caches in memory
- Data format
- Any files, kept as plain objects in your bucket
- File semantics
- Standard POSIX operations
- Shuffle and resume
- Up to your loader, so keep the streaming one
- A new job or cluster
- Mounts the same disk, warm for an hour after its last read
- Checkpoints
- Durable at fsync(), readable from any node