Shard Cache Hierarchy¶
This document explains how Datavo caches shard tars on the consumer side and how to shape that cache for fast IO.
The shard cache hierarchy is a property of the machine running the consumer
code. Datavo dataset consumers (SampleStream.from_dataset(...)) read from it
transparently. They never need to know which tiers exist, where they live, or
how big they are.
Goals¶
The hierarchy is designed around one boundary:
- Consumers open a dataset and iterate samples. They do not configure storage.
- The local IO topology is described in a single YAML file — no tier needs pre-warming.
Specifically:
- A consumer-side iteration over a Datavo split should hit local disk on the second pass, even when several users share one machine.
- You can add a shared scratch dir, a per-user SSD, or both by editing one config file. No code change on the consumer side.
- The cache should fill itself naturally as consumers read. There is no separate prefetch or bulk-import step.
Identity And Cache Key¶
Datavo workset shards are addressed by content_hash. The hash is a
deterministic function of the dataset state and the requested input keys:
- dataset manifest reference,
- dataset revision watermark,
- shard index,
- requested input keys,
- the ordered list of sample ids for that shard.
For a given (dataset, shard_index, required_input_keys, revision watermark)
the hash is stable across worksets and processes. That makes it safe to use as
a global cache key. Two different worksets that ask for the same shard of the
same dataset with the same keys hit the same cache file.
Files are stored as <content_hash>.tar at the top level of each tier
directory. Tier directories also contain a .staging/ subdirectory for
in-flight downloads and a .locks/ subdirectory for per-shard advisory locks.
Tiers¶
A tier is a backend that can hold shards by content hash. Two kinds of tier ship today; both implement the same small interface so the hierarchy can treat them uniformly.
Folder tiers¶
A folder tier is a directory on a local filesystem.
ShardCacheTier(
path="/scratch/datavo/shards",
max_bytes=1_099_511_627_776, # 1 TiB
name="scratch",
)
Folder tier semantics:
pathis created on first use.~and environment variables are expanded.max_bytesis optional. Missing or zero means unbounded.- Eviction is LRU based on file mtime. Reads update mtime so popular shards stay resident.
- Each tier locks per
content_hash(POSIXfcntl.flock) so concurrent processes on the same machine do not double-fetch or stomp on installs. - The tier is directly readable: the consumer iterates the tar file in place, no copying involved.
Cloud tiers¶
Three cloud-backed tiers ship today, one per major cloud:
AzureBlobShardCacheTier— Azure Blob containerS3ShardCacheTier— AWS S3 bucketGcsShardCacheTier— Google Cloud Storage bucket
All three behave the same way from the hierarchy's perspective:
- Shards are stored at
<content_hash>.tarinside the bucket/container. An optionalprefixextends that to<prefix>/<content_hash>.tarfor sharing one bucket across deployments. - The tier is not directly readable. A cloud-tier hit is materialized into a folder tier first; the consumer always reads from local disk. See Cloud topology below.
max_bytesis informational. Cloud buckets do not evict locally — eviction is expected to be managed via cloud-side lifecycle policies (Azure blob rules, S3 lifecycle, GCS lifecycle/autoclass). Settingmax_bytesstill lets the hierarchy skip the tier for shards that exceed the limit.- The cloud SDK is imported lazily. A tier is constructable without the SDK installed; the import only fires when you actually exercise the tier. Concrete extras:
- Azure:
pip install 'datavo-sdk[azure]'(azure-storage-blob, plusazure-identityfor managed-identity auth). - AWS:
pip install 'datavo-sdk[aws]'(boto3). - GCP:
pip install 'datavo-sdk[gcp]'(google-cloud-storage). - All three:
pip install 'datavo-sdk[all-clouds]'.
AzureBlobShardCacheTier(
container_url="https://datavoeastus.blob.core.windows.net/shards",
name="region-eastus",
# sas_token: SAS string for ad-hoc auth, otherwise DefaultAzureCredential
# managed_identity_client_id: pin to a specific user-assigned MI
)
S3ShardCacheTier(
bucket="datavo-cache-us-east-1",
region="us-east-1",
name="region-us-east-1",
# boto3's default credential chain — env vars, EC2 instance profile,
# ECS task role, EKS service account...
)
GcsShardCacheTier(
bucket="datavo-cache-us-central1",
project="datavo-prod",
name="region-us-central1",
# Application Default Credentials — workload identity on GKE, service
# account on GCE, gcloud auth locally...
)
Tiers are independent of each other. Each tier evicts on its own schedule against its own capacity. The hierarchy never coordinates eviction across tiers.
Hierarchy¶
A ShardCacheHierarchy is an ordered list of tiers, fastest first. The
default order is the order they appear in YAML.
The hierarchy enforces one invariant: at least one folder-backed tier must be present. That tier is the "local landing zone" where any remote-tier hit is materialized so the consumer can read a regular file. A hierarchy made up of only remote tiers would have nothing to hand back to the consumer.
Reads¶
hierarchy.get_or_fetch(content_hash, fetcher) walks tiers fastest to slowest
looking for the shard. The first tier that has it serves the read.
If no tier has it, the caller-supplied fetcher writes the tar into a staging
file. The fetcher is opaque to the cache. For a Datavo consumer the fetcher
issues a workset stream ticket and downloads the shard.
Writes: populate on miss¶
On a complete miss the staged tar is installed into every tier that can hold it. Each tier respects its own capacity:
- if a tier's
max_bytesis smaller than the shard, the tier is skipped with a warning; - if at least one tier accepts the shard, the call succeeds and returns the topmost installed path;
- if no tier accepts, the call raises.
This is what makes the cache self-warming. You do not need to pre-fill the scratch tier. The first consumer to read a shard puts it everywhere.
Writes: promote on hit¶
When a slower tier serves a read, the shard is also copied into the faster tiers above it that did not have it. After promotion the call returns the topmost installed path so the caller reads from the fastest available tier even on a first-time slow-tier hit.
Promotion is per-tier subject to capacity:
- tiers too small for the shard are skipped with a warning;
- if every faster folder tier rejects, the read is served from the original slower-tier hit (no error) — unless the hit was on a remote tier, in which case the call raises because there is no local landing zone for the consumer.
Promotion happens once per call. After a promote the shard is in the faster tier; subsequent calls find it there directly.
Reads from remote tiers¶
A hit on a blob tier (or any tier with supports_direct_read = False) is
materialized through the topmost folder tier's .staging/ area on its way
to the consumer:
- The hierarchy downloads the blob into a staging file inside the topmost folder tier.
- The staging file is then installed into every faster folder tier that can hold it (the same promote-on-hit path used for slow-folder hits).
- The hierarchy returns the path inside whichever folder tier accepted first — the consumer reads from a regular local file.
That single staging step is what couples the local tier and the regional blob tier into a real hierarchy: the consumer doesn't notice the blob exists, and the blob's network cost is paid once per shard per VM.
Hits do not refetch¶
The fetcher is never called when any tier has the shard, including the
promote-on-hit path. There is no revalidation step. A content hash uniquely
identifies the bytes; if a tier has content_hash.tar, it is by definition
the correct bytes.
Configuration¶
The hierarchy is resolved at consumer startup. The resolution order is:
- An explicit
cache_config_pathargument toSampleStream.from_dataset(...)orget_default_shard_cache(...). /etc/datavo/shards.yaml(system-wide).~/.config/datavo/shards.yaml(per-user override).- A built-in default: a single user-local tier at
$DATAVO_SHARD_CACHE_DIRor~/.cache/altavo/datavo/shardswith 50 GiB LRU capacity.
System-first is intentional. The shared IO topology is a property of the machine, not the user. Per-user overrides exist for laptops and developer workstations.
YAML schema¶
tiers:
- name: scratch
type: folder # default; can be omitted
path: /scratch/datavo/shards
max_bytes: 1TB
- name: home
path: ~/.cache/altavo/datavo/shards
max_bytes: 50GB
- name: region-eastus
type: azure_blob
container_url: https://datavoeastus.blob.core.windows.net/shards
# sas_token: "..." # optional; otherwise DefaultAzureCredential
# prefix: "team-a" # optional namespacing inside the container
# managed_identity_client_id: "..." # optional; specific UAMI client id
- name: region-us-east-1
type: s3
bucket: datavo-cache-us-east-1
region: us-east-1
# prefix: "team-a"
- name: region-us-central1
type: gcs
bucket: datavo-cache-us-central1
# project: "datavo-prod" # optional
# prefix: "team-a"
Common fields:
tiers(required, non-empty list). Order is fastest first. At least one tier must be folder-backed.tiers[].type(optional). One offolder,azure_blob,s3,gcs. Defaults tofolder.tiers[].name(optional). Display name used in logs.tiers[].max_bytes(optional). Integer or string with a SI-style suffix (B,KB,MB,GB,TB, 1024-based). Missing or zero means unbounded.
Folder-tier fields:
tiers[].path(required). Local filesystem path.~and$VARare expanded.
Azure-blob-tier fields:
tiers[].container_url(required). Azure Blob container URL.tiers[].sas_token(optional). When present, used for SAS-based auth. Otherwise the tier falls back toDefaultAzureCredential.tiers[].prefix(optional). Path-style prefix prepended to each blob name.tiers[].managed_identity_client_id(optional). Passed through toDefaultAzureCredentialfor pinning to a specific user-assigned managed identity.
S3-tier fields:
tiers[].bucket(required). S3 bucket name.tiers[].region(optional). AWS region; boto3 figures it out from the default chain if omitted but explicit is usually clearer.tiers[].prefix(optional). Key prefix prepended to each object name.
GCS-tier fields:
tiers[].bucket(required). GCS bucket name.tiers[].project(optional). GCP project id; ADC usually resolves it.tiers[].prefix(optional). Object name prefix.
The legacy root field from the pre-Datavo cache hierarchy is accepted for
backward compatibility but ignored, with a warning. The source of truth in the
Datavo model is the server itself; the hierarchy only describes local tiers.
Recommended deployment: shared on-prem workstation¶
For a shared GPU workstation with a fast NVMe and a slow shared NFS scratch share:
# /etc/datavo/shards.yaml
tiers:
- name: local
path: /var/cache/datavo/shards
max_bytes: 200GB
- name: scratch
path: /scratch/datavo/shards
max_bytes: 5TB
The first consumer on this machine to read a shard downloads it once and
populates both tiers. Subsequent reads on the same shard hit local.
A different user reading the same shard later still hits scratch and gets
the shard promoted into local on their first read.
Single-user laptop¶
The built-in default is usually enough; no YAML required.
Cloud topology¶
On a cloud cluster of GPU VMs the latency and egress profile of the origin Datavo server is unfavorable to read from on every iteration. The recommended pattern is to put one bucket per region in between the per-VM SSD and the origin: every VM in that region shares the cache, and the cache fills naturally as users in the region consume.
The same shape works on all three clouds, swapping the bucket-side tier:
# /etc/datavo/shards.yaml on each GPU VM in eastus (Azure)
tiers:
- name: nvme
path: /mnt/cache/datavo/shards
max_bytes: 200GB
- name: region-eastus
type: azure_blob
container_url: https://datavoeastus.blob.core.windows.net/shards
# managed identity picks up the VM's identity; no secrets in this file
# us-east-1 on AWS
tiers:
- name: nvme
path: /mnt/cache/datavo/shards
max_bytes: 200GB
- name: region-us-east-1
type: s3
bucket: datavo-cache-us-east-1
region: us-east-1
# us-central1 on GCP
tiers:
- name: nvme
path: /mnt/cache/datavo/shards
max_bytes: 200GB
- name: region-us-central1
type: gcs
bucket: datavo-cache-us-central1
What happens (identical across clouds):
- First VM in the region opens a dataset. Both tiers miss. The origin
server streams the tar to the VM, the hierarchy stages it under
/mnt/cache/datavo/shards/.staging/, then installs intonvmeand uploads to the region bucket. The shard is now warm for the whole region. - Second VM in the same region opens the same dataset.
nvmemisses on that VM but the region bucket hits. The hierarchy materializes the object into the VM'snvme.staging/, installs intonvme, and returns the local path. The origin server is never contacted. - A VM in a different region opens the same dataset. That region's bucket misses (different bucket), so the call falls through to the origin server and warms up the new region. Cross-region egress is paid once per region, not once per VM.
For the consumer:
- Nothing changes in code.
SampleStream.from_dataset(...)reads the local YAML, walks the hierarchy, and hands back a local path.
The hierarchy treats all three cloud tiers identically; you can in principle mix providers in one config (e.g. an Azure deployment that also reads from a shared GCS bucket of legacy datasets), though that's an unusual setup.
Why not stream straight from the bucket?¶
The hierarchy could in principle read the tar straight out of the bucket over the network, skipping the local copy. The current design instead materializes the object into the local tier on first read because:
- Iterating a Datavo dataset is usually a long-running training loop that reads each shard several times across epochs; the local copy pays for itself after one re-read.
- Local mtime-driven LRU then becomes the per-VM working-set policy without any bucket-side coordination.
- The consumer's read path stays a single
tarfile.open(local_path)call; there is no separate "remote shard" iteration mode.
If a future workload changes that tradeoff (pure shuffle-once, very large shards) a streaming read mode can be added as an opt-in flag without touching the rest of the design.
Concurrency¶
Folder-tier installs are guarded by per-content-hash advisory file locks
(tier.path/.locks/<content_hash>.lock). Two processes on the same machine
that race on the same shard:
- both stage their own download into the same tier's
.staging/, - one acquires the lock first, finishes its rename, releases the lock,
- the second acquires the lock, observes the file already exists, discards its own staged copy, and returns the existing path.
The lock is per-shard, not per-tier, so concurrent fetches of different shards do not block each other. The hierarchy itself holds no locks; the locking lives entirely inside each tier.
Cloud-tier installs are race-free at the storage layer: uploads of the same content hash from two VMs may both succeed, but they write the same bytes (same content hash means same payload), so the resulting object is correct either way. The cost is one redundant upload, which is negligible compared to the network fetch from the origin server that both VMs would otherwise do.
Operational Considerations¶
- Disk usage. Each tier independently respects its
max_bytes. If you set the top tier too small, large shards are skipped there and shared tiers do the work. This is intentional: a too-tight top tier does not break reads, it just degrades them. - Filesystem requirements. Tier directories must support
os.renameatomically within themselves (any POSIX filesystem does). Cross-tier installs useshutil.copyfileand tolerate cross-filesystem layouts. - Cleanup. Stale entries in
.staging/may accumulate if a process is killed mid-download. They are safe to remove manually. - Cache poisoning. Files in a tier are trusted because they are addressed by content hash. Anyone who manually drops tars into a tier directory must name them with the correct hash.
- Observability. The hierarchy logs skipped tiers, promotion attempts,
and install failures at
WARNING. There is no per-tier metrics emission today; capacity and hit rates are surfaced via the standard logging stream.
Programmatic Access¶
The hierarchy and its tiers are public SDK surface:
from datavo_sdk import (
AzureBlobShardCacheTier,
SampleStream,
GcsShardCacheTier,
S3ShardCacheTier,
ShardCacheHierarchy,
ShardCacheTier,
get_default_shard_cache,
)
# Default consumer path.
ds = SampleStream.from_dataset("voice_demo_v1/train", keys=["audio.wav"])
# Inspect or override:
cache = get_default_shard_cache()
for tier in cache.tiers:
print(tier.name, tier.max_bytes, tier.supports_direct_read)
# Custom on-prem topology, e.g. for tests.
custom = ShardCacheHierarchy(
[
ShardCacheTier("/tmp/fast", max_bytes=10 * 1024**3, name="fast"),
ShardCacheTier("/tmp/slow", name="slow"),
]
)
ds = SampleStream.from_dataset("voice_demo_v1/train", keys=["audio.wav"], cache=custom)
# Cloud topology — pick the tier class that matches the region's cloud.
cloud_azure = ShardCacheHierarchy(
[
ShardCacheTier("/mnt/cache/datavo/shards", max_bytes=200 * 1024**3, name="nvme"),
AzureBlobShardCacheTier(
container_url="https://datavoeastus.blob.core.windows.net/shards",
name="region-eastus",
),
]
)
cloud_aws = ShardCacheHierarchy(
[
ShardCacheTier("/mnt/cache/datavo/shards", max_bytes=200 * 1024**3, name="nvme"),
S3ShardCacheTier(
bucket="datavo-cache-us-east-1",
region="us-east-1",
name="region-us-east-1",
),
]
)
cloud_gcp = ShardCacheHierarchy(
[
ShardCacheTier("/mnt/cache/datavo/shards", max_bytes=200 * 1024**3, name="nvme"),
GcsShardCacheTier(
bucket="datavo-cache-us-central1",
name="region-us-central1",
),
]
)
The same ShardCacheHierarchy can be reused across multiple SampleStream
instances. It carries no per-dataset state.
Importing a cloud tier class does not require the cloud SDK; the SDK is
imported lazily the first time the tier hits the wire. That keeps the cost
of from datavo_sdk import S3ShardCacheTier to zero when boto3 is not
installed.