Section D · Operating it

Cost, Scale & Feeding the GPUs

In an ML data plane the expensive resource is not storage or compute in the warehouse — it is a GPU waiting for data. This chapter does the arithmetic: storage tiers, the data-loading path from object store to accelerator, feature-computation cost, egress, and the governance that stops any of it from surprising finance.

Where the money goes

Ask a team where their ML data budget goes and they will say "storage". Measure it and the answer is usually different. For a platform at GridDP's scale the cost stack, roughly in descending order, is: GPU hours lost to input starvation (the training job runs but the accelerator sits at 30% utilization because the dataloader can't keep up); feature computation (recomputing large aggregations on a schedule regardless of whether anything changed); warehouse compute for training-set builds (point-in-time joins over years of history, run per experiment); egress (moving datasets between the lake's region and wherever the GPUs are); and only then storage, which is cheap per byte but compounds through copies — bronze, silver, feature history, per-experiment training snapshots, served-feature logs.

That ordering dictates the priorities. The first engineering hour goes to the loading path, not to Parquet compression settings.

The unit that matters

Price everything in GPU-hour equivalents. An H100 hour costs on the order of $2–4 on a neocloud and $6–10 on a hyperscaler; a terabyte-month of object storage costs about the same as seven hours of one H100 ($20 divided by about $3). If a design choice saves a terabyte of storage but adds more than seven hours of GPU idle time a month, it loses. This single conversion settles most arguments in this chapter.

Storage tiers & the math

Storage is cheap but not free, and the lever is tiering by access pattern, not compression heroics. Four tiers cover an ML data plane:

TierWhat lives thereAccess patternMediumOrder-of-magnitude cost
HotOnline feature store, current training shards being consumedms reads, high QPSRedis / DynamoDB; local NVMe on trainers$$$$ per GB — keep it tiny
WarmFeature history (offline store), gold marts, last N training sets, served-feature log (recent)Scans, point-in-time joins, dailyObject store standard tier, Parquet/Iceberg~$20/TB-month
CoolOlder training snapshots, bronze beyond 90 d, decision logs past 1 yRare, audit, backfillInfrequent-access object tier~$10/TB-month + retrieval
ArchiveRegulatory retention (ledger, AML decision logs)Almost neverGlacier-class~$1–4/TB-month, hours to restore

The math for GridDP's dominant source, the telemetry firehose: ~150M samples/day at ~120 bytes compressed ≈ 18 GB/day ≈ 6.5 TB/year in bronze. Keep 90 days hot-ish in the lake (1.6 TB), the hourly rollup forever (~2% of raw), and tier the rest. Feature history for the ~40 tier-1 features across 17,000 GPUs and 200k accounts at hourly grain is another ~1 TB/year. The copies are the surprise: if every experiment materializes its own training set and nobody expires them, a team of eight data scientists produces several TB a quarter. The fix is snapshot-based training sets (chapter 05) — a training set is a query plus a snapshot ID, not a copy — with a 90-day expiry on any physical materialization not referenced by a registered model.

The training data-loading path

The path from object store to GPU memory is where ML data architecture differs most from analytics. A warehouse query returns a result set; a training job needs a continuous, shuffled, sharded stream of samples delivered faster than the model consumes them, resumable mid-epoch, across many workers. Tabular models at GridDP scale (tens of millions of rows) are easy — the whole set fits on one trainer's NVMe. The firehose-derived forecasting model and the ticket-text models are not, and any multimodal or embedding work is decidedly not.

The proven shape is sharded sequential files plus a streaming dataset library:

  • Shard the dataset into files of 64–512 MB — Parquet row groups for tabular, WebDataset tar shards for images/audio/text, or the library's native format (MosaicML Streaming's MDS, Ray Data blocks). Sequential reads of large objects are what object storage is good at; millions of small files are what it is terrible at.
  • Pre-shuffle at write time so that reading shards in order is already random enough; then shuffle within a buffer at read time. A global shuffle at read time over object storage is a random-I/O disaster.
  • Stream, don't stage — libraries like MosaicML Streaming, WebDataset, or Ray Data read shards directly from S3/GCS with prefetch and a local cache, so a 5 TB dataset trains on a node with 1 TB of disk and the first step starts in seconds rather than after a copy.
  • Deterministic resumption — the loader records shard index and offset per worker so a preempted job (interruptible GPUs are GridDP's own product) resumes mid-epoch without replaying or skipping samples.
train_loader.py
from streaming import StreamingDataset          # MosaicML Streaming
from torch.utils.data import DataLoader

# Shards were written pre-shuffled, 256 MB each, to s3://griddp-ml/datasets/ticket_intent/v7/
dataset = StreamingDataset(
    remote="s3://griddp-ml/datasets/ticket_intent/v7/",
    local="/nvme/cache/ticket_intent_v7",   # local shard cache, evicted LRU
    shuffle=True, shuffle_seed=42,           # deterministic across restarts
    batch_size=256,
    predownload=8 * 256,                     # keep 8 batches ahead of the GPU
    cache_limit="400gb",
)
loader = DataLoader(dataset, batch_size=256, num_workers=8, pin_memory=True,
                    prefetch_factor=4, persistent_workers=True)

# On preemption: StreamingDataset.state_dict() captures (epoch, sample offset) per rank;
# the job checkpoints it with the model and resumes exactly where it stopped.

Throughput to the GPU

The loader has to deliver bytes faster than the model eats them; everything else is secondary. Work the numbers before the first run:

  1. Samples per second the GPU can process — from a short profiling run with synthetic data: say 3,000 samples/s for the ticket-text model on one H100.
  2. Bytes per sample after decoding: 2 KB tokenized → 6 MB/s per GPU. Trivial. For an image model at 150 KB/sample and 800 samples/s it is 120 MB/s per GPU — with 8 GPUs, ~1 GB/s per node, which is where object-store throughput, NIC bandwidth, and decode CPU all start to matter.
  3. CPU decode budget — JPEG decode and augmentation are usually the real bottleneck; count cores per GPU (8–16 is typical) or move decode to the GPU (NVIDIA DALI).
  4. Object-store parallelism — a single S3 stream tops out around 50–100 MB/s; you reach 1 GB/s with 16–32 concurrent shard reads, which is what prefetch and multiple workers give you.

Then measure the only metric that matters: GPU utilization during training (and its cousin, dataloader wait time per step). Below 80% sustained, the money is going into idle silicon and the fix is in the data path, not the model. GridDP sells GPU-hours; it should be the last company to waste them.

The warehouse is not a dataloader

Pulling training data through a SQL warehouse at train time — a Snowflake or BigQuery query per epoch — is the most common expensive mistake. Warehouse compute is priced for interactive analytics, its throughput to a single client is modest, and re-running the point-in-time join per experiment multiplies cost. Build the training set once, write it to sharded files in the lake with a snapshot reference, and let the trainer read those.

Feature-computation cost

Feature pipelines are the quiet budget line: they run on a schedule whether or not anything changed, and windowed aggregations over long histories are expensive to recompute from scratch. Three levers:

  • Incremental, not full recompute. host_reliability_7d should update from the last hour's events plus a sliding-window subtraction, not rescan seven days of telemetry every hour. dbt incremental models with a lookback window, or Flink windowed aggregates, make this the default.
  • Materialize by demand. A feature computed for 200k accounts hourly, of which 4k are scored per day, is 98% waste. Compute batch features for the active population; compute on-demand features at request time from the online store's raw inputs.
  • Stream when freshness pays for it. Streaming features (chapter 06) cost a permanently running job; batch costs a burst. bid_depth_5m for pricing needs the stream; acct_spend_trend_30d for churn does not. Put each feature on the cheapest path that meets its freshness SLA.

Tag every feature pipeline with its cost per run and its consumers. A feature with no consuming model in the registry for 60 days gets its pipeline paused automatically — the registry knows, so let it act.

Egress & locality

Egress is the cost line that teams discover on the invoice. Cloud providers charge ~$0.05–0.09 per GB to move data out of a region; a 5 TB dataset pulled to GPUs in another cloud costs $250–450 per training run, and teams run many. Two rules keep it bounded:

  • Put the training data where the GPUs are, once. Replicate the sharded dataset to a bucket in the GPU region (or in the neocloud's object storage) and pin the trainer's cache there. Pay egress once per dataset version, not per experiment.
  • Move the feature store's online tier to the inference region, and keep the offline store next to the warehouse. Cross-region reads on the hot path are both slow and metered.

This is the natural bridge to the sibling guide Data Engineering for Neoclouds: the cheapest GPU-hour is worthless if the data lives three regions away.

Cost governance

Governance is not a spreadsheet; it is attribution built into the plane. Tag every dataset, feature pipeline, training job, and online-store namespace with an owner and a model, export cost by tag into the same gold marts that hold everything else, and show it back on a dashboard per team and per model. Then three policies with teeth:

  • Budgets per model tier. Tier-1 models (ranking, fraud, pricing) get a budget and an on-call; experiments get a per-scientist monthly cap with a soft warning at 80%.
  • Expiry as the default. Training materializations, experiment artifacts, and unpinned snapshots expire on a clock unless referenced by a registered model.
  • Utilization gates. A training job whose GPU utilization stays below 60% for ten minutes is flagged; repeat offenders are blocked from the shared queue until the loader is fixed.

The reporting side reuses the semantic layer: ml_cost_per_model and gpu_idle_hours are metrics defined once, beside gmv, in the same governed layer (DPE guide, chapter 08).

GridDP by the numbers

A plausible monthly cost profile for GridDP's ML data plane, at 17,000 GPUs on the fleet and five production models, after the fixes in this chapter:

LineBeforeAfterWhat changed
GPU hours for training (all models, monthly retrains + experiments)~1,900 h @ 45% util~900 h @ 85% utilSharded streaming datasets, prefetch, DALI decode for the image-quality model
Warehouse compute for training-set builds~$9k~$2kBuild once per dataset version to the lake; experiments read shards
Feature pipelines~$6k~$2.5kIncremental windows; pipelines for unused features paused; two features moved off streaming
Egress~$4k~$0.6kDatasets replicated once to the GPU region
Storage (lake + online + logs)~$2.2k~$1.4kTiering; 90-day expiry on unpinned materializations

Storage was the smallest line before and after. The thousand GPU-hours were the story — and at neocloud prices that line alone is the salary of the engineer who fixed it.

When to choose what

The cost-and-scale heuristic
  • Measure GPU utilization first. If it is below 80%, nothing else in this chapter matters until the loader is fixed.
  • Dataset fits on one node's NVMe (< ~1 TB): copy once, train locally, don't over-engineer.
  • Larger, or many workers: pre-shuffled shards of 64–512 MB + a streaming dataset library with prefetch and resumable state.
  • Never pull training data through the warehouse at train time; build once to the lake with a snapshot reference.
  • Features: incremental by default; stream only where the freshness SLA demands it; pause pipelines with no consumers.
  • Egress: one replica of each dataset version in the GPU region; online store in the inference region.
  • Governance: tag, show back, expire by default, gate on utilization.
The answer at four levels
  • L1 · Foundational. Thinks cost means storage; would compress Parquet harder; loads training data with a warehouse query per run.
  • L2 · Practitioner. Knows GPU idle is the real cost; uses a PyTorch DataLoader with workers and prefetch; materializes training sets to Parquet; tiers old data.
  • L3 · Senior. Does the throughput math (samples/s × bytes/sample vs object-store and decode limits); sharded, pre-shuffled, streaming datasets with resumable state; incremental feature pipelines routed batch vs stream by SLA; datasets replicated to the GPU region; cost attribution by model with expiry policies.
  • L4 · Staff. Prices every decision in GPU-hour equivalents; designs the plane so the cheap path is the default (snapshot-referenced datasets, registry-driven pipeline pausing, utilization gates on the shared queue); co-locates storage with compute across clouds deliberately; puts ML cost metrics in the semantic layer beside revenue so finance and ML argue from the same numbers.

Next, the three ways the whole plane can be assembled, and how to pick one — 13 · Reference Architectures Compared.

✓ Check yourself

Answer from memory before opening each one.

  1. A training job runs at 30% GPU utilisation and the team proposes better Parquet compression to cut cost. Why is this the wrong first move, and what do you measure?

    Show answer

    Storage is usually the smallest line, and idle GPU hours dominate. Measure GPU utilisation and dataloader wait time per step, and if utilisation sits below about 80%, fix the data path before anything else. See where the money goes.

  2. An image model needs 800 samples per second at 150 KB per sample on 8 GPUs. What throughput must the node sustain, and which limits does it reach first?

    Show answer

    About 120 MB/s per GPU, so around 1 GB/s per node. A single object-store stream gives roughly 50 to 100 MB/s, so you need 16 to 32 concurrent shard reads, and CPU decode of JPEGs and augmentation is often the real limit, at 8 to 16 cores per GPU. See throughput to the GPU.

  3. Why is a global shuffle at read time over object storage a problem, and what do you do at write time?

    Show answer

    It turns sequential reads of large objects into random I/O across many small ranges, which object stores handle badly. Pre-shuffle when writing shards, then shuffle within a buffer at read time. See the training data-loading path.

  4. A spot GPU is preempted mid-epoch. What state must the loader have saved for a resume that neither replays nor skips samples?

    Show answer

    The epoch, shard index and sample offset per worker or rank, restored from the loader's state and checkpointed with the model, plus the same shuffle seed. See the training data-loading path.

  5. A feature is computed hourly for 200,000 accounts, and 4,000 are scored per day. What do you change, and which other levers cut feature cost?

    Show answer

    Compute batch features only for the active population and the rest on demand, which removes about 98% of the work. Also update windows incrementally instead of rescanning, and put each feature on the cheapest path that meets its freshness SLA, pausing pipelines that have no consumer. See feature-computation cost.

  6. A scientist pulls a 5 TB dataset to GPUs in another cloud for every experiment. Estimate the egress cost per run and give the design that fixes it.

    Show answer

    At $0.05 to $0.09 per GB, about $250 to $450 a run. Replicate the sharded dataset to the GPU region once per dataset version and pin the trainer's cache there, so egress is paid once, not per experiment. See egress and locality.

Takeaway

  • The expensive resource in an ML data plane is a GPU waiting for data, so price every decision in GPU-hour equivalents and measure utilisation first.
  • A terabyte-month of warm object storage costs about as much as seven hours of one H100, so tiering matters less than keeping the accelerator busy.
  • Feed training from pre-shuffled, sharded files of 64 to 512 MB through a streaming loader with prefetch and resumable state, not from a warehouse query per run.
  • Do the throughput arithmetic before the first run: samples per second times bytes per sample, then check object-store parallelism and decode CPU.
  • Feature pipelines run whether or not anything changed, so compute incrementally, only for active entities, and stream only where freshness pays.
  • Pay egress once per dataset version, tag cost to owners and models, expire by default and gate jobs on utilisation.

With the costs priced, the remaining choice is how to assemble the whole plane. → Reference Architectures Compared

Further reading