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

  • Amazon Web Services
  • Google Cloud
  • CoreWeave
  • Lambda
  • Nebius

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.

01

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
02

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
03

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
04

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.

streaming_loader.py
from streaming import MDSWriter, StreamingDataset
from torch.utils.data import DataLoader
 
# Once: write MDS shards onto the disk, uncompressed,
# so every node reads them in place
with 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 evicted
dataset = StreamingDataset(local="/data/mds/train",
shuffle=True, batch_size=8)
loader = DataLoader(dataset, batch_size=8, num_workers=8)
wds_loader.py
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 retrieval

Write 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 data

One 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
datasets.yaml
PersistentVolume and claim
# One Archil disk over your training bucket
apiVersion: v1
kind: PersistentVolume
metadata:
name: datasets
spec:
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: v1
kind: PersistentVolumeClaim
metadata:
name: datasets
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1000Gi
volumeName: datasets
trainer.yaml
Pod
# Every training pod mounts the same disk
apiVersion: v1
kind: Pod
metadata:
name: trainer-0
spec:
containers:
- name: trainer
image: "your-training-image"
resources:
limits:
nvidia.com/gpu: 8
volumeMounts:
- name: datasets
mountPath: /data
volumes:
- name: datasets
persistentVolumeClaim:
claimName: datasets

Every 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.

loader.py
Plain PyTorch, random access
import numpy as np
import torch
from 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)
warm.sh
Outside Kubernetes
# A GPU or TPU VM outside Kubernetes: mount, and warm
# the tokenizer and validation set into this node's memory
archil 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 with
archil prefetch /mnt/data tokens/train/part-0000 tokens/train/part-0001

Warm 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.

Caching docs

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.

save.py
Model checkpoints
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)
sweep.sh
Disk checkpoints and branches
# Freeze the run, then fork two experiments from it
archil checkpoints create /data before-lr-sweep
 
archil branches --region aws-us-east-1 create dsk-XXXXXXXXXXXX \
lr-3e-4 --from-checkpoint before-lr-sweep
archil branches --region aws-us-east-1 create dsk-XXXXXXXXXXXX \
lr-1e-4 --from-checkpoint before-lr-sweep
 
# Each branch mounts anywhere, like any disk
archil mount <disk-name> /data --region aws-us-east-1 --branch lr-3e-4

Bytes 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

10 TB
500 GB200 TB
8
1128
16
1128
Its partition
Its partitionAll of it
50%
0%100%
1
04

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.

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