Skip to content
Merged
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
10 changes: 7 additions & 3 deletions examples/experimental/eval/eval_delegate_rollout.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from miles.rollout.base_types import RolloutFnEvalOutput, RolloutFnTrainOutput
from miles.rollout.sglang_rollout import generate_rollout as base_generate_rollout
from miles.utils.file_arg_utils import PSEUDO_FILE_PREFIX, resolve_file_arg

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -37,19 +38,22 @@ def _get_delegate_client(args) -> EvalDelegateClient | None:
if not config_path:
return None

if config_path.startswith(PSEUDO_FILE_PREFIX):
return _build_delegate_client(args, resolve_file_arg(config_path))

config_path = str(Path(config_path).expanduser())
cache_entry = _DELEGATE_CACHE.get(config_path)
mtime = _safe_mtime(config_path)
if cache_entry and cache_entry[0] == mtime:
return cache_entry[1]

client = _build_delegate_client(args, config_path)
client = _build_delegate_client(args, resolve_file_arg(config_path))
_DELEGATE_CACHE[config_path] = (mtime, client)
return client


def _build_delegate_client(args, config_path: str) -> EvalDelegateClient | None:
cfg = OmegaConf.load(config_path)
def _build_delegate_client(args, config_text: str) -> EvalDelegateClient | None:
cfg = OmegaConf.create(config_text)
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
if not isinstance(cfg_dict, dict):
logger.warning("--eval-config must contain a mapping at the root.")
Expand Down
11 changes: 6 additions & 5 deletions miles/utils/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizerType
from miles.utils.environ import enable_experimental_ft_trainer, enable_experimental_rollout_refactor
from miles.utils.eval_config import EvalDatasetConfig, build_eval_dataset_configs, ensure_dataset_list
from miles.utils.file_arg_utils import resolve_file_arg
from miles.utils.ft_utils.health_checker import SimpleHealthCheckerConfig
from miles.utils.hf_config import is_dsa, load_hf_config
from miles.utils.logging_utils import configure_logger_raw
Expand Down Expand Up @@ -1187,7 +1188,8 @@ def add_eval_arguments(parser):
type=str,
default=None,
help=(
"Path to an OmegaConf YAML/JSON file describing evaluation datasets. "
"Path to an OmegaConf YAML/JSON file describing evaluation datasets, or an "
"inline `base64:<payload>` carrying the same document. "
"When provided, this overrides --eval-prompt-data."
),
)
Expand Down Expand Up @@ -2611,7 +2613,7 @@ 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, or an inline `base64:<payload>`.",
)
reset_arg(parser, "--padded-vocab-size", type=int, default=None)

Expand Down Expand Up @@ -2713,7 +2715,7 @@ def _resolve_eval_datasets(args) -> list[EvalDatasetConfig]:
if args.eval_config:
from omegaconf import OmegaConf

cfg = OmegaConf.load(args.eval_config)
cfg = OmegaConf.create(resolve_file_arg(args.eval_config))
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
if not isinstance(cfg_dict, dict):
raise ValueError("--eval-config must contain a mapping at the root.")
Expand Down Expand Up @@ -3423,8 +3425,7 @@ def miles_validate_args(args):
args.use_routing_replay = True

if args.custom_config_path:
with open(args.custom_config_path) as f:
data = yaml.safe_load(f) or {}
data = yaml.safe_load(resolve_file_arg(args.custom_config_path)) 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}.")
Expand Down
10 changes: 4 additions & 6 deletions miles/utils/external_utils/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
This file is not for miles framework itself, but as an optional utility to easily launch miles jobs and tests.
"""

import base64
import datetime
import json
import os
import random
import shlex
import socket
import time
from dataclasses import dataclass, field
from functools import partial
from pathlib import Path

from miles.utils.external_utils.exec_command import exec_command_cpu, exec_command_gpu, exec_command_multi_node
from miles.utils.file_arg_utils import PSEUDO_FILE_PREFIX
from miles.utils.http_utils import wait_for_server_ready
from miles.utils.typer_utils import dataclass_cli

Expand Down Expand Up @@ -332,11 +333,8 @@ def start_mooncake_master(
) from exc


def save_to_temp_file(text: str, ext: str):
path = Path(f"/tmp/miles_temp_file_{time.time()}_{random.randrange(0, 10000000)}.{ext}")
path.write_text(text)
print(f"Write the following content to {path=}: {text=}")
return str(path)
def encode_pseudo_file(text: str) -> str:
return PSEUDO_FILE_PREFIX + base64.b64encode(text.encode()).decode()


NUM_GPUS_OF_HARDWARE = {
Expand Down
11 changes: 11 additions & 0 deletions miles/utils/file_arg_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import base64
from pathlib import Path

PSEUDO_FILE_PREFIX = "base64:"


def resolve_file_arg(value: str) -> str:
"""Read a command line argument that is either a file path or an inline `base64:` payload."""
if value.startswith(PSEUDO_FILE_PREFIX):
return base64.b64decode(value[len(PSEUDO_FILE_PREFIX) :], validate=True).decode()
return Path(value).read_text(encoding="utf-8")
2 changes: 1 addition & 1 deletion scripts/amd/run_qwen3_30b_a3b.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def execute(args: ScriptArgs):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
4 changes: 2 additions & 2 deletions scripts/run_deepseek_v32.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def _execute_train(args: ScriptArgs):
config: "bf16"
""".strip()
if "--te-precision-config-file" not in args.extra_args:
misc_args += f"--te-precision-config-file {U.save_to_temp_file(te_precision_config_text, 'yaml')} "
misc_args += f"--te-precision-config-file {U.encode_pseudo_file(te_precision_config_text)} "
else:
if args.use_single_node:
sglang_world_size = 2
Expand Down Expand Up @@ -419,7 +419,7 @@ def _execute_train(args: ScriptArgs):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
2 changes: 1 addition & 1 deletion scripts/run_deepseek_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ def _train(args: ScriptArgs):
misc_args += "--transformer-impl transformer_engine " "--bf16 " "--fp8-format e4m3 " "--fp8-recipe blockwise "

