Files
wehub-resource-sync e64161ec32
Release / release_and_publish (push) Waiting to run
CI / ci (3.11) (push) Has been cancelled
CI / ci (3.10) (push) Has been cancelled
CI / dependabot (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:15 +08:00

388 lines
14 KiB
Python

import json
import os
import re
import shutil
from pathlib import Path
from typing import Any, Literal
from rdagent.app.finetune.llm.conf import FT_RD_SETTING
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
from rdagent.core.experiment import FBWorkspace
from rdagent.log import rdagent_logger as logger
from rdagent.scenarios.finetune.scen.utils import _compute_column_stats
from rdagent.utils.agent.tpl import T
from rdagent.utils.env import (
BenchmarkCondaConf,
BenchmarkCondaEnv,
BenchmarkDockerConf,
BenchmarkDockerEnv,
DockerEnv,
Env,
FTCondaConf,
FTCondaEnv,
FTDockerEnv,
)
def is_docker_env(env: Env) -> bool:
"""Check if the environment is Docker-based."""
return isinstance(env, DockerEnv)
def get_workspace_prefix(env: Env) -> str:
"""Return workspace path prefix based on env type.
Docker uses /workspace as mount point, conda uses current directory.
"""
return "/workspace" if is_docker_env(env) else "."
FT_YAML_FILE_NAME = "train.yaml"
FT_DATA_PROC_FILE_NAME = "data_process.py"
FT_DEBUG_YAML_FILE_NAME = "debug_train.yaml"
FT_TEST_PARAMS_FILE_NAME = "test_params.yaml"
FT_DATA_FILE_NAME = "data.json"
FT_DATA_SCRIPT_NAME = "process_data.py"
# ENV Info: the path of the model and dataset in the container/environment
FT_MODEL_PATH = "/assets/models"
FT_DATASET_PATH = "/assets/datasets"
def get_data_processing_cache_key(local_path: str | Path) -> list[list[str]]:
"""Generate cache key based only on data processing script and dataset info.
This ensures that data processing results are reused as long as the script
and dataset configuration remain unchanged, even if other files in the
workspace (like training config) have been modified.
"""
content = []
local_path = Path(local_path)
# We only care about the script that generates data and the dataset configuration
for filename in [FT_DATA_SCRIPT_NAME, "dataset_info.json"]:
file_path = local_path / filename
if file_path.exists():
content.append([filename, file_path.read_text()])
content.sort(key=lambda x: x[0])
return content
class FTPathConfig:
"""Centralized path configuration for FT scenario.
Provides environment-aware paths for Docker vs Conda modes.
Uses lazy evaluation (properties) to avoid import-time errors.
Usage:
from rdagent.components.coder.finetune.conf import FT_PATHS
models_path = FT_PATHS.models # e.g., "/assets/models/" or "/path/to/finetune/models/"
datasets_path = FT_PATHS.datasets # e.g., "/assets/datasets/" or "/path/to/finetune/datasets/"
workspace_path = FT_PATHS.workspace # e.g., "/workspace/" or "./"
"""
@property
def is_docker(self) -> bool:
"""Check if current environment is Docker-based."""
# FIXME: the env should work in same way for docker and conda env.
# We should not expose the env type everywhere.
return FTCoderCoSTEERSettings().env_type == "docker"
@property
def models(self) -> str:
"""Model directory path (with trailing slash)."""
if self.is_docker:
return FT_MODEL_PATH + "/"
return str(FT_RD_SETTING.file_path / "models") + "/"
@property
def datasets(self) -> str:
"""Dataset directory path for raw datasets (with trailing slash)."""
if self.is_docker:
return FT_DATASET_PATH + "/"
return str(FT_RD_SETTING.file_path / "datasets") + "/"
@property
def workspace(self) -> str:
"""Workspace path prefix for prompts (with trailing slash)."""
return "/workspace/" if self.is_docker else "./"
@property
def deepspeed(self) -> str:
"""DeepSpeed config directory."""
if self.is_docker:
return "/app/examples/deepspeed/"
# Conda mode: use bundled deepspeed configs in project
# Path: conf.py -> finetune -> coder -> components -> rdagent -> scenarios/finetune/env/conda/deepspeed
rdagent_root = Path(__file__).parent.parent.parent.parent
deepspeed_path = rdagent_root / "scenarios" / "finetune" / "env" / "conda" / "deepspeed"
return str(deepspeed_path) + "/" if deepspeed_path.exists() else ""
# Singleton instance for path configuration
FT_PATHS = FTPathConfig()
class FTCoderCoSTEERSettings(CoSTEERSettings):
"""LLM Fine-tuning CoSTEER settings"""
class Config:
env_prefix = "FT_Coder_CoSTEER_"
max_seconds_multiplier: int = 8
"""LLM training takes longer, use higher multiplier"""
env_type: str = "docker"
"""Environment type for LLM fine-tuning (docker/conda)"""
extra_eval: list[str] = []
"""Extra evaluators"""
def _get_standard_ft_volumes() -> dict:
"""Get standard mount volume configuration for LLM finetune environments.
Creates standard directory mappings:
- models -> /assets/models (ro)
- datasets -> /assets/datasets (ro)
Returns:
Dictionary of local_path -> docker_mount_config mappings
"""
base_path = Path(FT_RD_SETTING.file_path)
volumes = {}
# Read-only mounts for data and models
readonly_mounts = [
("models", FT_MODEL_PATH),
("datasets", FT_DATASET_PATH),
]
for local_dir, docker_path in readonly_mounts:
local_path = base_path / local_dir
volumes[str(local_path)] = {"bind": docker_path, "mode": "ro"}
return volumes
def get_ft_env(
extra_volumes: dict = {},
operation: str = "full_training",
enable_cache: bool | None = None,
) -> Env:
"""LLM finetune dedicated environment construction function.
Automatically includes standard finetune volume mounts:
- models -> /assets/models (ro)
- datasets -> /assets/datasets (ro)
- output -> /workspace/output (rw, auto-created)
Note: .llama_factory_info is no longer automatically mounted.
Pass llama_factory_info volume via extra_volumes when needed.
Args:
extra_volumes: Additional volume mounts beyond standard ones
operation: Operation type for timeout selection.
- "data_processing": Data processing (data_processing_timeout)
- "micro_batch": Micro-batch test (micro_batch_timeout)
- "full_training": Full training (full_timeout)
enable_cache: Whether to enable caching (None means use config value)
Returns:
Configured environment ready for use
"""
conf = FTCoderCoSTEERSettings()
# Select timeout based on operation type
timeout_map = {
"data_processing": FT_RD_SETTING.data_processing_timeout,
"debug_data_processing": FT_RD_SETTING.debug_data_processing_timeout,
"micro_batch": FT_RD_SETTING.micro_batch_timeout,
"full_training": FT_RD_SETTING.full_timeout,
}
running_timeout_period = timeout_map.get(operation, FT_RD_SETTING.full_timeout)
# Use config value if enable_cache is not explicitly provided
if enable_cache is None:
enable_cache = FT_RD_SETTING.docker_enable_cache
# Use dedicated LLM docker or conda env based on config
if conf.env_type == "docker":
env = FTDockerEnv()
# Docker mode: setup volume mounts for models/datasets
standard_volumes = _get_standard_ft_volumes()
combined_volumes = standard_volumes.copy()
combined_volumes.update(extra_volumes)
env.conf.extra_volumes = combined_volumes
elif conf.env_type == "conda":
env = FTCondaEnv(conf=FTCondaConf()) # Auto-installs dependencies if env doesn't exist
# Conda mode: no volume mounts needed, use local paths directly
# extra_volumes are ignored in conda mode
else:
raise ValueError(f"Unknown env type: {conf.env_type}")
env.conf.running_timeout_period = running_timeout_period
env.conf.enable_cache = enable_cache
env.prepare()
return env
def get_data_processing_env(
enable_cache: bool | None = None,
is_debug: bool = False,
) -> tuple[Env, dict]:
"""Get environment for data processing scripts with LLM API access.
This environment is configured for running data processing scripts that may
need to call LLM APIs. It includes:
- Standard finetune volume mounts (datasets, models)
- LLM API environment variables (OPENAI_API_KEY, OPENAI_BASE_URL, etc.)
Args:
enable_cache: Whether to enable Docker caching
is_debug: Whether running in debug mode (shorter timeout, default 20 min vs 1 hour)
Returns:
Tuple of (env, env_vars) where env_vars contains LLM API keys
to be passed to env.run() as the env parameter
"""
env = get_ft_env(
operation="debug_data_processing" if is_debug else "data_processing",
enable_cache=enable_cache,
)
# Collect LLM API environment variables to pass to env.run()
llm_env_vars = {"PYTHONPATH": "./"} # Base env var
# Pass OPENAI_API_KEY directly
if api_key := os.getenv("OPENAI_API_KEY"):
llm_env_vars["OPENAI_API_KEY"] = api_key
# Read OPENAI_API_BASE from env, but pass as OPENAI_BASE_URL (OpenAI SDK expects this name)
if api_base := os.getenv("OPENAI_API_BASE"):
llm_env_vars["OPENAI_BASE_URL"] = api_base
# Pass model pools as JSON environment variables for load balancing
llm_env_vars["STRONG_MODEL_POOL"] = json.dumps(FT_RD_SETTING.strong_models)
llm_env_vars["WEAK_MODEL_POOL"] = json.dumps(FT_RD_SETTING.weak_models)
return env, llm_env_vars
def clear_workspace(workspace: FBWorkspace, env: Env) -> None:
"""
Clean the files in LLM finetune workspace.
Only keeps the files that are injected by the coder (in workspace.file_dict) and `logs`.
Args:
workspace: The workspace object containing the file dictionary.
env: The environment to execute the clean command in.
"""
target_path = workspace.workspace_path
if not target_path.exists():
return
# The cache_path is created when mounting, so the permissions changes does not work.
keep_items = {"logs", T("scenarios.data_science.share:scen.cache_path").r()}
for file_path in workspace.file_dict.keys():
top_level = Path(file_path).parts[0]
keep_items.add(top_level)
remove_items = []
for item in target_path.iterdir():
if item.name in keep_items:
continue
remove_items.append(item.name)
if remove_items:
ws_prefix = get_workspace_prefix(env)
# Construct rm command with all items to remove
# Items are relative to workspace root inside the env
items_str = " ".join([f"'{ws_prefix}/{item}'" for item in remove_items])
cmd = f"rm -rf {items_str}"
workspace.execute(env=env, entry=cmd)
def get_benchmark_env(
extra_volumes: dict = {},
timeout: int | None = None,
) -> Env:
"""OpenCompass benchmark environment construction function.
Supports both Docker and conda environments based on FT_Coder_CoSTEER_env_type.
Args:
extra_volumes: Additional volume mounts (only used in Docker mode)
timeout: Running timeout in seconds (None uses config default)
Returns:
Configured environment ready for benchmark evaluation
"""
conf = FTCoderCoSTEERSettings()
# Use benchmark-specific timeout or config default
if timeout is None:
# 0 means no timeout, use 7 days as practical "infinite"
timeout = FT_RD_SETTING.benchmark_timeout if FT_RD_SETTING.benchmark_timeout > 0 else 86400 * 7
benchmark_volumes = {}
# Setup finetune share folder mount for models
(FT_RD_SETTING.file_path / "benchmarks").mkdir(parents=True, exist_ok=True)
# NOTE: we choose a folder in the workspace as the mount point due to we may run multiple instances in same
# host machine. If conda env is used, the mount point will conflict with each other.
benchmark_volumes[str((FT_RD_SETTING.file_path / "benchmarks").resolve())] = {
"bind": "./benchmarks",
"mode": "rw",
}
env_dict = {"COMPASS_DATA_CACHE": "./benchmarks/opencompass_data"}
# Mount models directory for LoRA base model access (vLLM needs base model config)
models_path = FT_RD_SETTING.file_path / "models"
if models_path.exists():
benchmark_volumes[str(models_path.resolve())] = {"bind": FT_MODEL_PATH, "mode": "ro"}
benchmark_volumes.update(extra_volumes)
if conf.env_type == "docker":
docker_conf = BenchmarkDockerConf()
docker_conf.running_timeout_period = timeout
docker_conf.extra_volumes = benchmark_volumes
docker_conf.env_dict = env_dict
env = BenchmarkDockerEnv(conf=docker_conf)
elif conf.env_type == "conda":
# NOTE:
# We assume user has the permissions to create the softlink in the target directory.
# If we have requirements in the future, we suggest make the target directory configurable in BenchmarkCondaConf.
conda_conf = BenchmarkCondaConf()
conda_conf.running_timeout_period = timeout
conda_conf.extra_volumes = benchmark_volumes
conda_conf.env_dict = env_dict
env = BenchmarkCondaEnv(conf=conda_conf) # Auto-installs dependencies if env doesn't exist
else:
raise ValueError(f"Unknown env type: {conf.env_type}")
env.prepare()
return env
def inject_data_stats(implementation: FBWorkspace, data: list, stdout: str) -> None:
"""Compute token statistics and inject data_stats.json.
Used by both FTDataEvaluator (coding stage) and FTRunnerEvaluator (running stage).
Args:
implementation: The workspace to inject data_stats.json into
data: The data list from data.json
stdout: The stdout from process_data.py execution
"""
token_stats = _compute_column_stats(data)
data_stats = {
"total_samples": len(data),
"token_stats": token_stats,
"stdout_summary": stdout,
}
implementation.inject_files(**{"data_stats.json": json.dumps(data_stats, indent=2)})
logger.info(f"Injected data_stats.json with {len(data)} samples")