IronStratum

pi 0.5 quantile-normalization ValueError: the loud crash

Training stops on the first batch with ValueError: QUANTILES normalization mode requires q01 and q99 stats. The cause is simple. Your dataset's stats file holds no q01 and no q99, and pi0.5 normalizes state and action with those two bounds. Two fixes work. Recompute the stats, or run the job in mean and std mode. The recompute writes to $HF_LEROBOT_HOME, not to the copy your training command reads. That is where most people lose an afternoon.

This is the friendly error. It stops the run before a bad checkpoint exists, and long before a robot moves on numbers nobody checked. Its quiet sibling is worse: stats that are missing or mismatched, with no exception at all. That page is the norm stats error family. This page covers the crash in front of you. It covers the two fixes, the trap inside the first fix, and the ways a repaired run can still go wrong.

The background is short. A policy never sees your joint angles. It sees numbers shifted and scaled into a narrow band. The stats file holds that shift and that scale. Every normalization mode, and where each trainer keeps its file, is in the norm stats concept guide.

The two strings, and which one you got

The short string is what the official pi0.5 doc prints (pi0.5 policy doc):

ValueError: QUANTILES normalization mode requires q01 and q99 stats

The long string is what the training run actually raises, and it names the script to run:

ValueError: QUANTILES normalization mode requires q01 and q99 stats, please update the dataset with the correct stats using the `augment_dataset_quantile_stats.py` script

Both strings raise from the same error in the LeRobot normalizer (normalize processor source); the doc prints it shortened. Search the short one and the source will not match. Search the long one and it will.

A second, different string comes from the openpi code base:

quantile stats must be provided if use_quantile_norm is True. Key {k} is missing q01 or q99.

That one is raised when the openpi data pipeline is built (openpi transforms source). Different code, same requirement. Both errors mean one thing. The stats file needs q01 and q99.

Here is how it looks from the inside. Five reports, in the words of the people who hit them.

The fix ran, and nothing changed:

So I tried to update the stats using the command: ... However, the stats were not updated using this command.

A run that appears to hang:

Did the processing hang or did the script take to long?

Memory runs out before it finishes:

I wanted to finetune it on my custom dataset using quantiles instead of mean std but my local compute either runs out of memory or takes too long.

The loss explodes instead of crashing:

this will cause loss explosion if one dimension value of q1 and q99 is very close

And the same flag, one stack over, gives the quiet version:

The arm just spins around with success rate of 0 for all tasks.

Read them as one shape. The requirement never changes. What changes is how far the run got before some code noticed.

What the crash is really about

Quantile normalization rescales a value against two bounds: the 1st percentile and the 99th percentile of the training data. The LeRobot normalizer does it in two core lines, with an exact-zero guard on the denominator between them (normalize processor source):

denom = q99 - q01
return 2.0 * (tensor - q01) / denom - 1.0

openpi does the same thing with a small constant added to the denominator (openpi transforms source):

return (x - q01) / (q99 - q01 + 1e-6) * 2.0 - 1.0

So q01 and q99 are not extra fields. They are the two ends of the band the model expects to work inside.

Your dataset stats file holds min, max, mean, and std. Those four numbers describe a range and an average. They do not describe the 1st and 99th percentile of your own actions. A dataset recorded before quantile stats existed cannot know them. Nobody ever computed them.

One naming trap, worth a minute. In bioinformatics, quantile normalization means aligning the distribution of many samples onto a shared reference. Here it means the opposite. One number gets pulled against two bounds. Same phrase, opposite operation. That is why a search for this error returns biology papers before it returns robotics ones.

Fix one: recompute the statistics

The official command, from the pi0.5 doc (pi0.5 policy doc):

lerobot-edit-dataset \
    --repo_id your_dataset \
    --new_repo_id your_dataset \
    --operation.type recompute_stats \
    --operation.overwrite true

What it does: it reads every episode file in the dataset. It computes the stats for each feature and writes them back. The quantile keys it writes are q01, q10, q50, q90, and q99 (compute stats source).

Now the trap, in the doc's own words:

The result lands in $HF_LEROBOT_HOME/your_dataset, not the cache --dataset.repo_id reads, so train with --dataset.root=$HF_LEROBOT_HOME/your_dataset, or add --push_to_hub true above.

Work through what that means. The command succeeds. No error appears. The dataset your training command opens is still the old one, without the quantile keys. The crash comes back, word for word. Now you have two copies of your dataset on disk. And no way to tell which one the run read.

The check that ends the guessing costs one line of code. Print the resolved path of the stats file at startup, in training and in evaluation alike. If that path does not end in the directory you just wrote to, you have found the problem. You found it before you paid for a GPU hour.

The other route to the same numbers is the augmentation script. It scans every episode with a running histogram. It estimates whole-dataset quantiles.

python src/lerobot/scripts/augment_dataset_quantile_stats.py \
    --repo-id=your_dataset \
    --overwrite \
    --skip-images

