388 lines
14 KiB
Python
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")
|