In this article
Run AI workloads on any cloud, store on Hugging Face: zero-egress storage with SkyPilot
Hugging Face and SkyPilot have combined their systems so that models and datasets remain on the Hub while compute runs on any cluster with GPUs. You mount a Hugging Face Bucket or any Hub repo into a SkyPilot job using a single
hf://
URL and your existing
HF_TOKEN
, then launch the job wherever capacity is available. Hugging Face charges no egress, so reading data onto those GPUs costs nothing, regardless of the cloud provider.
Here is what has changed:
- Your Hub data in any job. The
store: hf
option mounts a Hugging Face Bucket (read-write) or any model, dataset, or Space repo (read-only) into a SkyPilot task with one
hf://
URL and your existing
HF_TOKEN
. This works via
MOUNT
or
COPY
.
- Run it on any GPU, on any cloud. SkyPilot finds compute across 20+ clouds, Kubernetes, Slurm, and on-prem. The same run uses whichever of your reserved or on-demand GPUs is available, on any vendor.
- No egress to read your data. Hugging Face Storage charges no egress or CDN fees. Wherever SkyPilot lands the job, it reads your models and datasets straight from the same bucket, with no per-cloud copies and no egress bill to pull them in.
- Xet-backed dedup. Buckets are built on Xet, so incremental checkpoints and model variants only store and transfer the chunks that changed.
- Built together. Hugging Face and SkyPilot shipped this jointly, and the Hugging Face team upstreamed the
hf-mount
FUSE fixes that make it work in unprivileged containers.
Hugging Face Storage is now a first-class SkyPilot backend
SkyPilot tasks already read and write cloud object stores like S3, GCS, Azure, R2, and many others by mounting them at a local path. Hugging Face Storage now joins that list as
store: hf
, reached through the
hf://
scheme:
file_mounts: # A Hugging Face Bucket, read-write, for checkpoints, logs, processed data. /checkpoints: source: hf://buckets/my-org/qwen-sft store: hf mode: MOUNT # or COPY # A model repo, mounted read-only. /base-model: source: hf://Qwen/Qwen3.5-4B store: hf mode: MOUNT # A dataset repo, pinned to a revision, read-only. /data: source: hf://datasets/my-org/my-dataset@main store: hf mode: MOUNTThat single
hf://scheme covers the whole lifecycle. You read the model and dataset from their repos, write checkpoints to a Bucket while training, publish the finished model back to a repo, and pull it onto inference servers when serving. Most teams already keep their models and datasets on the Hub, so there is no migration step and no new storage account to create.
MOUNTuses Hugging Face’s
hf-mountFUSE backend. A bucket or repo shows up as a local path next to SkyPilot’s other FUSE mounts like
gcsfuse,
blobfuse2,
rclone, and
goofys. The fetching happens at the filesystem layer. When your code issues a
read(), the driver pulls just those bytes from the Xet backend. Only the data you actually touch crosses the network, and
hf-mountkeeps an on-disk cache so repeat reads stay local. That on-disk cache is the behavior SkyPilot gives its other backends under
MOUNT_CACHED. A plain
MOUNTinstead streams every read from the bucket with nothing kept locally. For the
hfstore,
MOUNTand
MOUNT_CACHEDbehave the same, so either mode keeps the cache.
Because reads are lazy, a process can start working through a large file before the whole file has downloaded. It does not block on a full copy first. This keeps the GPU busy almost immediately, training on data as it streams in rather than sitting idle and billing while a dataset or checkpoint copies down. It pays off most on the first epoch, when nothing is cached yet.
COPYtakes the other route and downloads through
huggingface_hubup front, with no special requirements.
Authentication is the token you already have. Set
HF_TOKENin your environment and hand it to a run with
--secret HF_TOKEN. SkyPilot uses it for the mount on whatever cloud the job lands. One token works whether the job lands on AWS, GCP, Azure, Nebius, Lambda, or your own Kubernetes cluster. There are no per-cloud bucket keys to juggle.
No egress: storage stops deciding where you run
GPU capacity rarely comes from one place anymore. To get enough H100s and H200s, teams hold reserved and committed capacity across several vendors at once. They might have a block on a hyperscaler, a cluster on a neocloud, or an on-prem rack. They run wherever they have allocation. SkyPilot is built for this: one job spec, scheduled across 20+ clouds, Kubernetes, and on-prem, landing on whichever reserved cluster is free.
Object storage has been the catch. Object stores are regional and per-cloud. Feeding a GPU or an inference server that sits in a different vendor’s data center means either keeping a copy of your data in every vendor’s bucket or paying to pull it across. Most clouds charge egress, around $0.09/GB out of AWS, the moment data leaves their network. They often charge between regions inside one cloud too. Pulling a base model onto every inference node, or iterating a dataset for several epochs from a cluster on another cloud, adds a hefty bill on top of GPUs you have already reserved. Teams end up pinning each run to whichever vendor holds the data and leaving the rest of their capacity idle.
Hugging Face Storage takes that cost off the table where it bites: the read side. With no egress or CDN fees and storage at $12-18/TB/month, versus AWS S3 at roughly $23/TB plus egress, the same bucket is reachable from every one of those clusters. Reading from it is free no matter where the GPUs run. Writing back still costs your compute cloud’s usual egress, the same as it would to any off-cloud store. For most AI work, the reads dominate. A dataset streamed over many epochs, or model weights pulled onto every new training or inference node. You stop pinning each run to whichever vendor holds a copy of the data.
A quick benchmark
To collect some benchmark numbers, we ran a small fine-tune:
Qwen/Qwen3.5-4Bon the
HuggingFaceH4/Multilingual-Thinkingdataset with TRL’s
SFTTrainer. We mounted the model read-only from its Hub repo and wrote every checkpoint to a Hugging Face Bucket. The same SkyPilot YAML ran on AWS, GCP, and Lambda, changing only
--infra. SkyPilot placed each job wherever GPUs were free, and all three read and wrote the same bucket.
# qwen-sft.yaml. Launch anywhere: sky launch qwen-sft.yaml --infra aws|gcp|... resources: accelerators: H100:1 # or whatever the cloud has file_mounts: /base-model: source: hf://Qwen/Qwen3.5-4B # read-only, lazy-mounted from the Hub store: hf mode: MOUNT /checkpoints: source: hf://buckets/my-org/qwen-sft # read-write Bucket store: hf mode: MOUNT run: | python train.py --model /base-model --output_dir /checkpointsWhat we measured:
- The model loaded free on every cloud. Lazy reads pull only what
from_pretrained
touches. It was ready to train in about 30 seconds. Because Hugging Face charges no egress, that pull cost nothing. Had the model lived in S3, every read to a GPU on another cloud would have been billed egress.
- Checkpoints streamed straight to the bucket at up to ~170 MB/s and persisted past the GPU instance.
Per cloud, checkpoints wrote to the bucket at:
| Cloud | GPU | Checkpoint write |
|---|---|---|
| AWS (us-east-2) | L40S | ~168 MB/s |
| GCP (us-central1) | L4 | ~123 MB/s |
| Lambda (us-west-3) | H100 | ~112 MB/s |
Xet-backed storage: dedup for checkpoints and model variants
Hugging Face Buckets are built on Xet, which uses content-defined chunking to split files into ~64 KB chunks and store each unique chunk once. Because the boundaries follow the content, an edit changes only the chunks it touches and the rest are recognised as already stored. This pays off in a few places:
- Incremental and adapter checkpoints. When you freeze layers, train adapters, or otherwise leave most weights untouched between saves, only the changed chunks upload instead of the whole checkpoint.
- Model variants that share a base. Fine-tunes and quantizations of one base model overlap heavily, so the shared chunks are stored once across all of them.
- Datasets you append to. Logs like conversation traces or inference outputs grow by appending rows to large Parquet files. The existing row groups stay byte-identical, so only the new rows transfer. In Hugging Face’s test, appending 10K rows to a 100K-row table moved about 10 MB instead of the full ~106 MB. If you edit or delete rows in place, write with
use_content_defined_chunking=True
to keep changes local
Source Read original →




