Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions miles/utils/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2289,7 +2289,9 @@ def add_user_provided_function_arguments(parser):
"--custom-config-path",
type=str,
default=None,
help="Path to the YAML config for custom function arguments.",
help="Path to the YAML config for custom function arguments. Entries are applied onto "
"the parsed args before validation, so overriding a core argument behaves like "
"passing it on the command line.",
)
reset_arg(parser, "--padded-vocab-size", type=int, default=None)

Expand Down Expand Up @@ -2431,6 +2433,11 @@ def _resolve_ft_components(args: argparse.Namespace) -> list[str]:


def miles_validate_args(args):
# Config-file overrides must land before any validation or derivation below,
# so that derived flags (e.g. use_critic from advantage_estimator) and the
# worker topology are computed from the overridden values.
_apply_custom_config_overrides(args)

validate_dashboard_args(args)

args.ft_components = _resolve_ft_components(args)
Expand Down Expand Up @@ -2900,14 +2907,6 @@ def miles_validate_args(args):
if args.use_rollout_routing_replay:
args.use_routing_replay = True

if args.custom_config_path:
with open(args.custom_config_path) as f:
data = yaml.safe_load(f) or {}
for k, v in data.items():
if hasattr(args, k):
logger.info(f"Warning: Argument {k} is already set to {getattr(args, k)}, will override with {v}.")
setattr(args, k, v)

if args.use_rollout_indexer_replay:
args.use_indexer_replay = True
assert args.context_parallel_size == 1, "indexer replay does not support context parallelism yet"
Expand Down Expand Up @@ -2970,6 +2969,25 @@ def validate_async_off_policy_correction(args) -> None:
)


def _apply_custom_config_overrides(args) -> None:
"""Apply --custom-config-path YAML entries onto the parsed args.

Runs at the very start of `miles_validate_args`, so an overridden argument
behaves as if it had been passed on the command line: it participates in
every subsequent validation and derivation instead of silently bypassing
them. Tests call `miles_validate_args` with partial namespaces, so a
missing attribute is treated as "no config file".
"""
if not getattr(args, "custom_config_path", None):
return
with open(args.custom_config_path) as f:
data = yaml.safe_load(f) or {}
for k, v in data.items():
if hasattr(args, k):
logger.info(f"Warning: Argument {k} is already set to {getattr(args, k)}, will override with {v}.")
setattr(args, k, v)


def _maybe_apply_dumper_overrides(args) -> None:
if not args.dumper_enable:
return
Expand Down
28 changes: 28 additions & 0 deletions tests/fast/utils/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from miles.backends.sglang_utils.arguments import add_sglang_arguments
from miles.backends.sglang_utils.arguments import validate_args as validate_sglang_args
from miles.utils.arguments import (
_apply_custom_config_overrides,
_maybe_apply_dumper_overrides,
_resolve_ft_components,
get_miles_extra_args_provider,
Expand Down Expand Up @@ -496,3 +497,30 @@ def test_ppo_with_any_correction_passes(self, flag):

def test_non_ppo_estimators_are_unaffected(self):
validate_async_off_policy_correction(_make_async_ppo_args(use_critic=False))


class TestApplyCustomConfigOverrides:
def test_no_config_path_is_a_noop(self):
args = SimpleNamespace(custom_config_path=None, advantage_estimator="grpo")
_apply_custom_config_overrides(args)
assert args.advantage_estimator == "grpo"

def test_missing_attribute_is_a_noop(self):
args = SimpleNamespace(advantage_estimator="grpo")
_apply_custom_config_overrides(args)
assert args.advantage_estimator == "grpo"

def test_empty_config_is_a_noop(self, tmp_path):
config = tmp_path / "overrides.yaml"
config.write_text("")
args = SimpleNamespace(custom_config_path=str(config), advantage_estimator="grpo")
_apply_custom_config_overrides(args)
assert args.advantage_estimator == "grpo"

def test_overrides_existing_and_adds_new_keys(self, tmp_path):
config = tmp_path / "overrides.yaml"
config.write_text("advantage_estimator: ppo\nmy_custom_function_arg: 7\n")
args = SimpleNamespace(custom_config_path=str(config), advantage_estimator="grpo")
_apply_custom_config_overrides(args)
assert args.advantage_estimator == "ppo"
assert args.my_custom_function_arg == 7
Loading