Make later Defaults List overrides take precedence#3315
Conversation
Replace an earlier config-sourced override of the same config group when a later one is encountered, so that the last override in depth first order wins, as documented. Command line overrides continue to win over overrides coming from a config, and an override that was already applied to a default is left in place because the group it targets is already resolved. Add regression coverage for an appended experiment overriding a config group that the primary config already overrides, and for the case where the group is resolved before a nested override is registered. Fixes facebookresearch#3229
|
This patch pre-registers all overrides from a config before traversing its children. The This fails for: defaults:
- group1: file1
- experiment: override_config_group # overrides group1 with file2
- override group1: file3There is no command-line override in this example. Both competing overrides come from Defaults Lists.
Override resolution must remain part of defaults-tree construction because selecting a config can introduce additional defaults and overrides. Please register each Defaults List override when it is encountered during the existing reverse traversal instead of registering all overrides from a config in advance. With the original first-wins behavior, reverse traversal will retain the last override in depth-first order. Command-line overrides are registered before traversal and will continue to take priority. Please add regression coverage for both this ordering and the ordering reported in issue #3229. Defaults List composition is one of the more challenging areas of Hydra. If this turns out to be more involved than you want to tackle, that is completely fine. There are other, easier issues in the Hydra 1.4 milestone that would also be valuable contributions. |
Move override registration out of _update_overrides and into the reverse traversal in _create_defaults_tree_impl, per review. add_override keeps its original first-wins behavior; since traversal is reversed, the first override registered for a key is the last one in depth first order, and command line overrides, registered before traversal, retain priority. One case needs care: an externally appended group default is the one legal way a group default can follow an override of the same group in a single defaults list, and the reverse traversal visits the appended entry before that override registers. Resolving the append therefore first registers the current list's own override for its key, preserving existing behavior for "defaults: [override db: sqlite]" composed with "+db=mysql". Add regression tests for the ordering from review (two competing Defaults List overrides, later one wins), for the appended-group case above, and keep the coverage for the issue 3229 ordering. Each of the three fails on exactly one prior version of this change.
|
Thank you for the review — you're right. I reproduced your example against my patch and it produces Reworked as you described:
One case surfaced while testing compositions against this that I want to flag explicitly, since it's a deviation from the literal prescription. An externally appended group default is the one legal way a group default can follow an override of the same group within a single defaults list, and the reverse traversal visits the appended entry before that override registers: # primary config
defaults:
- override db: sqliteOn Regression coverage for all three orderings, each failing on exactly one version of this change:
|
|
Thanks for calling this out. Your analysis of Hydra's current behavior is correct, but that behavior is a separate bug. A dangling Defaults List override should fail before Hydra processes command-line overrides, so I filed issue #3318 to track that problem. It is separate from the override-precedence bug in issue #3229, so it should be fixed separately from this PR. |
Fixes #3229
Problem
A Defaults List override inside a config appended with
+group=optiondid not override an earlier override of the same config group.Overrides.add_overriderecorded only the first override for a key, so the first override won instead of the last one in depth-first order, contradicting the documented rule.Fix
Overrides were registered per-config in
_update_overrides, before descending into that config's children — which encodes list position rather than depth-first position. Registration now happens in the reverse traversal in_create_defaults_tree_impl, at the point eachoverrideentry is encountered:add_override's existing first-wins behavior keeps it.add_overrideitself is unchanged frommainapart from a comment.Overrides.__init__, before any traversal, so they continue to take priority over Defaults List overrides._update_overridesstill validates ordering ("Overrides must be at the end of the defaults list") and rejects overrides in interpolated subtrees; it just no longer registers.+db=mysql) is the one legal way a group default can follow an override of the same group in a single defaults list, and the reverse traversal visits the appended entry before that override registers. Resolving an external append therefore first registers the current list's own override for its key, preservingmain's behavior fordefaults: [override db: sqlite]composed with+db=mysql.Override resolution remains part of defaults-tree construction, so configs selected during traversal can still introduce additional defaults and overrides.
(An earlier version of this PR pre-registered a config's overrides before traversing its children, with a replace-unless-used rule. The review pointed out an ordering it gets wrong — two competing Defaults List overrides where the deeper one is registered later — and that case is now part of the regression tests below.)
Tests
group_default_with_override++experiment=override_config_group— the appended config's override wins (the Defaults-list overrides in appended configs do not override earlier overrides #3229 ordering). Fails onmain.group1=file1on the command line — the command line still wins.group_default_with_explicit_experiment_and_override—group1: file1,experiment: override_config_group,override group1: file3: both competing overrides come from Defaults Lists and the later (file3) wins. Fails on the previous version of this patch.group1=file1— CLI priority.group_default_after_explicit_experiment— a group resolved before a nested override for it is registered resolves per its own list; no spurious "Could not override" error.group_override_only++group1=file1— a config whose defaults list only overrides a group, composed with an append of that group: the override applies. Fails without the external-append handling.pytest tests/defaults_list tests/test_compose.py→ 422 passed. Full suite excludingtest_completion→ 2055 passed (test_completionfails identically on an unmodified checkout in my environment).