use_mean_std is plumbed and serialised but never consumed

Author: jiajingc-nvCreated Aug 10, 2026Updated Sep 6, 2026

Summary

use_mean_std has a complete producer chain — model config, both setup.py construction paths, the processor constructor, save_pretrained, and the from_pretrained override list — and no consumer anywhere in gr00t/. Setting it has no effect. Its docstring says "Use mean/std normalization instead of min/max", so the name promises a normalization mode that never engages.

Meanwhile the mechanism that does implement mean/std normalization, ModalityConfig.mean_std_embedding_keys, is not populated by any embodiment in gr00t/configs/data/embodiment_configs.py. So mean/std normalization is currently unreachable through either route.

Evidence

Every occurrence on main (b9955401) is a write; there is no read:

gr00t/configs/model/gr00t_n1d7.py:120                  declaration
gr00t/model/gr00t_n1d7/setup.py:182,211                plumbed from model config
gr00t/model/gr00t_n1d7/processing_gr00t_n1d7.py:245    constructor parameter
gr00t/model/gr00t_n1d7/processing_gr00t_n1d7.py:262    stored on self
gr00t/model/gr00t_n1d7/processing_gr00t_n1d7.py:784    serialised by save_pretrained
gr00t/model/gr00t_n1d7/processing_gr00t_n1d7.py:874    listed in from_pretrained overrides

The omission is visible in one place. Gr00tN1d7Processor.__init__ forwards its sibling normalization flags to StateActionProcessor and then stores them; use_mean_std is only stored:

python
        self.state_action_processor = StateActionProcessor(
            modality_configs=modality_configs,
            statistics=statistics,
            use_percentiles=use_percentiles,
            clip_outliers=clip_outliers,
            apply_sincos_state_encoding=apply_sincos_state_encoding,
            use_relative_action=use_relative_action,
        )

        # Save state action processor settings
        self.use_percentiles = use_percentiles
        self.use_mean_std = use_mean_std      # <- forwarded nowhere

StateActionProcessor.__init__ has no use_mean_std parameter at all, so wiring it is not a one-line forward.

git log -S "use_mean_std" shows the flag arrived in 23ace64f (N1.7 Release) and never had a reader — it is dead from birth, not a regression.

For contrast, use_percentiles two lines up is forwarded and consumed at gr00t/data/state_action/state_action_processor.py:157.

Reproduce

bash
rg -n "use_mean_std" gr00t/          # writes only, no reads
rg -n "use_mean_std" gr00t/data/     # empty: never reaches the normalizer
rg -n "mean_std_embedding_keys" gr00t/configs/data/embodiment_configs.py   # empty

Why this matters

use_mean_std is serialised into every processor_config.json, so checkpoints record a normalization mode that was never applied. Anyone reading a checkpoint config to reconstruct preprocessing — including the deployment and ONNX paths — will see a flag that does not describe the data.

Resolution needs a decision from the team

Two options, and I do not have the context to pick:

  1. Wire it. Add the parameter to StateActionProcessor and make the global flag imply mean/std for all keys, presumably as a default that mean_std_embedding_keys can refine. This changes normalization math for anyone who has been setting the flag expecting it to work.
  2. Delete it. If mean_std_embedding_keys is the intended and only interface, drop the flag from the model config, the processor, and save_pretrained. This is a config-schema change affecting saved processor_config.json files, so it needs a read path that tolerates the key's absence and presence.

Happy to send a PR for whichever direction you prefer.

Note

Found while working on #744, which fixes a different failure in the same area (kwargs dropped in transit between setup.py and from_pretrained). The two are independent: use_mean_std was already in the old allowlist, so #744's diff has no effect on its behaviour.