[Haller]
Data and policies
§Data

Temporal ensembling

What ACT's temporal ensembling actually does to a chunked policy — the overlapping-chunk picture, the exponential weighting whose sign runs opposite to intuition, and an honest account of what it buys on Haller's 45% baseline.

Haller's ACT policy predicts a chunk of 100 actions from a single observation and then executes all of them before looking again. At 30 fps that is 3.33 seconds of open-loop motion per glance.

Temporal ensembling is the inference-time change that removes that. It costs no retraining — two keys in a checkpoint's config.json — which is exactly why it is worth understanding precisely rather than switching on and hoping. The mechanism is not what its name suggests, and the sign of its one tunable parameter runs opposite to almost everyone's first guess.

Companion pages: Dataset collection for the episodes this policy was trained on, and RunPod inference and finetuning for the larger generalist policies that share the same eval harness.

Why chunking exists at all

A policy that maps one observation to one action, queried 30 times a second, fails in two specific ways.

It jitters. Tiny perception noise makes consecutive actions disagree, and the arm buzzes around its trajectory instead of tracking it.

It dithers. At a genuine decision point — grasp the cube from the left or the right? — consecutive observations can flip the answer, and the arm oscillates between two plans forever instead of committing to either.

Action chunking fixes both by making the policy commit. One observation produces a whole sequence, and the sequence is internally consistent because a single forward pass produced it. Committing to a plan is what stops the dithering.

Three ways to spend a chunk

Once you have 100 predicted actions, how you consume them is a separate decision from how you produced them.

n_action_stepsQueriedCosts
Spend it all100once per 3.33 sblind between observations; a discontinuous plan swap at every chunk boundary
Spend one, discard 991every stepclosed-loop, but jitter and dithering are back — you threw away the commitment
Ensemble1every step~100× the inference; nothing else

The first is Haller's baseline. The second is the naive fix and is strictly worse than either neighbour. The third is what this page is about, and it exists because of a structural fact about the second one that is easy to miss.

The picture that explains it

Query every step and keep every chunk, and something useful accumulates. Each chunk starts one step later than the last but is still 100 long — so they overlap, and any given timestep ends up predicted many times over.

Fig. 1 — one timestep, a hundred predictions
obs t=0obs t=99100 chunkspredicted 99 steps aheadpredicted 1 step aheadtimestep 99 executes here05099150198timestep →
Every row is one chunk of 100 actions, produced from one observation, and each starts a step later than the row above. The orange line marks a single timestep. Every one of the 100 rows crosses it — the top row predicted it 99 steps in advance, the bottom row one step in advance, and the 98 rows between at every distance in between. Temporal ensembling averages that column.

By the time a timestep is actually sent to the arm, 100 different chunks have each made a prediction about it, from 100 different observations, at 100 different distances into the future. The oldest of those guessed it 99 steps in advance from a scene three seconds stale; the newest guessed it one step in advance from the current frame.

Temporal ensembling executes the weighted average of that column. That is the entire idea: you get chunking's commitment and a policy that is looking every step, instead of choosing between them.

Strictly, 100 is the steady state. At the start of an episode the buffer is still filling, and lerobot tracks this per-timestep in ensembled_actions_count — the first prediction for a timestep is averaged with weight w₀ alone, the second with w₀ and w₁, and so on.

The weighting, and the sign nearly everyone gets backwards

The weights are wᵢ = exp(−coeff · i), and the indexing is the crux:

i = 0 is the oldest prediction, not the newest.

So a positive coefficient — which is the ACT paper's default of 0.01, and what Haller is running — weights the stale predictions more heavily. Drag it and watch:

Fig. 2 — the weighting, and the sign that surprises people
+0.010
i = 0OLDESTmade 99 steps agoi = 99NEWESTmade this stepweightstale predictions weigh MORE
newest 1
0.58%
this step
newest 10
6.12%
0.33 s
newest 30
20.36%
1.0 s
oldest 10
15.05%
first seen

Mean age of the information in one executed action: 57.7 steps = 1.92 s.

Drag the coefficient. At the positive default the curve slopes down to the right: the oldest predictions carry the most weight and the freshest observation contributes 0.58% of the action actually sent to the arm. Only a negative coefficient tilts the average toward fresh information.

At +0.01 the tilt is 2.69× from oldest to newest, and the freshest observation — the one taken at this very control step — contributes 0.58% of the action the arm actually receives.

That is deliberate, and lerobot's own docstring says why: weighting new actions aggressively "may diminish the benefits of action chunking." Chase the newest prediction hard enough and you have re-invented the dithering that chunking existed to kill. The positive coefficient is a brake on that, not an oversight.

What it actually buys — stated honestly

It is tempting to describe ensembling as "the policy corrects a stale plan with fresh observations." That description is wrong, and the numbers above are why: the fresh observation contributes half a percent.