Check the path in your own checkout before you run it. The older location, under src/lerobot/datasets/v30/, circulates in the wild: the issue threads carry it and the search engine's own answer to the error string repeats it. On October 26, 2026 that path returned a 404. The official doc prints the live one, src/lerobot/scripts/, and has since a June 2026 fix to the doc's own paths. Expect to meet the older one anyway.

--skip-images keeps the existing image stats and skips decoding video. --root reads a local dataset instead of the Hub.

This script is the slow one. On a large dataset it wants a lot of memory, and the operating system can kill it:

[ERROR] Pipeline failed: Command '[... -m lerobot.datasets.v30.augment_dataset_quantile_stats, --repo-id=XXX/XXX]' died with <Signals.SIGKILL: 9>

One user's log for a 1,000 episode dataset shows roughly 13 to 21 seconds per episode. That is hours, and it looks exactly like a hang. The thread asks the right question. Did the processing hang, or did the script take too long?

Fix two: run the job in mean and std mode

If you need the run to start now, override the normalization mapping on the command line (pi0.5 policy doc):

--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}'

The pi0.5 policy config defaults to identity for visual features and quantiles for both state and action (pi0.5 policy config). Your override replaces the two quantile entries with mean and std. Those numbers are already in your file. The crash has nowhere to come from.

Two things to hold onto. The mapping is per feature type, so state and action have to be changed separately. Visual features stay on identity. And this changes what the model sees. It is not the same run as the one the doc's results describe. That doc flags the same choice for its own benchmark run. Its results come from mean and std normalization, matching the released LIBERO fine-tune rather than pi0.5's quantile default.

Pick the recompute when this is a fresh fine-tune on your own data. It keeps the model on the quantile path it was trained for. Pick mean and std when you need the run to start. Also pick it when you are reproducing a published number computed that way.

Three ways a repaired run still goes wrong

One: the merged envelope is not the true quantile

Recording, resuming, and merging aggregate quantiles from per-episode summaries. The result is a conservative envelope, not whole-dataset quantiles. It takes the minimum for the low half and the maximum for the high half. The LeRobot code says so in its own comment (compute stats source):

if q_percent <= 50:
    aggregated[q_key] = np.min(quantile_values, axis=0)
else:
    aggregated[q_key] = np.max(quantile_values, axis=0)

The maintainers confirmed this on October 21, 2026. They called the envelope the documented behaviour. They named the augmentation script as the way to get whole-dataset estimates (LeRobot issue 4156).

The size of the gap is worth knowing. One published measurement on the public DROID dataset found that the shipped action bounds leave a lot outside them. Between about 12 percent and about 79 percent of all action values, depending on the dimension. Exact whole-dataset bounds leave about 2 percent. A wider band means a smaller spread of normalized values. A smaller spread means a different loss scale. That is the difference between a training run that converges and one that quietly does not.

Two: a near-zero quantile span becomes a loss spike

The crash needs a missing key. The dangerous case is a key that exists and is wrong.

One report on the openpi tracker shows a dimension where q01 and q99 almost touch. The loss runs near 0.2 for a few steps. Then it lands on 7,812,572,160, with a gradient norm of 229,180 at step 4. The run continues. The loss returns to its normal range on the next step (openpi issue 763).

A second report, on a drone dataset, found the same shape with numbers attached. A yaw channel carries a real command in about 0.55 percent of frames. Its q99 minus q01 comes to 0.00034. Both directions of a valid turn fall outside the bounds, and they normalize to roughly plus or minus 4,986. A scan of the dataset found 1,588 action elements above 1,000 in absolute value. Loss sat near 0.5 on normal steps. It hit 28,611.54 on the spike steps (AIR-VLA issue 6).

Both are loss spikes, not crashes. Neither is divergence. The run comes back. So do not debug the optimizer. Measure the span per dimension first.

Three: the flag moved, and a checkpoint that used to work stopped

The openpi data config sets one line. It decides whether quantile normalization is used (openpi training config):

use_quantile_norm=model_config.model_type != ModelType.PI0,

That is every model except pi0, which includes pi0.5. A user fine-tuning pi0.5 on an ARX arm found the value hard-coded. With it switched off, the fine-tune worked well. With it on, the performance was much worse (openpi issue 763). The thread does not settle the cause. One reply points out that the official code does not clip actions or state during normalization. Another notes the two runs used different assets files, so the comparison is not clean.

The same one-line change caused a different failure elsewhere. A released pi0-FAST LIBERO checkpoint trained with mean and std normalization. When the default flipped, the arm spun in place at a zero success rate. Six users reported the same thing (openpi issue 849). The repair is a pull request that pins the two affected configs back to mean and std. It was still open and unmerged on October 26, 2026 (pull request 971).

The lesson for a reader with a crash in front of them: the flag is a real switch. It has real consequences. And it is not always yours to set.

