Part XX — Embodied AI & Production Systems: VLA Robotics, High-Throughput Serving & MLOps · Chapter 10

Data pipelines & feature stores (Feast)

Hook

A model's training set and its serving traffic are supposed to see the same features computed the same way. They usually don't — not because anyone wrote a bug, but because "the same raw event" can honestly produce two different feature values depending on when the pipeline looks at it.

Intuition
t=1: $10
t=2: $20
t=4: $15
t=6: $50
t=8: $5
t=9: $30
query t=5

Sum of every purchase that happened strictly before t=5: $45

Same six raw events the whole time — only the question "as of when?" changes the answer.

Six purchases, one user, no ambiguity about what happened or when. Slide the query time and watch the sum change — not because the events changed, but because "as of when?" changed.

Formalize

A feature store serves the same feature two ways: online, at prediction time, where only the past is visible by construction; and offline, when building a training set from history, where the entire future is sitting right there in the same table unless the pipeline explicitly refuses to look at it.

The correct, point-in-time feature for a label produced at time tt only uses events strictly before it:

feature(t)=e:te<tamount(e)\text{feature}(t) = \sum_{e:\, t_e < t} \text{amount}(e)
  • feature(t)\text{feature}(t) — the correct, point-in-time feature value: what the feature legitimately looked like as of time tt.
  • tt — the time a label was produced; the moment a real prediction would have been made.
  • ee — one raw event in the stream (here, a purchase).
  • tet_e — the timestamp of event ee.
  • amount(e)\text{amount}(e) — the value contributed by event ee.

A naive offline join instead grabs whatever the feature table currently holds — the latest snapshot — and attaches it to every historical label, regardless of tt:

naive(t)=feature(tnow)for every t\text{naive}(t) = \text{feature}(t_{\text{now}}) \quad \text{for every } t
  • naive(t)\text{naive}(t) — the (incorrect) feature value the naive join attaches to a label at time tt.
  • tnowt_{\text{now}} — the current time, when the offline join actually runs — not the label's own time.
  1. The leak window is every event between t and t_now

    Every event that happened after the label's own time but before the offline join actually ran gets folded into naive(t)\text{naive}(t) regardless — a real prediction made at tt could never have seen any of it.

Play
label timecorrect (point-in-time)naive (latest snapshot)leaked
t=3$30$130$100
t=6$45$130$85
t=9$100$130$30

The naive join always returns the same $130 — whatever the feature store happens to hold right now — no matter which historical moment the label actually came from.

Three training labels, the same six raw events, two joins. The correct join gives each label a different answer, because each label happened at a different moment. The naive join gives every label the same answer — today's snapshot — because it never consulted the label's own timestamp at all.

Worked example
  1. A label lands at t=6

    Point-in-time correct: only count purchases strictly before t=6 — the ones at t=1, 2, 4 — for a total of 10+10 + 20 + 15=15 = 45.

  2. The naive join ignores t=6 entirely

    It reaches for the feature table's current total, computed as of t=11 (after every event in the stream): 10+10+20+15+15+50+5+5+30 = $130.

  3. The gap is the leak

    130130 − 45 = $85 of future spending — purchases at t=6, 8, and 9 — silently informing a feature that's supposed to describe the world before t=6. A model trained on this row learns a correlation that will never exist at serving time, because serving time never has next week's purchases to leak from.

Checkpoint

A new label arrives at t=7. Pick the correct point-in-time feature value — the sum of only the purchases that happened strictly before t=7.

Pick a value to try it
Summary
feature(t)=e:te<tamount(e)naive(t)=feature(tnow)\text{feature}(t) = \sum_{e:\, t_e < t} \text{amount}(e) \qquad \neq \qquad \text{naive}(t) = \text{feature}(t_{\text{now}})

The bug never touches the raw events — they're recorded correctly either way. It lives entirely in the join: whether the pipeline asks "what did this feature look like at label time?" or silently substitutes "what does this feature look like right now?" A feature store's whole job is making the first question cheap to ask correctly, every time, instead of leaving it to whichever engineer remembers to filter by timestamp. The next chapter turns to the training runs those correctly-joined features feed into, and what it takes to reproduce one exactly.