if (args.train_fp8 or args.train_mxfp8) and "--te-precision-config-file" not in args.extra_args:
misc_args += f"--te-precision-config-file " f"{U.save_to_temp_file(_DSV4_TE_PRECISION_CONFIG, 'yaml')} "
misc_args += f"--te-precision-config-file " f"{U.encode_pseudo_file(_DSV4_TE_PRECISION_CONFIG)} "

train_args = (
f"{ckpt_args} "
Expand Down
2 changes: 1 addition & 1 deletion scripts/run_glm45_355b_a32b.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def _execute_train(args: ScriptArgs):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
4 changes: 2 additions & 2 deletions scripts/run_joy_ai_llm_flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def execute(args: ScriptArgs, *, wandb_file: str = __file__):
optimizer_args += (
"--optimizer-cpu-offload " "--overlap-cpu-optimizer-d2h-h2d " "--use-precision-aware-optimizer "
)
misc_args += f"--te-precision-config-file {U.save_to_temp_file(MXFP8_TE_PRECISION_CONFIG, 'yaml')} "
misc_args += f"--te-precision-config-file {U.encode_pseudo_file(MXFP8_TE_PRECISION_CONFIG)} "
else:
sglang_args += "--rollout-num-gpus-per-engine 1 " "--sglang-cuda-graph-max-bs 256 "
case _:
Expand All @@ -271,7 +271,7 @@ def execute(args: ScriptArgs, *, wandb_file: str = __file__):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
2 changes: 1 addition & 1 deletion scripts/run_mcore_fsdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def execute(args: ScriptArgs):
rm_type: ifbench
n_samples_per_eval_prompt: 1
""".strip()
eval_args += f"--eval-config {U.save_to_temp_file(eval_config_text, 'yaml')} "
eval_args += f"--eval-config {U.encode_pseudo_file(eval_config_text)} "
else:
eval_args += (
f"--eval-prompt-data aime {args.data_dir}/aime-2024/aime-2024.jsonl "
Expand Down
4 changes: 2 additions & 2 deletions scripts/run_qwen3_30b_a3b.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def execute(args: ScriptArgs):
pattern: "*"
config: "bf16"
""".strip()
misc_args += f"--te-precision-config-file {U.save_to_temp_file(te_precision_config_text, 'yaml')} "
misc_args += f"--te-precision-config-file {U.encode_pseudo_file(te_precision_config_text)} "

if args.enable_megatron_bridge:
misc_args += "--megatron-to-hf-mode bridge "
Expand Down Expand Up @@ -394,7 +394,7 @@ def execute(args: ScriptArgs):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
4 changes: 2 additions & 2 deletions scripts/run_qwen3_4b.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def execute(args: ScriptArgs):
rm_type: ifbench
n_samples_per_eval_prompt: 1
""".strip()
eval_args += f"--eval-config {U.save_to_temp_file(eval_config_text, 'yaml')} "
eval_args += f"--eval-config {U.encode_pseudo_file(eval_config_text)} "
else:
eval_args += (
f"--eval-prompt-data aime {args.data_dir}/aime-2024/aime-2024.jsonl "
Expand Down Expand Up @@ -283,7 +283,7 @@ def execute(args: ScriptArgs):
tis_batch_normalize: true
""".strip()
misc_args += (
f"--custom-config-path {U.save_to_temp_file(config_text, 'yaml')} "
f"--custom-config-path {U.encode_pseudo_file(config_text)} "
"--custom-tis-function-path examples.infra_features.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp "
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def prepare():

def execute():
os.environ.setdefault("RAY_TMPDIR", "/tmp/ray")
te_precision_config_path = U.save_to_temp_file(TE_PRECISION_CONFIG, "yaml")
te_precision_config_path = U.encode_pseudo_file(TE_PRECISION_CONFIG)

ckpt_args = f"--hf-checkpoint {MODEL_DIR}/{MODEL_NAME}-MXFP8/ " f"--ref-load {MODEL_DIR}/{MODEL_NAME}_torch_dist "

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/megatron/test_glm5_2_744b_a40b_5layer_nvfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def execute():
os.environ.update(NVFP4_ENV)
os.environ.update(GLM5_ENV)
os.environ.setdefault("RAY_TMPDIR", "/tmp/ray")
te_precision_config_path = U.save_to_temp_file(TE_PRECISION_CONFIG, "yaml")
te_precision_config_path = U.encode_pseudo_file(TE_PRECISION_CONFIG)

ckpt_args = f"--hf-checkpoint {MODEL_DIR}/{MODEL_NAME}-NVFP4/ " f"--ref-load {MODEL_DIR}/{MODEL_NAME}_torch_dist "

Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import base64
from types import SimpleNamespace

import pytest

from examples.experimental.eval import eval_delegate_rollout
from miles.utils.file_arg_utils import PSEUDO_FILE_PREFIX

_CONFIG = """
eval:
delegate:
- name: aime
"""


@pytest.fixture
def recorded_env_configs(monkeypatch):
seen = []
monkeypatch.setattr(eval_delegate_rollout, "_rebuild_delegate_config", lambda args, entries, defaults: entries)
monkeypatch.setattr(
eval_delegate_rollout.EvalDelegateClient,
"maybe_create",
classmethod(lambda cls, args, env_configs: seen.append(env_configs)),
)
eval_delegate_rollout._DELEGATE_CACHE.clear()
return seen


class TestGetDelegateClient:
def test_accepts_an_inline_eval_config(self, recorded_env_configs):
"""The main parser resolves --eval-config, so the delegate must resolve the same value too."""
encoded = base64.b64encode(_CONFIG.encode()).decode()
args = SimpleNamespace(eval_config=f"{PSEUDO_FILE_PREFIX}{encoded}")

eval_delegate_rollout._get_delegate_client(args)

assert recorded_env_configs == [[{"name": "aime"}]]

def test_accepts_a_plain_eval_config_path(self, recorded_env_configs, tmp_path):
"""A file path keeps working and is still cached by mtime."""
path = tmp_path / "eval.yaml"
path.write_text(_CONFIG)
args = SimpleNamespace(eval_config=str(path))

eval_delegate_rollout._get_delegate_client(args)

assert recorded_env_configs == [[{"name": "aime"}]]
6 changes: 3 additions & 3 deletions tests/fast/launch_scripts/py_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ def freeze_environment(monkeypatch) -> None:
def install_command_recorder(monkeypatch) -> Recording:
recording = Recording(commands=record_commands(monkeypatch), pseudo_files=[])

def fake_save_to_temp_file(text: str, ext: str) -> str:
def fake_encode_pseudo_file(text: str) -> str:
recording.pseudo_files.append(text)
return f"/frozen/pseudo_file_{len(recording.pseudo_files)}.{ext}"
return f"base64:<frozen-pseudo-file-{len(recording.pseudo_files)}>"

monkeypatch.setattr(command_utils, "create_run_id", lambda: FROZEN_RUN_ID)
monkeypatch.setattr(command_utils, "save_to_temp_file", fake_save_to_temp_file)
monkeypatch.setattr(command_utils, "encode_pseudo_file", fake_encode_pseudo_file)

return recording

Expand Down
25 changes: 16 additions & 9 deletions tests/fast/utils/test_command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from tests.fast.utils.command_recorder import record_commands

import miles.utils.external_utils.command_utils as command_utils
from miles.utils.file_arg_utils import resolve_file_arg


@pytest.fixture
Expand Down Expand Up @@ -576,16 +577,22 @@ def test_defaults_to_off(self, monkeypatch):
assert command_utils.get_env_enable_infinite_run() is True


class TestSaveToTempFile:
def test_writes_the_content_and_returns_a_unique_path(self):
"""Config text handed to a subprocess has to exist on disk under a collision-free name."""
first = command_utils.save_to_temp_file("hello: world", "yaml")
second = command_utils.save_to_temp_file("hello: world", "yaml")
class TestEncodePseudoFile:
def test_round_trips_through_resolve_file_arg(self):
"""The encoded argument is what the training process will be asked to resolve."""
encoded = command_utils.encode_pseudo_file("hello: world")

assert first != second
assert first.endswith(".yaml")
with open(first) as f:
assert f.read() == "hello: world"
assert resolve_file_arg(encoded) == "hello: world"

def test_is_deterministic(self):
"""A hot restart must recompute the identical launch command."""
assert command_utils.encode_pseudo_file("hello: world") == command_utils.encode_pseudo_file("hello: world")

def test_survives_a_command_line_round_trip(self):
"""The value is interpolated into a shell command, so it must not need quoting."""
encoded = command_utils.encode_pseudo_file("a: 1\nb: 'two words'\n")

assert shlex.split(f"--custom-config-path {encoded}")[1] == encoded


class TestHardwareTables:
Expand Down
Loading
Loading