hydra.mode discovery composes the config twice on every invocation
Description
Every Hydra app invocation composes its config twice.
_run_hydra needs to know the run mode before it can dispatch to run() or
multirun(), so it calls Hydra.get_mode(), which composes a full config
purely to read cfg.hydra.mode and then throws it away. The real composition
then happens inside run() or multirun().
# hydra/_internal/utils.py
if args.run or args.multirun:
run_mode = hydra.get_mode(config_name=config_name, overrides=overrides)
_run_app(run=args.run, multirun=args.multirun, mode=run_mode, ...)# hydra/_internal/hydra.py
def get_mode(self, config_name, overrides):
try:
cfg = self.compose_config(
config_name=config_name,
overrides=overrides,
with_log_configuration=False,
run_mode=RunMode.MULTIRUN,
validate_sweep_overrides=False,
)
return cfg.hydra.mode
except Exception:
return NoneWhy it is written this way
This is not gratuitous. run_mode is an input to composition, so there is a
genuine ordering problem: you cannot compose without a mode, and the mode may
be declared inside the config, which you can only read by composing.
run_mode affects composition in two places:
validate_sweep_overrides_legal: underRUN,x=1,2,3is rejected as ambiguous; underMULTIRUN, sweeps over the hydra node are rejected.create_defaults_list(..., skip_missing=run_mode == RunMode.MULTIRUN). That flag decides whether a config group with a missing default is skipped or raisesYou must specify 'db', e.g, db=<OPTION>, so the composed defaults list itself differs by mode.
get_mode resolves this by composing in the permissive mode
(MULTIRUN, hence skip_missing=True) with validation disabled, so the probe
succeeds whichever mode turns out to be real.
Because the fallback is except Exception: return None, the cost is invisible:
it affects latency, never correctness, and a failed probe silently defers to
flag-based dispatch. It has been this way since #2085 (March 2022) and has
never been reported.
Proposal
Require hydra.mode to come from the primary config or the command line, and
read it without composing:
- parse the overrides for a
hydra.modekey, which needs no composition; - otherwise load only the primary config via
ConfigRepository.load_config(config_name), which is one source lookup, one file read and one parse, with no defaults list traversal and therefore no dependence on the mode; - otherwise return
Noneand let flag-based dispatch apply, exactly as today.
This removes the second composition rather than optimizing around it.
What this gives up
Worth deciding deliberately, since 2_multirun.md currently promises
hydra.mode can be configured "in any supported way":
hydra.modeset from a config group or a defaults list entry, for example an experiment config carryinghydra.mode: MULTIRUN, would stop working.hydra.modeas an interpolation referring to other config nodes would stop working, since nothing is composed. A literal or a self-contained resolver such as${oc.env:...}is fine.
Neither path is exercised anywhere in this repo: no YAML here sets
hydra.mode, and every test drives it through a command line override. The
originating request, #394, only asked to default to multirun from Hydra's
configuration, which the primary config satisfies.
Both losses are detectable, so this can fail loudly rather than silently.
Notes
- The two compositions do not take identical input:
_run_appappendshydra.mode=RUNorhydra.mode=MULTIRUNto the overrides before dispatching, so the second composition sees an override the probe never did. - The blanket
except Exceptionshould be narrowed as part of this. With a single file load the failure modes are small and enumerable. - Docs would need updating:
website/docs/tutorials/basic/running_your_app/2_multirun.md.
Source: facebookresearch/hydra