The ten-minute check

Run these in order. None of them needs a GPU. All of them read data you already have.

First, read the string. The long form with augment_dataset_quantile_stats.py means LeRobot. A form that names use_quantile_norm and a missing key means openpi. The requirement is the same. So is the fix. Only the commands differ.

Second, print the path of the stats file the run opened, at startup. This is the highest-value line on the page. It catches the copy problem in one stroke, and the copy problem is the documented reason a successful fix changes nothing.

Third, open that file and look for q01 and q99. If they are not there, the crash is still real. The recompute never wrote to this copy.

Fourth, subtract. For every dimension, take q99 minus q01. A span that rounds to zero is the loss-spike cause from the section above. A span far wider than the data deserves points at the merge envelope.

Fifth, if the dataset was merged or recorded across sessions, compare the span against a full scan. Do this before you trust the loss curve. The augmentation script with --skip-images gives whole-dataset estimates on the numeric features only.

Sixth, run the sanity check. Train a small ACT policy on the same dataset. It is cheap, and it uses mean and std. So it also tells you whether the task itself is learnable. If ACT cannot learn it either, no stats fix will save the run.

Prevention

The habit that removes this error is one line of code and one check.

Compute the stats for the dataset you are about to train on. Confirm that q01 and q99 are in the file before the first run. Print the stats file path at startup, every run, in training and in evaluation. Keep one stats file per robot body and per data split. Recompute whenever the dataset grows. Also recompute whenever the action format changes, and whenever you merge sources, because a merged file is an envelope. Keep the recomputed copy where the run reads it, or push it to the Hub. After any framework upgrade, run one training step and watch for the crash. A default that moved once will move again.

What a served policy owes you

When you train the policy, you own the stats file, and the checks above are the whole job. When a policy is served to you, you cannot read the other machine's disk. That pair has to travel inside the response instead. Numbers that shaped the actions have to come back with the actions, every time.

A fuller version of that contract sits in the concept guide (norm stats concept guide). Our action route reference documents the response shape. Each robot model page states its family's current status. The pi0.5 LIBERO checkpoint page covers the benchmark tune this error hits most often. For what any metered route costs, the answer lives on the pricing guide, never in prose. If you would rather fix this class of failure on hardware you own, the local versus hosted break-even guide works through that trade.

Common questions

Does this mean my dataset is corrupt? No. It means one file is missing two keys. The stats file holds min, max, mean, std, and it needs q01 and q99 next to them. The rest of your dataset is untouched.

Can I ignore the error and keep training? You cannot. The run stops on the first batch, so there is no checkpoint and no half-trained model to clean up. This is the good failure.

Which fix should I use? Recompute for a fresh fine-tune on your own data, because it keeps the model on the quantile path it was trained for. Mean and std when you need the run to start now. Also when you are reproducing a published number computed that way.

How long does the recompute take? It reads every episode file in the dataset. Minutes on a small one, hours on a large one. The augmentation script is slower still unless you pass --skip-images, and on a big dataset it can be killed for memory.

Why does the crash come back after I ran the fix? Almost always the output location. The recomputed file sits in $HF_LEROBOT_HOME while the run reads a different copy. Print both paths and compare them. If they match, the file was written and something else is missing the keys.

Is this the same quantile normalization as the genomics technique? No. In genomics it aligns the distribution of many samples onto a common reference. In a robot policy it pulls one number against two bounds. Those bounds are the 1st and 99th percentile. Same name, opposite operation, which is why this search lands in biology papers.

My run crashes with a message about use_quantile_norm instead. That is the openpi side of the same requirement. The flag is forced on for every model except pi0, and the stats file still has to carry q01 and q99. Use the openpi stats script for that path, and read the openpi norm stats guidance (openpi norm stats doc) before you point a config at a borrowed file.

The run starts now, and the arm misbehaves anyway. Then this was never the crash you had. Wrong or mismatched stats produce quiet wrong motion with no error at all. That is the norm stats error family, and it needs a different set of checks.

The short version

The crash means one file is missing two keys. Those keys are q01 and q99, the 1st and 99th percentile bounds pi0.5 normalizes against. Recompute them with lerobot-edit-dataset --operation.type recompute_stats. Then check that the training command reads the copy you just wrote. The result lands in $HF_LEROBOT_HOME, and the run may still be opening the old one. Or override the normalization mapping to mean and std, and accept that the run is no longer on the quantile path. If the loss spikes instead of crashing, measure q99 minus q01 per dimension. A near-zero span turns a valid command into a normalized value in the thousands. And when a checkpoint that used to work starts spinning the arm, suspect the flag, not your data. The concept background is in the norm stats concept guide, and the method for measuring whether a fix worked is the evaluation guide. Costs for any hosted route live on the pricing guide. You can sign up when you are ready to make a first call.

Last verified: 2026-10-26