What ensembling actually is, is a low-pass filter over the policy's beliefs across time. The real benefits are three, and none of them is "fresher":

  1. Never fully blind. Information from every observation in the last 3.33 s is present in every action — thinly, but present. The baseline has literally zero new information between chunk boundaries.
  2. Variance reduction. Averaging ~100 predictions of the same instant cancels independent noise, so the approach is steadier.
  3. No seam. The baseline swaps plans discontinuously every 100 steps, which is visible in the motion. Ensembling has no such boundary.

The third figure makes the trade concrete, and it is less flattering than the pitch:

Fig. 3 — what the baseline pays, and what ensembling pays instead
baselinen_action_steps=100blind 3.33 sblind 3.33 sblind 3.33 sseamensembledn_action_steps=1observes every step · no seam3.33 s0 sinfo agebaseline sawtoothbaseline mean 1.65 sensembled · 1.92 s, constant0.0s3.3s6.7s10.0s
The baseline observes once per chunk, so the information driving the arm ages from fresh to 3.33 s stale and snaps back — and the plan changes discontinuously at every snap, which is the seam you can see in the motion. Ensembling flattens the sawtooth to a constant 1.92 s. Note where that sits: above the baseline’s average of 1.65 s, not below it. At the positive default, ensembling is on average marginally staler than the baseline. What it removes is the excursion to 3.33 s and the discontinuity — not the staleness.

Ensembling at the default coefficient is not, on average, working from fresher information. The baseline's information age sweeps 0 → 3.33 s across a chunk, averaging 1.65 s. The ensemble at +0.01 sits at a constant 1.92 sabove that average. What it removes is the excursion to 3.33 s and the discontinuity at the seam. It does not remove the staleness, and at this coefficient it slightly increases the mean.

Read that as a reason to expect a modest effect on Haller, not a dramatic one.

The gripper caveat

Haller's action vector is six joints, and the gripper is one of the six averaged dimensions.

Closing a gripper is a near-discontinuity: the right action goes from "open" to "closed" over very few steps. Averaging 100 predictions that disagree about when that transition happens smears a snap into a squeeze.

The original ACT work grasps well with ensembling on, so this is not fatal. But if the trials show the gripper closing mushily or late, that is this mechanism and not a coincidence — worth recognising rather than re-debugging from scratch.

What Haller is testing

The baseline is 9/20 = 45% real-arm success on so101_pick_cube, single top camera, 77 demos. The failures are mostly missed grasps, with a few grasped-then-dropped — no wrong-place failures and no freezing. That is a precision signature, which is what put ensembling on the list: a steadier, seam-free approach plausibly closes on the cube more accurately.

The test checkpoint is a byte-identical copy of the 90k baseline weights with exactly two keys changed:

{
  "n_action_steps": 1,
  "temporal_ensemble_coeff": 0.01
}

Those two are not independent choices. lerobot enforces the pairing in configuration_act.py:

NotImplementedError: `n_action_steps` must be 1 when using temporal ensembling.
This is because the policy needs to be queried every step to compute the
ensembled action.

The latency question, measured

Querying every step instead of every hundredth raises inference roughly 100×, and the rollout runner enforces a hard 27 Hz floor. That is exactly the kind of change that quietly drops the control rate below the floor and invalidates the comparison, so it was measured before any trial ran, on the desktop RTX 4080 SUPER:

p50p95p99sustainable
ensembling (every step)3.37 ms3.79 ms4.40 ms264 Hz
baseline (1 in 100)0.22 ms0.24 ms3.37 ms

Against the 33.33 ms budget at 30 fps that leaves 29.5 ms of headroom at p95. Not close. (The baseline's p50 is the cached-chunk path; its p99 is the every-hundredth step where the forward pass actually runs.)

Twenty hand-scored trials cannot resolve a ten-point move, and a no-change re-test returns ≥12/20 about 13% of the time. Read the result as "did something large happen" — never as a point estimate.

If +0.01 does not move the number

Do not conclude that ensembling does not help. That would be one point on a knob whose sign is the interesting part.

A negative coefficient flips the tilt toward fresh information, and tests the staleness story far more directly than the positive default does:

temporal_ensemble_coeffnewest 10oldest 10mean info age
+0.01 (ACT default, current)6.12%15.05%1.92 s
0.00 (uniform)10.00%10.00%1.65 s
−0.0115.05%6.12%1.38 s
−0.0539.61%0.44%0.63 s

Same trick each time: copy the checkpoint, change one number, no retraining. But finish the current 20 first — switching knobs mid-sample makes both halves unreadable.

Reference

  • Zhao et al., Learning Fine-Grained Bimanual Manipulation with Low-Cost HardwareAlgorithm 2 is the ensembling scheme.
  • lerobot/policies/act/modeling_act.pyACTTemporalEnsembler, including the online-average formulation that avoids caching a history.
  • lerobot/policies/act/configuration_act.py — the n_action_steps validation.

On this page