2024-07-03 04:54:24 +08:00
"""
2025-04-04 12:08:18 +08:00
The motivation of the utils is for environment management
2024-07-03 04:54:24 +08:00
Tries to create uniform environment for the agent to run;
- All the code and data is expected included in one folder
"""
2024-09-11 15:26:52 +08:00
2024-07-24 16:56:27 +08:00
# TODO: move the scenario specific docker env into other folders.
2024-07-15 08:28:34 +00:00
2025-06-12 11:44:14 +08:00
import contextlib
2024-08-19 19:10:30 +08:00
import json
2024-07-03 04:54:24 +08:00
import os
2024-09-11 15:26:52 +08:00
import pickle
2025-01-17 22:53:05 +08:00
import re
2025-05-29 16:16:51 +08:00
import select
2025-02-14 16:24:26 +08:00
import shutil
2024-07-09 12:45:32 +08:00
import subprocess
2025-01-17 22:53:05 +08:00
import time
2024-09-11 15:26:52 +08:00
import uuid
2025-01-17 22:53:05 +08:00
import zipfile
2024-07-09 12:45:32 +08:00
from abc import abstractmethod
2026-03-02 19:04:10 +08:00
from collections import deque
2025-07-02 15:11:18 +08:00
from dataclasses import dataclass
2026-03-02 19:04:10 +08:00
from datetime import datetime
2024-07-09 12:45:32 +08:00
from pathlib import Path
2025-02-20 00:42:10 +08:00
from types import MappingProxyType
2026-03-02 19:04:10 +08:00
from typing import (
Any ,
Callable ,
Deque ,
Dict ,
Generator ,
Generic ,
Iterable ,
Mapping ,
Optional ,
TypeVar ,
cast ,
)
2024-07-15 08:28:34 +00:00
2025-01-17 22:53:05 +08:00
import docker # type: ignore[import-untyped]
import docker.models # type: ignore[import-untyped]
import docker.models.containers # type: ignore[import-untyped]
import docker.types # type: ignore[import-untyped]
2025-03-12 11:36:28 +08:00
from pydantic import BaseModel , model_validator
2025-03-05 18:30:02 +08:00
from pydantic_settings import SettingsConfigDict
2024-09-25 12:06:27 +08:00
from rich import print
2024-09-25 18:12:11 +08:00
from rich.console import Console
2026-03-02 19:04:10 +08:00
from rich.live import Live
2024-09-25 14:25:25 +08:00
from rich.progress import Progress , SpinnerColumn , TextColumn
2024-09-25 12:06:27 +08:00
from rich.rule import Rule
2024-09-25 14:25:25 +08:00
from rich.table import Table
2026-03-02 19:04:10 +08:00
from rich.text import Text
2025-04-10 12:00:27 +08:00
from tqdm import tqdm
2024-07-15 08:28:34 +00:00
2025-03-05 18:30:02 +08:00
from rdagent.core.conf import ExtendedBaseSettings
2025-01-17 22:53:05 +08:00
from rdagent.core.experiment import RD_AGENT_SETTINGS
2026-03-02 19:04:10 +08:00
from rdagent.core.utils import cache_with_pickle
2024-07-16 20:35:42 +08:00
from rdagent.log import rdagent_logger as logger
2025-01-17 22:53:05 +08:00
from rdagent.oai.llm_utils import md5_hash
2025-09-10 10:02:49 +08:00
from rdagent.utils import filter_redundant_text
2025-06-12 17:44:31 +08:00
from rdagent.utils.agent.tpl import T
2025-09-10 10:02:49 +08:00
from rdagent.utils.fmt import shrink_text
2025-02-10 08:59:46 +08:00
from rdagent.utils.workflow import wait_retry
2024-07-03 04:54:24 +08:00
2026-03-02 19:04:10 +08:00
CacheKeyFunc = Callable [[ str | Path ], list [ list [ str ]]]
def extract_dir_name_from_path_config ( path_str : str ) -> str :
"""
Extract the first directory component from a relative path string.
This is used to get the basename from path configurations like "./workspace_input/"
to use in chmod exclusion patterns.
Args:
path_str: A path string, typically from T() template configuration
Returns:
The first directory component, or empty string if not a relative path
Examples:
"./workspace_input/" -> "workspace_input"
"./assets/" -> "assets"
"/absolute/path" -> ""
"""
p = Path ( path_str )
if not p . is_absolute () and p . parts :
return p . parts [ 0 ]
return ""
2025-03-12 11:36:28 +08:00
2025-06-19 18:32:50 +08:00
def cleanup_container ( container : docker . models . containers . Container | None , context : str = "" ) -> None : # type: ignore[no-any-unimported]
"""
Shared helper function to clean up a Docker container.
Always stops the container before removing it.
Parameters
----------
container : docker container object or None
The container to clean up, or None if no container to clean up
context : str
Additional context for logging (e.g., "health check", "GPU test")
"""
if container is not None :
try :
# Always stop first - stop() doesn't raise error if already stopped
container . stop ()
container . remove ()
except Exception as cleanup_error :
# Log cleanup error but don't mask the original exception
context_str = f " { context } " if context else ""
logger . warning ( f "Failed to cleanup { context_str } container { container . id } : { cleanup_error } " )
2025-06-12 11:44:14 +08:00
# Normalize all bind paths in volumes to absolute paths using the workspace (working_dir).
def normalize_volumes ( vols : dict [ str , str | dict [ str , str ]], working_dir : str ) -> dict :
abs_vols : dict [ str , str | dict [ str , str ]] = {}
def to_abs ( path : str ) -> str :
# Converts a relative path to an absolute path using the workspace (working_dir).
return os . path . abspath ( os . path . join ( working_dir , path )) if not os . path . isabs ( path ) else path
for lp , vinfo in vols . items ():
# Support both:
# 1. {'host_path': {'bind': 'container_path', ...}}
# 2. {'host_path': 'container_path'}
if isinstance ( vinfo , dict ):
# abs_vols = cast(dict[str, dict[str, str]], abs_vols)
vinfo = vinfo . copy ()
vinfo [ "bind" ] = to_abs ( vinfo [ "bind" ])
abs_vols [ lp ] = vinfo
else :
# abs_vols = cast(dict[str, str], abs_vols)
abs_vols [ lp ] = to_abs ( vinfo )
return abs_vols
2025-04-10 12:00:27 +08:00
def pull_image_with_progress ( image : str ) -> None :
client = docker . APIClient ( base_url = "unix://var/run/docker.sock" )
pull_logs = client . pull ( image , stream = True , decode = True )
progress_bars = {}
for log in pull_logs :
if "id" in log and log . get ( "progressDetail" ):
layer_id = log [ "id" ]
progress_detail = log [ "progressDetail" ]
current = progress_detail . get ( "current" , 0 )
total = progress_detail . get ( "total" , 0 )
if total :
if layer_id not in progress_bars :
progress_bars [ layer_id ] = tqdm ( total = total , desc = f "Layer { layer_id } " , unit = "B" , unit_scale = True )
progress_bars [ layer_id ] . n = current
progress_bars [ layer_id ] . refresh ()
elif "status" in log :
print ( log [ "status" ])
for pb in progress_bars . values ():
pb . close ()
2025-03-12 18:49:00 +08:00
class EnvConf ( ExtendedBaseSettings ):
2025-03-12 11:36:28 +08:00
default_entry : str
2026-03-02 19:04:10 +08:00
env_dict : dict = {}
2025-03-12 11:36:28 +08:00
extra_volumes : dict = {}
2025-07-12 18:26:45 +08:00
running_timeout_period : int | None = 3600 # 10 minutes
2026-03-02 19:04:10 +08:00
"""it is a function to calculating hash keys"""
def get_workspace_content_for_hash ( self , local_path : str | Path ) -> list [ list [ str ]]:
"""Get content of key files in workspace for cache hash calculation.
Scans .py, .csv, and .yaml files.
"""
# we must add the information of data (beyond code) into the key.
# Otherwise, all commands operating on data will become invalid (e.g. rm -r submission.csv)
# So we recursively walk in the folder and add the sorted relative filename list as part of the key.
# data_key = []
# for path in Path(local_path).rglob("*"):
# p = str(path.relative_to(Path(local_path)))
# if p.startswith("__pycache__"):
# continue
# data_key.append(p)
# data_key = sorted(data_key)
local_path = Path ( local_path )
return [
[ str ( path . relative_to ( local_path )), path . read_text ()]
for path in sorted (
list ( local_path . rglob ( "*.py" )) + list ( local_path . rglob ( "*.csv" )) + list ( local_path . rglob ( "*.yaml" ))
)
]
redirect_stdout_to_file : bool = False
2025-03-12 11:36:28 +08:00
# helper settings to support transparent;
enable_cache : bool = True
retry_count : int = 5 # retry count for the docker run
retry_wait_seconds : int = 10 # retry wait seconds for the docker run
2026-03-02 19:04:10 +08:00
exclude_chmod_paths : list [ str ] = [] # List of directory names to exclude from chmod operation
2024-07-03 04:54:24 +08:00
2025-07-12 18:26:45 +08:00
model_config = SettingsConfigDict (
# TODO: add prefix ....
env_parse_none_str = "None" , # Nthis is the key to accept `RUNNING_TIMEOUT_PERIOD=None`
)
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
ASpecificEnvConf = TypeVar ( "ASpecificEnvConf" , bound = EnvConf )
2025-07-02 15:11:18 +08:00
@dataclass
class EnvResult :
"""
The result of running the environment.
It contains the stdout, the exit code, and the running time in seconds.
"""
2026-03-02 19:04:10 +08:00
full_stdout : str
2025-07-02 15:11:18 +08:00
exit_code : int
running_time : float
2026-03-02 19:04:10 +08:00
stored_full_stdout_to_truncated_stdout : Dict [ str , str ]
def __init__ ( self , stdout : str , exit_code : int , running_time : float ):
self . full_stdout = stdout
self . exit_code = exit_code
self . running_time = running_time
self . stored_full_stdout_to_truncated_stdout = {}
def update_stdout ( self , stdout : str ) -> None :
self . full_stdout = stdout
2025-07-02 15:11:18 +08:00
2026-03-02 19:04:10 +08:00
@property
def stdout ( self ) -> str :
if self . full_stdout not in self . stored_full_stdout_to_truncated_stdout :
truncated : str = self . _get_truncated_stdout ( self . full_stdout )
self . stored_full_stdout_to_truncated_stdout [ self . full_stdout ] = truncated
return self . stored_full_stdout_to_truncated_stdout [ self . full_stdout ]
def hash_full_stdout ( self , full_stdout : str ) -> str :
return md5_hash ( full_stdout )
@cache_with_pickle ( hash_full_stdout )
def _get_truncated_stdout ( self , full_stdout : str ) -> str :
2025-09-10 10:02:49 +08:00
return shrink_text (
2026-03-02 19:04:10 +08:00
filter_redundant_text ( full_stdout ),
2025-09-10 10:02:49 +08:00
context_lines = RD_AGENT_SETTINGS . stdout_context_len ,
line_len = RD_AGENT_SETTINGS . stdout_line_len ,
)
2025-07-02 15:11:18 +08:00
2025-03-12 11:36:28 +08:00
class Env ( Generic [ ASpecificEnvConf ]):
2024-07-03 04:54:24 +08:00
"""
2024-11-25 16:27:34 +08:00
We use BaseModel as the setting due to the features it provides
- It provides base typing and checking features.
2024-07-03 04:54:24 +08:00
- loading and dumping the information will be easier: for example, we can use package like `pydantic-yaml`
"""
2024-07-05 17:42:00 +08:00
2025-03-12 11:36:28 +08:00
conf : ASpecificEnvConf # different env have different conf.
2025-02-20 00:42:10 +08:00
2025-03-12 11:36:28 +08:00
def __init__ ( self , conf : ASpecificEnvConf ):
2024-07-03 04:54:24 +08:00
self . conf = conf
2025-03-12 11:36:28 +08:00
def zip_a_folder_into_a_file ( self , folder_path : str , zip_file_path : str ) -> None :
"""
Zip a folder into a file, use zipfile instead of subprocess
"""
with zipfile . ZipFile ( zip_file_path , "w" ) as z :
for root , _ , files in os . walk ( folder_path ):
for file in files :
2026-03-02 19:04:10 +08:00
z . write (
os . path . join ( root , file ),
os . path . relpath ( os . path . join ( root , file ), folder_path ),
)
2025-03-12 11:36:28 +08:00
2026-04-03 11:55:05 +02:00
def _safe_extract_zip ( self , z : zipfile . ZipFile , path : str , files_to_extract : list [ str ] | None = None ) -> None :
"""Extract zipfile safely, preventing path traversal attacks (CWE-22)."""
abs_path = os . path . realpath ( path )
members = [ z . getinfo ( f ) for f in files_to_extract ] if files_to_extract else z . infolist ()
for member in members :
member_path = os . path . realpath ( os . path . join ( abs_path , member . filename ))
if not member_path . startswith ( abs_path ):
raise ValueError ( f "Attempted path traversal in zip file: { member . filename } " )
if files_to_extract is not None :
for file_name in files_to_extract :
try :
z . extract ( file_name , path )
except KeyError :
logger . warning ( f "File { file_name } not found in cache zip." )
else :
z . extractall ( path = path ) # nosec B202:tarfile_unsafe_members - validated above
2026-03-02 19:04:10 +08:00
def unzip_a_file_into_a_folder (
self , zip_file_path : str , folder_path : str , files_to_extract : list [ str ] | None = None
) -> None :
2025-03-12 11:36:28 +08:00
"""
Unzip a file into a folder, use zipfile instead of subprocess
"""
2026-03-02 19:04:10 +08:00
if files_to_extract is None :
# Clear folder_path before extracting
if os . path . exists ( folder_path ):
shutil . rmtree ( folder_path )
os . makedirs ( folder_path )
2025-03-12 11:36:28 +08:00
with zipfile . ZipFile ( zip_file_path , "r" ) as z :
2026-04-03 11:55:05 +02:00
self . _safe_extract_zip ( z , folder_path , files_to_extract )
2025-03-12 11:36:28 +08:00
2024-07-03 04:54:24 +08:00
@abstractmethod
2025-01-17 22:53:05 +08:00
def prepare ( self , * args , ** kwargs ) -> None : # type: ignore[no-untyped-def]
2024-07-03 04:54:24 +08:00
"""
Prepare for the environment based on it's configure
"""
2025-07-02 15:11:18 +08:00
def check_output (
2026-03-02 19:04:10 +08:00
self ,
entry : str | None = None ,
local_path : str = "." ,
env : dict | None = None ,
running_extra_volume : Mapping = MappingProxyType ({}),
cache_key_extra_func : CacheKeyFunc | None = None ,
cache_files_to_extract : list [ str ] | None = None ,
2025-07-02 15:11:18 +08:00
) -> str :
2026-03-02 19:04:10 +08:00
result = self . run (
entry = entry ,
local_path = local_path ,
env = env ,
running_extra_volume = running_extra_volume ,
cache_key_extra_func = cache_key_extra_func ,
cache_files_to_extract = cache_files_to_extract ,
)
2025-07-02 15:11:18 +08:00
return result . stdout
2025-02-20 00:42:10 +08:00
2025-07-03 11:24:05 +08:00
def __run_with_retry (
2025-03-12 11:36:28 +08:00
self ,
entry : str | None = None ,
local_path : str = "." ,
env : dict | None = None ,
running_extra_volume : Mapping = MappingProxyType ({}),
2025-07-02 15:11:18 +08:00
) -> EnvResult :
2025-03-12 11:36:28 +08:00
for retry_index in range ( self . conf . retry_count + 1 ):
try :
2025-03-13 16:57:10 +08:00
start = time . time ()
2025-07-03 11:24:05 +08:00
log_output , return_code = self . _run (
2025-07-09 14:20:33 +08:00
entry ,
local_path ,
env ,
running_extra_volume = running_extra_volume ,
2025-03-12 11:36:28 +08:00
)
2025-03-13 16:57:10 +08:00
end = time . time ()
2025-04-07 16:24:27 +08:00
logger . info ( f "Running time: { end - start } seconds" )
2025-07-12 18:26:45 +08:00
if self . conf . running_timeout_period is not None and end - start + 1 >= self . conf . running_timeout_period :
2025-04-07 16:24:27 +08:00
logger . warning (
f "The running time exceeds { self . conf . running_timeout_period } seconds, so the process is killed."
2025-03-13 16:57:10 +08:00
)
log_output += f " \n\n The running time exceeds { self . conf . running_timeout_period } seconds, so the process is killed."
2025-07-02 15:11:18 +08:00
return EnvResult ( log_output , return_code , end - start )
2025-03-12 11:36:28 +08:00
except Exception as e :
if retry_index == self . conf . retry_count :
raise
logger . warning (
f "Error while running the container: { e } , current try index: { retry_index + 1 } , { self . conf . retry_count - retry_index - 1 } retries left."
)
time . sleep ( self . conf . retry_wait_seconds )
raise RuntimeError # for passing CI
2025-07-03 11:24:05 +08:00
def run (
2025-03-12 11:36:28 +08:00
self ,
entry : str | None = None ,
local_path : str = "." ,
env : dict | None = None ,
2026-03-02 19:04:10 +08:00
running_extra_volume : Mapping = MappingProxyType ({}),
cache_key_extra_func : CacheKeyFunc | None = None ,
cache_files_to_extract : list [ str ] | None = None ,
2025-07-02 15:11:18 +08:00
) -> EnvResult :
2025-02-20 00:42:10 +08:00
"""
2025-07-02 15:11:18 +08:00
Run the folder under the environment and return the stdout, exit code, and running time.
2025-02-20 00:42:10 +08:00
Parameters
----------
entry : str | None
We may we the entry point when we run it.
For example, we may have different entries when we run and summarize the project.
local_path : str | None
the local path (to project, mainly for code) will be mounted into the docker
Here are some examples for a None local path
- for example, run docker for updating the data in the extra_volumes.
- simply run the image. The results are produced by output or network
env : dict | None
Run the code with your specific environment.
2026-03-02 19:04:10 +08:00
running_extra_volume : Mapping
Extra volumes to mount during execution.
cache_key_extra_func : CacheKeyFunc | None
Optional function to calculate extra information for cache key calculation
cache_files_to_extract : list[str] | None
Optional list of files to extract from cache zip. If None, extract all.
2025-02-20 00:42:10 +08:00
Returns
-------
2025-07-02 15:11:18 +08:00
EnvResult: An object containing the stdout, the exit code, and the running time in seconds.
2025-02-20 00:42:10 +08:00
"""
2026-03-02 19:04:10 +08:00
_env = self . conf . env_dict . copy ()
if env :
_env . update ( env )
env = _env
2025-03-12 11:36:28 +08:00
if entry is None :
entry = self . conf . default_entry
2025-05-06 16:00:13 +08:00
if "|" in entry :
logger . warning (
"You are using a command with a shell pipeline (i.e., '|'). "
"The exit code ($exit_code) will reflect the result of "
"the last command in the pipeline." ,
)
2026-03-02 19:04:10 +08:00
# Exclude configured directories from chmod operation to prevent modifying
# read-only or specially configured directories that may produce warnings.
2025-06-12 17:44:31 +08:00
def _get_chmod_cmd ( workspace_path : str ) -> str :
2025-08-18 18:16:16 +08:00
find_cmd = f "find { workspace_path } -mindepth 1 -maxdepth 1"
2026-03-02 19:04:10 +08:00
# Use configurable exclude paths from DockerConf
for name in self . conf . exclude_chmod_paths :
if name : # Skip empty names
find_cmd += f " ! -name { name } "
2025-08-18 18:16:16 +08:00
chmod_cmd = f " { find_cmd } -exec chmod -R 777 {{}} +"
2025-06-12 17:44:31 +08:00
return chmod_cmd
2026-03-02 19:04:10 +08:00
if self . conf . redirect_stdout_to_file :
log_file_name = md5_hash ( entry )[: 8 ] + ".log"
log_file = Path ( local_path ) / f " { log_file_name } "
log_file_relative_path = log_file . relative_to ( Path ( local_path ))
entry = f " { entry } > { log_file_relative_path } 2>&1"
2025-07-12 18:26:45 +08:00
if self . conf . running_timeout_period is None :
timeout_cmd = entry
else :
timeout_cmd = f "timeout --kill-after=10 { self . conf . running_timeout_period } { entry } "
2025-03-12 11:36:28 +08:00
entry_add_timeout = (
2026-05-02 23:21:38 +02:00
"/bin/sh -c '" # start of the sh command
+ timeout_cmd . replace ( "'" , "' \\ ''" ) + "; entry_exit_code=$?; "
2025-06-12 11:44:14 +08:00
+ (
2025-06-12 21:02:07 +08:00
f " { _get_chmod_cmd ( self . conf . mount_path ) } ; "
2025-06-12 17:44:31 +08:00
if isinstance ( self . conf , DockerConf )
2025-06-12 11:44:14 +08:00
else ""
)
2025-07-12 18:26:45 +08:00
+ "exit $entry_exit_code"
+ "'" # end of the sh command
2025-03-12 11:36:28 +08:00
)
if self . conf . enable_cache :
2026-03-02 19:04:10 +08:00
result = self . cached_run (
entry_add_timeout ,
local_path ,
env ,
running_extra_volume ,
cache_key_extra_func ,
cache_files_to_extract ,
)
2025-03-12 11:36:28 +08:00
else :
2025-07-03 11:24:05 +08:00
result = self . __run_with_retry (
2025-07-09 14:20:33 +08:00
entry_add_timeout ,
local_path ,
env ,
running_extra_volume ,
2025-03-12 11:36:28 +08:00
)
2026-03-02 19:04:10 +08:00
if self . conf . redirect_stdout_to_file :
stdout = log_file . read_text ( errors = "replace" )
log_file . unlink ( missing_ok = True )
result . update_stdout ( stdout )
if str ( Path ( local_path ) . resolve ()) in result . stdout :
result . update_stdout ( result . stdout . replace ( str ( Path ( local_path ) . resolve ()), "<WORKSPACE_PATH>" ))
2025-03-12 11:36:28 +08:00
2025-07-02 15:11:18 +08:00
return result
2025-03-12 11:36:28 +08:00
def cached_run (
self ,
entry : str | None = None ,
local_path : str = "." ,
env : dict | None = None ,
running_extra_volume : Mapping = MappingProxyType ({}),
2026-03-02 19:04:10 +08:00
cache_key_extra_func : CacheKeyFunc | None = None ,
cache_files_to_extract : list [ str ] | None = None ,
2025-07-02 15:11:18 +08:00
) -> EnvResult :
2025-03-12 11:36:28 +08:00
"""
Run the folder under the environment.
Will cache the output and the folder diff for next round of running.
Use the python codes and the parameters(entry, running_extra_volume) as key to hash the input.
"""
target_folder = Path ( RD_AGENT_SETTINGS . pickle_cache_folder_path_str ) / f "utils.env.run"
target_folder . mkdir ( parents = True , exist_ok = True )
2026-03-02 19:04:10 +08:00
if cache_key_extra_func is not None :
cache_key_extra = cache_key_extra_func ( local_path )
else :
cache_key_extra = self . conf . get_workspace_content_for_hash ( local_path )
2025-03-12 11:36:28 +08:00
key = md5_hash (
2026-03-02 19:04:10 +08:00
json . dumps ( cache_key_extra )
2025-03-12 11:36:28 +08:00
+ json . dumps ({ "entry" : entry , "running_extra_volume" : dict ( running_extra_volume )})
+ json . dumps ({ "extra_volumes" : self . conf . extra_volumes })
2025-07-08 15:22:39 +08:00
# + json.dumps(data_key)
2025-03-12 11:36:28 +08:00
)
if Path ( target_folder / f " { key } .pkl" ) . exists () and Path ( target_folder / f " { key } .zip" ) . exists ():
with open ( target_folder / f " { key } .pkl" , "rb" ) as f :
2025-07-02 15:11:18 +08:00
ret = pickle . load ( f )
2026-03-02 19:04:10 +08:00
self . unzip_a_file_into_a_folder ( str ( target_folder / f " { key } .zip" ), local_path , cache_files_to_extract )
2025-03-12 11:36:28 +08:00
else :
2025-07-09 14:20:33 +08:00
ret = self . __run_with_retry ( entry , local_path , env , running_extra_volume )
2025-03-12 11:36:28 +08:00
with open ( target_folder / f " { key } .pkl" , "wb" ) as f :
pickle . dump ( ret , f )
self . zip_a_folder_into_a_file ( local_path , str ( target_folder / f " { key } .zip" ))
2025-07-02 15:11:18 +08:00
return cast ( EnvResult , ret )
2025-03-12 11:36:28 +08:00
@abstractmethod
2025-07-03 11:24:05 +08:00
def _run (
2025-03-12 11:36:28 +08:00
self ,
entry : str | None ,
local_path : str = "." ,
env : dict | None = None ,
running_extra_volume : Mapping = MappingProxyType ({}),
** kwargs : Any ,
) -> tuple [ str , int ]:
"""
Execute the specified entry point within the given environment and local path.
Parameters
----------
entry : str | None
The entry point to execute. If None, defaults to the configured entry.
local_path : str
The local directory path where the execution should occur.
env : dict | None
Environment variables to set during execution.
kwargs : dict
Additional keyword arguments for execution customization.
Returns
-------
tuple[str, int]
2025-07-02 15:11:18 +08:00
A tuple containing the standard output and the exit code.
2025-03-12 11:36:28 +08:00
"""
pass
2025-05-29 16:16:51 +08:00
def dump_python_code_run_and_get_results (
self ,
code : str ,
dump_file_names : list [ str ],
local_path : str ,
env : dict | None = None ,
running_extra_volume : Mapping = MappingProxyType ({}),
code_dump_file_py_name : Optional [ str ] = None ,
) -> tuple [ str , list ]:
"""
Dump the code into the local path and run the code.
"""
random_file_name = f " { uuid . uuid4 () } .py" if code_dump_file_py_name is None else f " { code_dump_file_py_name } .py"
with open ( os . path . join ( local_path , random_file_name ), "w" ) as f :
f . write ( code )
entry = f "python { random_file_name } "
2025-07-02 15:11:18 +08:00
log_output = self . check_output ( entry , local_path , env , running_extra_volume = dict ( running_extra_volume ))
2025-05-29 16:16:51 +08:00
results = []
os . remove ( os . path . join ( local_path , random_file_name ))
for name in dump_file_names :
if os . path . exists ( os . path . join ( local_path , f " { name } " )):
results . append ( pickle . load ( open ( os . path . join ( local_path , f " { name } " ), "rb" )))
os . remove ( os . path . join ( local_path , f " { name } " ))
else :
return log_output , []
return log_output , results
2026-03-02 19:04:10 +08:00
def refresh_env ( self ) -> None :
"""Refresh the environment, e.g., pull the latest docker image. rebuild the conda env."""
pass
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
# class EnvWithCache
#
2024-07-03 04:54:24 +08:00
## Local Environment -----
2025-03-12 11:36:28 +08:00
class LocalConf ( EnvConf ):
bin_path : str = ""
"""path like <path1>:<path2>:<path3>, which will be prepend to bin path."""
retry_count : int = 0 # retry count for; run `retry_count + 1` times
2025-07-17 17:19:10 +08:00
live_output : bool = True
2025-03-12 11:36:28 +08:00
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
ASpecificLocalConf = TypeVar ( "ASpecificLocalConf" , bound = LocalConf )
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
class LocalEnv ( Env [ ASpecificLocalConf ]):
2024-07-03 04:54:24 +08:00
"""
2025-04-04 12:08:18 +08:00
Sometimes local environment may be more convenient for testing
2024-07-03 04:54:24 +08:00
"""
2024-07-15 08:28:34 +00:00
2025-03-12 11:36:28 +08:00
def prepare ( self ) -> None : ...
2024-07-09 12:45:32 +08:00
2025-07-03 11:24:05 +08:00
def _run (
2025-02-20 00:42:10 +08:00
self ,
entry : str | None = None ,
local_path : str | None = None ,
env : dict | None = None ,
2025-03-12 11:36:28 +08:00
running_extra_volume : Mapping = MappingProxyType ({}),
2025-02-20 00:42:10 +08:00
** kwargs : dict ,
) -> tuple [ str , int ]:
2025-05-29 16:16:51 +08:00
# Handle volume links
2025-03-14 21:03:46 +08:00
volumes = {}
2025-03-12 11:36:28 +08:00
if self . conf . extra_volumes is not None :
for lp , rp in self . conf . extra_volumes . items ():
2025-09-09 11:56:42 +08:00
volumes [ lp ] = rp [ "bind" ] if isinstance ( rp , dict ) else rp
2026-04-30 19:26:29 +02:00
cache_path = "/tmp/sample" if "/sample/" in "" . join ( self . conf . extra_volumes . keys ()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
2025-04-04 12:08:18 +08:00
Path ( cache_path ) . mkdir ( parents = True , exist_ok = True )
2025-06-12 17:44:31 +08:00
volumes [ cache_path ] = T ( "scenarios.data_science.share:scen.cache_path" ) . r ()
2025-03-12 11:36:28 +08:00
for lp , rp in running_extra_volume . items ():
2025-03-14 21:03:46 +08:00
volumes [ lp ] = rp
2025-03-12 11:36:28 +08:00
2026-05-01 13:49:05 +02:00
if local_path is None :
raise ValueError ( "local_path should not be None" )
2025-06-12 11:44:14 +08:00
volumes = normalize_volumes ( volumes , local_path )
2024-07-09 12:45:32 +08:00
2025-06-12 11:44:14 +08:00
@contextlib.contextmanager
def _symlink_ctx ( vol_map : Mapping [ str , str ]) -> Generator [ None , None , None ]:
created_links : list [ Path ] = []
try :
for real , link in vol_map . items ():
link_path = Path ( link )
real_path = Path ( real )
if not link_path . parent . exists ():
link_path . parent . mkdir ( parents = True , exist_ok = True )
if link_path . exists () or link_path . is_symlink ():
link_path . unlink ()
link_path . symlink_to ( real_path )
created_links . append ( link_path )
yield
finally :
for p in created_links :
try :
if p . is_symlink () or p . exists ():
p . unlink ()
except FileNotFoundError :
pass
with _symlink_ctx ( volumes ):
# Setup environment
if env is None :
env = {}
2026-03-02 19:04:10 +08:00
# Auto-propagate CUDA_VISIBLE_DEVICES for proper GPU isolation
if "CUDA_VISIBLE_DEVICES" in os . environ and "CUDA_VISIBLE_DEVICES" not in env :
env [ "CUDA_VISIBLE_DEVICES" ] = os . environ [ "CUDA_VISIBLE_DEVICES" ]
path = [
* self . conf . bin_path . split ( ":" ),
"/bin/" ,
"/usr/bin/" ,
* env . get ( "PATH" , "" ) . split ( ":" ),
]
2025-06-12 11:44:14 +08:00
env [ "PATH" ] = ":" . join ( path )
if entry is None :
entry = self . conf . default_entry
print ( Rule ( "[bold green]LocalEnv Logs Begin[/bold green]" , style = "dark_orange" ))
table = Table ( title = "Run Info" , show_header = False )
table . add_column ( "Key" , style = "bold cyan" )
2025-06-18 14:35:45 +08:00
table . add_column ( "Value" , style = "bold magenta" )
2025-06-12 11:44:14 +08:00
table . add_row ( "Entry" , entry )
table . add_row ( "Local Path" , local_path or "" )
table . add_row ( "Env" , " \n " . join ( f " { k } : { v } " for k , v in env . items ()))
2025-06-16 19:52:44 +08:00
table . add_row ( "Volumes" , " \n " . join ( f " { k } : \n { v } " for k , v in volumes . items ()))
2025-06-12 11:44:14 +08:00
print ( table )
cwd = Path ( local_path ) . resolve () if local_path else None
env = { k : str ( v ) if isinstance ( v , int ) else v for k , v in env . items ()}
2026-04-30 19:26:29 +02:00
process = subprocess . Popen ( # nosec B602 — entry is an internal command string set by LocalEnvConf, not user input
2025-06-12 11:44:14 +08:00
entry ,
cwd = cwd ,
env = { ** os . environ , ** env },
stdout = subprocess . PIPE ,
stderr = subprocess . PIPE ,
text = True ,
shell = True ,
bufsize = 1 ,
universal_newlines = True ,
)
2025-05-29 16:16:51 +08:00
2025-06-12 11:44:14 +08:00
# Setup polling
if process . stdout is None or process . stderr is None :
raise RuntimeError ( "The subprocess did not correctly create stdout/stderr pipes" )
2025-06-18 11:54:27 +08:00
if self . conf . live_output :
stdout_fd = process . stdout . fileno ()
stderr_fd = process . stderr . fileno ()
poller = select . poll ()
poller . register ( stdout_fd , select . POLLIN )
poller . register ( stderr_fd , select . POLLIN )
combined_output = ""
while True :
if process . poll () is not None :
break
events = poller . poll ( 100 )
for fd , event in events :
if event & select . POLLIN :
if fd == stdout_fd :
while True :
output = process . stdout . readline ()
if output == "" :
break
Console () . print ( output . strip (), markup = False )
combined_output += output
elif fd == stderr_fd :
while True :
error = process . stderr . readline ()
if error == "" :
break
Console () . print ( error . strip (), markup = False )
combined_output += error
# Capture any final output
remaining_output , remaining_error = process . communicate ()
if remaining_output :
Console () . print ( remaining_output . strip (), markup = False )
combined_output += remaining_output
if remaining_error :
Console () . print ( remaining_error . strip (), markup = False )
combined_output += remaining_error
else :
# Sacrifice real-time output to avoid possible standard I/O hangs
out , err = process . communicate ()
Console () . print ( out , end = "" , markup = False )
Console () . print ( err , end = "" , markup = False )
combined_output = out + err
2025-06-12 11:44:14 +08:00
return_code = process . returncode
print ( Rule ( "[bold green]LocalEnv Logs End[/bold green]" , style = "dark_orange" ))
return combined_output , return_code
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
class CondaConf ( LocalConf ):
conda_env_name : str
default_entry : str = "python main.py"
@model_validator ( mode = "after" )
def change_bin_path ( self , ** data : Any ) -> "CondaConf" :
2026-03-02 19:04:10 +08:00
self . _update_bin_path ()
return self
def _update_bin_path ( self ) -> None :
"""Update bin_path by querying the conda environment's PATH.
This is called during initialization and can be called again after prepare()
to ensure bin_path is set correctly even if the conda env was just created.
"""
2025-03-12 11:36:28 +08:00
conda_path_result = subprocess . run (
2026-04-30 19:26:29 +02:00
[ "conda" , "run" , "-n" , self . conda_env_name , "--no-capture-output" , "env" ],
2025-03-12 11:36:28 +08:00
capture_output = True ,
text = True ,
)
2026-04-30 19:26:29 +02:00
if conda_path_result . returncode == 0 :
path_lines = [ l for l in conda_path_result . stdout . splitlines () if l . startswith ( "PATH=" )]
self . bin_path = path_lines [ 0 ] . split ( "=" , 1 )[ 1 ] if path_lines else ""
else :
self . bin_path = ""
2024-07-03 04:54:24 +08:00
2025-03-12 11:36:28 +08:00
class MLECondaConf ( CondaConf ):
enable_cache : bool = False # aligning with the docker settings.
## Docker Environment -----
2025-03-12 18:49:00 +08:00
class DockerConf ( EnvConf ):
2024-07-15 08:28:34 +00:00
build_from_dockerfile : bool = False
2024-10-24 20:38:13 +08:00
dockerfile_folder_path : Optional [ Path ] = (
None # the path to the dockerfile optional path provided when build_from_dockerfile is False
)
2024-07-15 08:28:34 +00:00
image : str # the image you want to build
2024-07-03 04:54:24 +08:00
mount_path : str # the path in the docker image to mount the folder
default_entry : str # the entry point of the image
2025-01-17 22:53:05 +08:00
extra_volumes : dict = {}
2025-06-28 20:01:14 +08:00
"""It accept a dict of volumes, which can be either
{<host_path>: <container_path>} or
{<host_path>: {"bind": <container_path>, "mode": <mode, ro/rw/default is extra_volume_mode>}}
"""
2025-01-24 14:08:38 +08:00
extra_volume_mode : str = "ro" # by default. only the mount_path should be writable, others are changed to read-only
2026-03-02 19:04:10 +08:00
exclude_chmod_paths : list [ str ] = []
"""List of directory names to exclude from chmod -R 777 operation.
This prevents modifying permissions of read-only or specially configured directories."""
# Declarative configuration for auto-populating exclude_chmod_paths from share.yaml
# Subclasses can override these to specify which config keys to read
_scenario_name : str | None = None # e.g., "data_science", "finetune"
_exclude_path_keys : list [ str ] = [] # e.g., ["input_path", "cache_path"]
2024-07-03 04:54:24 +08:00
# Sometime, we need maintain some extra data for the workspace.
# And the extra data may be shared and the downloading can be time consuming.
# So we just want to download it once.
2024-07-15 08:28:34 +00:00
network : str | None = "bridge" # the network mode for the docker
2024-07-16 10:33:53 +08:00
shm_size : str | None = None
2024-07-24 16:56:27 +08:00
enable_gpu : bool = True # because we will automatically disable GPU if not available. So we enable it by default.
2024-09-21 21:31:56 +08:00
mem_limit : str | None = "48g" # Add memory limit attribute
2025-04-09 23:24:12 +08:00
cpu_count : int | None = None # Add CPU limit attribute
2024-07-03 04:54:24 +08:00
2025-07-12 18:26:45 +08:00
running_timeout_period : int | None = 3600 # 1 hour
2024-11-06 13:14:35 +08:00
2025-01-23 16:12:22 +08:00
enable_cache : bool = True # enable the cache mechanism
2025-02-17 17:36:05 +08:00
retry_count : int = 5 # retry count for the docker run
retry_wait_seconds : int = 10 # retry wait seconds for the docker run
2026-03-02 19:04:10 +08:00
save_logs_to_file : bool = True
terminal_tail_lines : int = 20
@model_validator ( mode = "after" )
def populate_exclude_chmod_paths ( self ) -> "DockerConf" :
"""
Automatically populate exclude_chmod_paths from share.yaml configuration.
This method reads path configurations from scenarios/<scenario_name>/share.yaml
based on _scenario_name and _exclude_path_keys class attributes.
"""
if not self . exclude_chmod_paths and self . _scenario_name and self . _exclude_path_keys :
# Extract directory names from scenario configuration
self . exclude_chmod_paths = [
name
for key in self . _exclude_path_keys
if (
name := extract_dir_name_from_path_config (
T ( f "scenarios. { self . _scenario_name } .share:scen. { key } " ) . r ()
)
)
]
return self
2025-02-17 17:36:05 +08:00
2024-07-03 04:54:24 +08:00
2025-05-29 16:16:51 +08:00
class QlibCondaConf ( CondaConf ):
conda_env_name : str = "rdagent4qlib"
enable_cache : bool = False
default_entry : str = "qrun conf.yaml"
# extra_volumes: dict = {str(Path("~/.qlib/").expanduser().resolve().absolute()): "/root/.qlib/"}
class QlibCondaEnv ( LocalEnv [ QlibCondaConf ]):
def prepare ( self ) -> None :
"""Prepare the conda environment if not already created."""
try :
2026-04-30 09:34:41 +02:00
envs = subprocess . run ([ "conda" , "env" , "list" ], capture_output = True , text = True )
2025-05-29 16:16:51 +08:00
if self . conf . conda_env_name not in envs . stdout :
print ( f "[yellow]Conda env ' { self . conf . conda_env_name } ' not found, creating...[/yellow]" )
subprocess . check_call (
2026-04-30 09:34:41 +02:00
[ "conda" , "create" , "-y" , "-n" , self . conf . conda_env_name , "python=3.10" ],
2025-05-29 16:16:51 +08:00
)
subprocess . check_call (
2026-04-30 09:34:41 +02:00
[ "conda" , "run" , "-n" , self . conf . conda_env_name , "pip" , "install" , "--upgrade" , "pip" , "cython" ],
2025-05-29 16:16:51 +08:00
)
subprocess . check_call (
2026-04-30 09:34:41 +02:00
[ "conda" , "run" , "-n" , self . conf . conda_env_name , "pip" , "install" ,
"git+https://github.com/microsoft/qlib.git@2fb9380b342556ddb50a4b24e4fe8655d548b2b8" ],
2025-05-29 16:16:51 +08:00
)
subprocess . check_call (
2026-04-30 09:34:41 +02:00
[ "conda" , "run" , "-n" , self . conf . conda_env_name , "pip" , "install" ,
"catboost" , "xgboost" , "tables" , "torch" ],
2025-05-29 16:16:51 +08:00
)
2026-03-02 19:04:10 +08:00
2025-05-29 16:16:51 +08:00
except Exception as e :
print ( f "[red]Failed to prepare conda env: { e } [/red]" )
2026-03-02 19:04:10 +08:00
# ========== Conda Environment Configuration Loader ==========
# Config files location: rdagent/scenarios/finetune/env/conda/
FT_CONDA_CONFIG_DIR = Path ( __file__ ) . parent . parent / "scenarios" / "finetune" / "env" / "conda"
# Track which conda environments have been prepared in this process
# This avoids redundant pip install checks that produce verbose output
_CONDA_ENV_PREPARED : set [ str ] = set ()
def _sync_conda_cache_with_real_envs () -> None :
"""Ensure the prepared cache includes environments that already exist on disk."""
try :
result = subprocess . run (
2026-04-30 19:26:29 +02:00
[ "conda" , "env" , "list" ],
2026-03-02 19:04:10 +08:00
capture_output = True ,
text = True ,
check = False ,
)
except Exception as exc : # pragma: no cover - best-effort helper
logger . warning ( f "Failed to inspect conda env list: { exc } " )
return
env_names : set [ str ] = set ()
for line in result . stdout . splitlines ():
line = line . strip ()
if not line or line . startswith ( "#" ):
continue
# Lines look like: "base * /opt/conda"
first_column = line . split ()[ 0 ]
name = first_column . replace ( "*" , "" ) . strip ()
if name :
env_names . add ( name )
_CONDA_ENV_PREPARED . update ( env_names )
def _prepare_conda_env ( env_name : str , requirements_file : Path , python_version : str = "3.10" ) -> None :
"""Prepare conda environment with dependencies from requirements.txt.
Creates the env if it doesn't exist, then installs dependencies.
Uses a process-level cache to avoid redundant preparation in the same run.
Args:
env_name: Conda environment name
requirements_file: Path to requirements.txt file
python_version: Python version for the environment
"""
# 1. Create conda environment if not exists
2026-04-30 19:26:29 +02:00
env_list = subprocess . run ([ "conda" , "env" , "list" ], capture_output = True , text = True , check = False )
env_exists = any ( line . split ()[ 0 ] == env_name for line in env_list . stdout . splitlines () if line and not line . startswith ( "#" ))
if not env_exists :
2026-03-02 19:04:10 +08:00
print ( f "[yellow]Creating conda env ' { env_name } ' (Python { python_version } )...[/yellow]" )
2026-04-30 19:26:29 +02:00
subprocess . check_call ([ "conda" , "create" , "-y" , "-n" , env_name , f "python= { python_version } " ])
subprocess . check_call ([ "conda" , "run" , "-n" , env_name , "pip" , "install" , "--upgrade" , "pip" ])
2026-03-02 19:04:10 +08:00
print ( f "[yellow]Installing dependencies from { requirements_file . name } ...[/yellow]" )
2026-04-30 19:26:29 +02:00
subprocess . check_call ([ "conda" , "run" , "-n" , env_name , "pip" , "install" , "-r" , str ( requirements_file )])
2026-03-02 19:04:10 +08:00
print ( f "[green]Conda env ' { env_name } ' ready[/green]" )
_CONDA_ENV_PREPARED . add ( env_name )
# ========== FT (LLaMA Factory) Conda Environment ==========
class FTCondaConf ( CondaConf ):
"""Conda configuration for LLM fine-tuning environment."""
model_config = SettingsConfigDict ( env_prefix = "FT_CONDA_" )
conda_env_name : str = "llm_finetune"
default_entry : str = "llamafactory-cli version"
enable_cache : bool = False
class FTCondaEnv ( LocalEnv [ FTCondaConf ]):
"""LLaMA Factory Conda Environment with auto-dependency installation.
Requirements: rdagent/scenarios/finetune/conda/llm_finetune_requirements.txt
Docker equivalent: rdagent/scenarios/finetune/docker/llm_finetune_docker/Dockerfile
"""
def prepare ( self ) -> None :
try :
# Skip if already prepared
_sync_conda_cache_with_real_envs ()
if self . conf . conda_env_name in _CONDA_ENV_PREPARED :
return
# Step 1: Install base dependencies (torch, llamafactory, etc.)
req_file = FT_CONDA_CONFIG_DIR / "llm_finetune_requirements.txt"
_prepare_conda_env ( self . conf . conda_env_name , req_file )
# Step 2: Install flash-attn (requires torch first, uses --no-build-isolation)
# --no-cache-dir: avoid cross-filesystem hardlink error when /tmp and ~/.cache/pip are on different mounts
# Note: flash-attn>=2.8 is required for B200 (sm_100) support
print ( "[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]" )
subprocess . check_call (
2026-04-30 19:26:29 +02:00
[ "conda" , "run" , "-n" , self . conf . conda_env_name , "pip" , "install" ,
"flash-attn>=2.8" , "--no-build-isolation" , "--no-cache-dir" ],
2026-03-02 19:04:10 +08:00
)
# Re-update bin_path after prepare() in case the conda env was just created
if not self . conf . bin_path :
self . conf . _update_bin_path ()
except Exception as e :
print ( f "[red]Failed to prepare LLaMA Factory conda env: { e } [/red]" )
# ========== Benchmark (OpenCompass) Conda Environment ==========
class BenchmarkCondaConf ( CondaConf ):
"""Conda configuration for OpenCompass benchmark evaluation."""
model_config = SettingsConfigDict ( env_prefix = "BENCHMARK_CONDA_" )
conda_env_name : str = "opencompass"
default_entry : str = "opencompass --help"
enable_cache : bool = False
env_dict : dict = { "COMPASS_DATA_CACHE" : "/benchmarks/opencompass_data" }
class BenchmarkCondaEnv ( LocalEnv [ BenchmarkCondaConf ]):
"""OpenCompass Conda Environment with auto-dependency installation.
Requirements: rdagent/scenarios/finetune/conda/opencompass_requirements.txt
Docker equivalent: rdagent/scenarios/finetune/docker/opencompass/Dockerfile
"""
def prepare ( self ) -> None :
try :
# Skip if already prepared
_sync_conda_cache_with_real_envs ()
if self . conf . conda_env_name in _CONDA_ENV_PREPARED :
return
req_file = FT_CONDA_CONFIG_DIR / "opencompass_requirements.txt"
_prepare_conda_env ( self . conf . conda_env_name , req_file )
# Re-update bin_path after prepare() in case the conda env was just created
if not self . conf . bin_path :
self . conf . _update_bin_path ()
except Exception as e :
print ( f "[red]Failed to prepare OpenCompass conda env: { e } [/red]" )
2024-07-15 08:28:34 +00:00
class QlibDockerConf ( DockerConf ):
2025-07-12 18:26:45 +08:00
model_config = SettingsConfigDict (
env_prefix = "QLIB_DOCKER_" ,
env_parse_none_str = "None" , # Nthis is the key to accept `RUNNING_TIMEOUT_PERIOD=None`
)
2024-07-17 15:00:13 +08:00
2024-07-15 08:28:34 +00:00
build_from_dockerfile : bool = True
dockerfile_folder_path : Path = Path ( __file__ ) . parent . parent / "scenarios" / "qlib" / "docker"
image : str = "local_qlib:latest"
mount_path : str = "/workspace/qlib_workspace/"
default_entry : str = "qrun conf.yaml"
2025-06-28 20:01:14 +08:00
extra_volumes : dict = {
2026-03-02 19:04:10 +08:00
str ( Path ( "~/.qlib/" ) . expanduser () . resolve () . absolute ()): {
"bind" : "/root/.qlib/" ,
"mode" : "rw" ,
}
2025-06-28 20:01:14 +08:00
}
2024-07-16 10:33:53 +08:00
shm_size : str | None = "16g"
2024-07-19 16:20:07 +08:00
enable_gpu : bool = True
2025-04-23 18:48:02 +08:00
enable_cache : bool = False
2026-03-02 19:04:10 +08:00
save_logs_to_file : bool = True # Explicitly inherit from DockerConf for compatibility
2024-07-03 04:54:24 +08:00
2024-08-19 10:58:28 +08:00
class KGDockerConf ( DockerConf ):
2025-03-05 18:30:02 +08:00
model_config = SettingsConfigDict ( env_prefix = "KG_DOCKER_" )
2024-08-19 10:58:28 +08:00
build_from_dockerfile : bool = True
2024-11-15 15:40:22 +08:00
dockerfile_folder_path : Path = Path ( __file__ ) . parent . parent / "scenarios" / "kaggle" / "docker" / "kaggle_docker"
2024-08-19 10:58:28 +08:00
image : str = "local_kg:latest"
# image: str = "gcr.io/kaggle-gpu-images/python:latest"
mount_path : str = "/workspace/kg_workspace/"
default_entry : str = "python train.py"
2024-09-20 20:49:44 +08:00
# extra_volumes: dict = {
# # TODO connect to the place where the data is stored
# Path("git_ignore_folder/data").resolve(): "/root/.data/"
# }
2024-08-19 10:58:28 +08:00
2025-07-12 18:26:45 +08:00
running_timeout_period : int | None = 600
2024-11-15 16:47:44 +08:00
mem_limit : str | None = (
"48g" # Add memory limit attribute # new-york-city-taxi-fare-prediction may need more memory
)
2024-11-06 13:14:35 +08:00
2024-08-19 10:58:28 +08:00
2025-01-17 22:53:05 +08:00
class DSDockerConf ( DockerConf ):
2025-03-05 18:30:02 +08:00
model_config = SettingsConfigDict ( env_prefix = "DS_DOCKER_" )
2025-01-17 22:53:05 +08:00
2025-07-10 18:10:32 +08:00
build_from_dockerfile : bool = True
dockerfile_folder_path : Path = Path ( __file__ ) . parent . parent / "scenarios" / "kaggle" / "docker" / "DS_docker"
image : str = "local_ds:latest"
2025-01-17 22:53:05 +08:00
mount_path : str = "/kaggle/workspace"
default_entry : str = "python main.py"
2025-07-12 18:26:45 +08:00
running_timeout_period : int | None = 600
2025-01-17 22:53:05 +08:00
mem_limit : str | None = (
"48g" # Add memory limit attribute # new-york-city-taxi-fare-prediction may need more memory
)
2026-03-02 19:04:10 +08:00
# Declarative configuration: automatically loads from scenarios/data_science/share.yaml
_scenario_name : str = "data_science"
_exclude_path_keys : list [ str ] = [ "input_path" , "cache_path" ]
2025-01-17 22:53:05 +08:00
2024-11-15 15:40:22 +08:00
class MLEBDockerConf ( DockerConf ):
2025-03-05 18:30:02 +08:00
model_config = SettingsConfigDict ( env_prefix = "MLEB_DOCKER_" )
2024-11-15 15:40:22 +08:00
build_from_dockerfile : bool = True
dockerfile_folder_path : Path = Path ( __file__ ) . parent . parent / "scenarios" / "kaggle" / "docker" / "mle_bench_docker"
image : str = "local_mle:latest"
# image: str = "gcr.io/kaggle-gpu-images/python:latest"
mount_path : str = "/workspace/data_folder/"
default_entry : str = "mlebench prepare --all"
# extra_volumes: dict = {
# # TODO connect to the place where the data is stored
# Path("git_ignore_folder/data").resolve(): "/root/.data/"
# }
2024-11-15 16:47:44 +08:00
mem_limit : str | None = (
"48g" # Add memory limit attribute # new-york-city-taxi-fare-prediction may need more memory
)
2025-01-23 16:12:22 +08:00
enable_cache : bool = False
2024-11-15 15:40:22 +08:00
2026-03-02 19:04:10 +08:00
class FTDockerConf ( DockerConf ):
model_config = SettingsConfigDict ( env_prefix = "FT_DOCKER_" )
build_from_dockerfile : bool = True
dockerfile_folder_path : Path = (
Path ( __file__ ) . parent . parent / "scenarios" / "finetune" / "env" / "docker" / "llm_finetune"
)
image : str = "local_llm_finetune:latest"
mount_path : str = "/workspace/"
default_entry : str = "llamafactory-cli version"
running_timeout_period : int | None = 36000 # 10 hours for training
mem_limit : str | None = "48g" # Large memory for LLM training
shm_size : str | None = "16g" # Shared memory for multi-GPU training
enable_gpu : bool = True # Enable GPU for LLM training
enable_cache : bool = False # Disable cache to avoid conflicts during training, True for debug
# Override log output control for FT training
save_logs_to_file : bool = True
terminal_tail_lines : int = 20
# Declarative configuration: automatically loads from scenarios/finetune/share.yaml
_scenario_name : str = "finetune"
_exclude_path_keys : list [ str ] = [ "assets_path" ]
network : str | None = "host" # Use host network for finetune access to litellm proxy
def get_workspace_content_for_hash ( self , local_path : str | Path ) -> list [ list [ str ]]:
"""Include dataset_info.json in cache key calculation."""
content = super () . get_workspace_content_for_hash ( local_path )
local_path = Path ( local_path )
# Add dataset_info.json if it exists
# NOTE: data.json is excluded because it is a generated file
for path in local_path . rglob ( "dataset_info.json" ):
content . append ([ str ( path . relative_to ( local_path )), path . read_text ()])
# Sort again to ensure deterministic order (though super is sorted, appended one might not be)
content . sort ( key = lambda x : x [ 0 ])
return content
class BenchmarkDockerConf ( DockerConf ):
"""Docker configuration for OpenCompass benchmark evaluation."""
model_config = SettingsConfigDict ( env_prefix = "BENCHMARK_DOCKER_" )
build_from_dockerfile : bool = True
dockerfile_folder_path : Path = (
Path ( __file__ ) . parent . parent / "scenarios" / "finetune" / "env" / "docker" / "opencompass"
)
image : str = "rdagent-opencompass:latest"
mount_path : str = "/workspace/"
default_entry : str = "opencompass --help"
running_timeout_period : int | None = 3600 # 1 hour default for benchmarks
mem_limit : str | None = "32g" # Moderate memory for inference
shm_size : str | None = "8g" # Shared memory for model loading
enable_gpu : bool = True # Enable GPU for fast inference
enable_cache : bool = False # Disable cache for reproducibility
# Benchmark-specific log settings
save_logs_to_file : bool = True
terminal_tail_lines : int = 50 # Show more lines for benchmark progress
network : str | None = "host" # Use host network for benchmark access to litellm proxy
env_dict : dict = { "COMPASS_DATA_CACHE" : "/benchmarks/opencompass_data" }
2024-07-24 16:56:27 +08:00
# physionet.org/files/mimic-eicu-fiddle-feature/1.0.0/FIDDLE_mimic3
2024-07-03 04:54:24 +08:00
class DockerEnv ( Env [ DockerConf ]):
# TODO: Save the output into a specific file
2025-01-17 22:53:05 +08:00
def prepare ( self , * args , ** kwargs ) -> None : # type: ignore[no-untyped-def]
2024-07-03 04:54:24 +08:00
"""
Download image if it doesn't exist
"""
client = docker . from_env ()
2025-01-17 22:53:05 +08:00
if (
self . conf . build_from_dockerfile
and self . conf . dockerfile_folder_path is not None
and self . conf . dockerfile_folder_path . exists ()
):
2024-07-16 20:35:42 +08:00
logger . info ( f "Building the image from dockerfile: { self . conf . dockerfile_folder_path } " )
2024-08-19 19:10:30 +08:00
resp_stream = client . api . build (
2026-03-02 19:04:10 +08:00
path = str ( self . conf . dockerfile_folder_path ),
tag = self . conf . image ,
network_mode = self . conf . network ,
2024-07-15 08:28:34 +00:00
)
2024-08-19 19:10:30 +08:00
if isinstance ( resp_stream , str ):
logger . info ( resp_stream )
2024-09-25 14:25:25 +08:00
with Progress ( SpinnerColumn (), TextColumn ( " {task.description} " )) as p :
2024-08-19 19:10:30 +08:00
task = p . add_task ( "[cyan]Building image..." )
for part in resp_stream :
2024-10-10 11:58:15 +08:00
lines = part . decode ( "utf-8" ) . split ( " \r\n " )
for line in lines :
if line . strip ():
status_dict = json . loads ( line )
if "error" in status_dict :
2026-03-02 19:04:10 +08:00
p . update (
task ,
description = f "[red]error: { status_dict [ 'error' ] } " ,
)
2024-10-10 11:58:15 +08:00
raise docker . errors . BuildError ( status_dict [ "error" ], "" )
if "stream" in status_dict :
p . update ( task , description = status_dict [ "stream" ])
2024-07-16 20:35:42 +08:00
logger . info ( f "Finished building the image from dockerfile: { self . conf . dockerfile_folder_path } " )
2024-07-03 04:54:24 +08:00
try :
client . images . get ( self . conf . image )
except docker . errors . ImageNotFound :
2024-08-19 19:10:30 +08:00
image_pull = client . api . pull ( self . conf . image , stream = True , decode = True )
current_status = ""
layer_set = set ()
completed_layers = 0
with Progress ( TextColumn ( " {task.description} " ), TextColumn ( " {task.fields[progress]} " )) as sp :
main_task = sp . add_task ( "[cyan]Pulling image..." , progress = "" )
status_task = sp . add_task ( "[bright_magenta]layer status" , progress = "" )
for line in image_pull :
if "error" in line :
2026-03-02 19:04:10 +08:00
sp . update (
status_task ,
description = f "[red]error" ,
progress = line [ "error" ],
)
2024-08-19 19:10:30 +08:00
raise docker . errors . APIError ( line [ "error" ])
layer_id = line [ "id" ]
status = line [ "status" ]
p_text = line . get ( "progress" , None )
if layer_id not in layer_set :
layer_set . add ( layer_id )
if p_text :
current_status = p_text
if status == "Pull complete" or status == "Already exists" :
completed_layers += 1
2026-03-02 19:04:10 +08:00
sp . update (
main_task ,
progress = f "[green] { completed_layers } [white]/ { len ( layer_set ) } layers completed" ,
)
2024-08-19 19:10:30 +08:00
sp . update (
status_task ,
description = f "[bright_magenta]layer { layer_id } [yellow] { status } " ,
progress = current_status ,
)
2024-07-03 04:54:24 +08:00
except docker . errors . APIError as e :
raise RuntimeError ( f "Error while pulling the image: { e } " )
2025-01-17 22:53:05 +08:00
def _gpu_kwargs ( self , client : docker . DockerClient ) -> dict : # type: ignore[no-any-unimported]
2026-03-02 19:04:10 +08:00
"""get gpu kwargs based on its availability.
Supports GPU selection via CUDA_VISIBLE_DEVICES environment variable.
If set, only the specified GPUs will be available in the container.
Example: CUDA_VISIBLE_DEVICES=0,1 will only expose GPU 0 and 1.
"""
2024-07-19 16:20:07 +08:00
if not self . conf . enable_gpu :
return {}
2026-03-02 19:04:10 +08:00
# Check if specific GPUs are requested via CUDA_VISIBLE_DEVICES
cuda_visible = os . environ . get ( "CUDA_VISIBLE_DEVICES" )
if cuda_visible :
# Use device_ids to specify exact GPUs (cannot use count with device_ids)
device_ids = [ gpu . strip () for gpu in cuda_visible . split ( "," ) if gpu . strip ()]
gpu_kwargs = {
"device_requests" : [ docker . types . DeviceRequest ( device_ids = device_ids , capabilities = [[ "gpu" ]])],
}
logger . info ( f "GPU selection: using specific GPUs { device_ids } " )
else :
# Default: use all available GPUs
gpu_kwargs = {
"device_requests" : [ docker . types . DeviceRequest ( count =- 1 , capabilities = [[ "gpu" ]])],
}
2025-02-10 08:59:46 +08:00
2025-04-10 12:00:27 +08:00
def get_image ( image_name : str ) -> None :
try :
client . images . get ( image_name )
except docker . errors . ImageNotFound :
pull_image_with_progress ( image_name )
2025-02-10 08:59:46 +08:00
@wait_retry ( 5 , 10 )
def _f () -> dict :
2025-06-19 18:32:50 +08:00
container = None
2025-02-10 08:59:46 +08:00
try :
2025-04-10 12:00:27 +08:00
get_image ( self . conf . image )
2025-06-19 18:32:50 +08:00
container = client . containers . run ( self . conf . image , "nvidia-smi" , detach = True , ** gpu_kwargs )
# Wait for container to complete
container . wait ()
2025-02-10 08:59:46 +08:00
logger . info ( "GPU Devices are available." )
except docker . errors . APIError :
return {}
2025-06-19 18:32:50 +08:00
finally :
cleanup_container ( container , context = "GPU test" )
2025-02-10 08:59:46 +08:00
return gpu_kwargs
return _f ()
2024-07-19 16:20:07 +08:00
2026-03-02 19:04:10 +08:00
def _generate_log_header ( self , entry : str | None = None ) -> str :
"""
Generate a header for log files with execution info.
Args:
entry: Command entry that was executed
Returns:
Formatted header string
"""
timestamp = datetime . now () . strftime ( "%Y-%m- %d %H:%M:%S" )
header = "=" * 80 + " \n "
header += f "Docker Execution Log \n "
header += f "Timestamp: { timestamp } \n "
header += f "Image: { self . conf . image } \n "
if entry :
header += f "Command: { entry } \n "
header += "=" * 80 + " \n\n "
return header
def _process_container_logs ( self , logs : Iterable [ bytes ], local_path : str = "." , entry : str | None = None ) -> str :
"""
Process Docker container logs with optional tail mode.
This method can be controlled via configuration:
- save_logs_to_file: Save full logs to timestamped files in logs/ subdirectory
- terminal_tail_lines: Show only last N lines in terminal (0 = show all)
Args:
logs: Docker container log stream
local_path: Path to workspace for saving log files
entry: Command entry that was executed (for logging header)
Returns:
Complete log output as string
"""
log_output = ""
# Determine if we should use tail mode
use_tail_mode = self . conf . terminal_tail_lines > 0
save_to_file = self . conf . save_logs_to_file
# Set up log file with timestamp if needed
log_file_path = None
if save_to_file and local_path :
workspace = Path ( local_path )
# Create logs subdirectory
logs_dir = workspace / "logs"
logs_dir . mkdir ( parents = True , exist_ok = True )
timestamp = datetime . now () . strftime ( "%Y%m %d _%H%M%S" )
log_file_path = logs_dir / f "docker_execution_ { timestamp } .log"
# Write header with execution info
header = self . _generate_log_header ( entry )
with open ( log_file_path , "w" , encoding = "utf-8" ) as f :
f . write ( header )
# Also create/update a symlink to the latest log for convenience
latest_link = logs_dir / "docker_execution_latest.log"
print ( f "[cyan]Full logs will be saved to: { log_file_path . absolute () } [/cyan]" )
# Process logs with tail mode
if use_tail_mode :
log_buffer : Deque [ str ] = deque ( maxlen = self . conf . terminal_tail_lines )
def format_tail_display () -> Text :
text = Text ()
text . append (
f "[Showing last { len ( log_buffer ) } / { self . conf . terminal_tail_lines } lines" ,
style = "dim" ,
)
if log_file_path :
text . append ( f " | Full log: { log_file_path . name } ] \n " , style = "dim cyan" )
else :
text . append ( "] \n " , style = "dim" )
text . append ( "-" * 80 + " \n " , style = "dim" )
for line in log_buffer :
text . append ( line + " \n " )
return text
with Live ( format_tail_display (), refresh_per_second = 2 , console = Console ()) as live :
for log in logs :
decoded_log = log . strip () . decode ()
log_output += decoded_log + " \n "
log_buffer . append ( decoded_log )
if log_file_path :
with open ( log_file_path , "a" , encoding = "utf-8" ) as f :
f . write ( decoded_log + " \n " )
live . update ( format_tail_display ())
else :
# Default behavior: show all logs
for log in logs :
decoded_log = log . strip () . decode ()
Console () . print ( decoded_log , markup = False )
log_output += decoded_log + " \n "
if log_file_path :
with open ( log_file_path , "a" , encoding = "utf-8" ) as f :
f . write ( decoded_log + " \n " )
# Show log file location and create latest symlink
if log_file_path and log_file_path . exists ():
print ( f "[green]Full execution log saved to: { log_file_path . absolute () } [/green]" )
# Create or update symlink to latest log
latest_link = log_file_path . parent / "docker_execution_latest.log"
if latest_link . exists () or latest_link . is_symlink ():
latest_link . unlink ()
try :
latest_link . symlink_to ( log_file_path . name )
print ( f "[dim]Latest log symlink: logs/ { latest_link . name } -> { log_file_path . name } [/dim]" )
except Exception :
# Symlinks might not work on all systems (e.g., Windows without admin)
pass
return log_output
2025-07-03 11:24:05 +08:00
def _run (
2024-09-11 15:26:52 +08:00
self ,
entry : str | None = None ,
2025-01-17 22:53:05 +08:00
local_path : str = "." ,
2024-09-11 15:26:52 +08:00
env : dict | None = None ,
2025-02-20 00:42:10 +08:00
running_extra_volume : Mapping = MappingProxyType ({}),
2025-03-12 11:36:28 +08:00
** kwargs : Any ,
2025-02-20 00:42:10 +08:00
) -> tuple [ str , int ]:
2024-07-03 04:54:24 +08:00
if env is None :
env = {}
2025-01-17 22:53:05 +08:00
env [ "PYTHONWARNINGS" ] = "ignore"
env [ "TF_CPP_MIN_LOG_LEVEL" ] = "2"
2025-02-18 20:07:49 +08:00
env [ "PYTHONUNBUFFERED" ] = "1"
2026-03-02 19:04:10 +08:00
env [ "TOKENIZERS_PARALLELISM" ] = "false" # Avoid tokenizer fork warning in multi-process training
2024-07-03 04:54:24 +08:00
client = docker . from_env ()
2025-03-14 21:03:46 +08:00
volumes = {}
2024-07-03 04:54:24 +08:00
if local_path is not None :
local_path = os . path . abspath ( local_path )
2025-03-14 21:03:46 +08:00
volumes [ local_path ] = { "bind" : self . conf . mount_path , "mode" : "rw" }
2025-04-04 12:08:18 +08:00
2024-07-03 04:54:24 +08:00
if self . conf . extra_volumes is not None :
for lp , rp in self . conf . extra_volumes . items ():
2025-06-28 20:01:14 +08:00
volumes [ lp ] = rp if isinstance ( rp , dict ) else { "bind" : rp , "mode" : self . conf . extra_volume_mode }
2026-04-30 19:26:29 +02:00
cache_path = "/tmp/sample" if "/sample/" in "" . join ( self . conf . extra_volumes . keys ()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
2025-04-04 12:08:18 +08:00
Path ( cache_path ) . mkdir ( parents = True , exist_ok = True )
2026-03-02 19:04:10 +08:00
volumes [ cache_path ] = {
"bind" : T ( "scenarios.data_science.share:scen.cache_path" ) . r (),
"mode" : "rw" ,
}
2025-02-20 00:42:10 +08:00
for lp , rp in running_extra_volume . items ():
2025-06-28 20:01:14 +08:00
volumes [ lp ] = rp if isinstance ( rp , dict ) else { "bind" : rp , "mode" : self . conf . extra_volume_mode }
2024-07-03 04:54:24 +08:00
2025-06-12 11:44:14 +08:00
volumes = normalize_volumes ( cast ( dict [ str , str | dict [ str , str ]], volumes ), self . conf . mount_path )
2024-07-03 04:54:24 +08:00
log_output = ""
2025-06-19 18:32:50 +08:00
container : docker . models . containers . Container | None = None # type: ignore[no-any-unimported]
2024-07-19 16:20:07 +08:00
2024-07-03 04:54:24 +08:00
try :
2025-06-19 18:32:50 +08:00
container = client . containers . run (
2024-07-03 04:54:24 +08:00
image = self . conf . image ,
command = entry ,
2025-03-14 21:03:46 +08:00
volumes = volumes ,
2024-07-03 04:54:24 +08:00
environment = env ,
detach = True ,
working_dir = self . conf . mount_path ,
2024-07-15 08:28:34 +00:00
# auto_remove=True, # remove too fast might cause the logs not to be get
network = self . conf . network ,
2024-07-16 10:33:53 +08:00
shm_size = self . conf . shm_size ,
2024-09-21 21:31:56 +08:00
mem_limit = self . conf . mem_limit , # Set memory limit
2025-04-09 23:24:12 +08:00
cpu_count = self . conf . cpu_count , # Set CPU limit
2024-07-26 12:12:16 +08:00
** self . _gpu_kwargs ( client ),
2024-07-03 04:54:24 +08:00
)
2026-05-01 13:49:05 +02:00
if container is None :
raise AssertionError ( "Docker container was not created successfully" )
2024-07-03 04:54:24 +08:00
logs = container . logs ( stream = True )
2024-09-25 12:06:27 +08:00
print ( Rule ( "[bold green]Docker Logs Begin[/bold green]" , style = "dark_orange" ))
2024-09-25 14:25:25 +08:00
table = Table ( title = "Run Info" , show_header = False )
table . add_column ( "Key" , style = "bold cyan" )
table . add_column ( "Value" , style = "bold magenta" )
2024-11-20 17:01:18 +08:00
table . add_row ( "Image" , self . conf . image )
2024-11-15 16:47:44 +08:00
table . add_row ( "Container ID" , container . id )
table . add_row ( "Container Name" , container . name )
2024-09-25 14:25:25 +08:00
table . add_row ( "Entry" , entry )
table . add_row ( "Env" , " \n " . join ( f " { k } : { v } " for k , v in env . items ()))
2025-06-18 14:35:45 +08:00
table . add_row ( "Volumes" , " \n " . join ( f " { k } : \n { v } " for k , v in volumes . items ()))
2024-09-25 14:25:25 +08:00
print ( table )
2026-03-02 19:04:10 +08:00
# Process logs (supports tail mode if configured)
log_output = self . _process_container_logs ( logs , local_path , entry = entry )
2025-02-20 00:42:10 +08:00
exit_status = container . wait ()[ "StatusCode" ]
2025-02-19 19:09:52 +08:00
print ( Rule ( "[bold green]Docker Logs End[/bold green]" , style = "dark_orange" ))
2025-02-20 00:42:10 +08:00
return log_output , exit_status
2024-07-03 04:54:24 +08:00
except docker . errors . ContainerError as e :
raise RuntimeError ( f "Error while running the container: { e } " )
except docker . errors . ImageNotFound :
raise RuntimeError ( "Docker image not found." )
except docker . errors . APIError as e :
raise RuntimeError ( f "Error while running the container: { e } " )
2025-06-19 18:32:50 +08:00
finally :
cleanup_container ( container )
2024-07-03 04:54:24 +08:00
2026-03-02 19:04:10 +08:00
def refresh_env ( self ) -> None :
"""Remove the Docker image associated with this environment."""
client = docker . from_env ()
try :
# Remove the specific image
client . images . remove ( image = self . conf . image , force = True )
logger . info ( f "Removed Docker image: { self . conf . image } " )
client . images . prune ()
client . api . prune_builds ()
logger . info ( f "Successfully removed Docker image: { self . conf . image } " )
except docker . errors . ImageNotFound :
logger . warning ( f "Docker image not found, cannot remove: { self . conf . image } " )
except docker . errors . APIError as e :
logger . error ( f "Error while removing Docker image: { e } " )
self . prepare ()
2024-07-03 04:54:24 +08:00
class QTDockerEnv ( DockerEnv ):
"""Qlib Torch Docker"""
2026-05-02 23:21:38 +02:00
def __init__ ( self , conf : DockerConf | None = None ):
super () . __init__ ( conf if conf is not None else QlibDockerConf ())
2024-07-03 04:54:24 +08:00
2025-05-29 22:58:29 +08:00
def prepare ( self , * args , ** kwargs ) -> None : # type: ignore[no-untyped-def]
2024-07-03 04:54:24 +08:00
"""
Download image & data if it doesn't exist
"""
super () . prepare ()
qlib_data_path = next ( iter ( self . conf . extra_volumes . keys ()))
if not ( Path ( qlib_data_path ) / "qlib_data" / "cn_data" ) . exists ():
2024-07-16 20:35:42 +08:00
logger . info ( "We are downloading!" )
2024-07-03 04:54:24 +08:00
cmd = "python -m qlib.run.get_data qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn --interval 1d --delete_old False"
2025-07-02 15:11:18 +08:00
self . check_output ( entry = cmd )
2024-07-03 04:54:24 +08:00
else :
2024-07-16 20:35:42 +08:00
logger . info ( "Data already exists. Download skipped." )
2024-07-24 16:56:27 +08:00
2024-08-19 10:58:28 +08:00
class KGDockerEnv ( DockerEnv ):
2024-09-11 15:26:52 +08:00
"""Kaggle Competition Docker"""
2024-08-19 10:58:28 +08:00
2026-05-02 23:21:38 +02:00
def __init__ ( self , competition : str | None = None , conf : DockerConf | None = None ):
super () . __init__ ( conf if conf is not None else KGDockerConf ())
2024-11-15 15:40:22 +08:00
class MLEBDockerEnv ( DockerEnv ):
"""MLEBench Docker"""
2026-05-02 23:21:38 +02:00
def __init__ ( self , conf : DockerConf | None = None ):
super () . __init__ ( conf if conf is not None else MLEBDockerConf ())
2026-03-02 19:04:10 +08:00
class FTDockerEnv ( DockerEnv ):
"""
LLM Fine-tuning Docker Environment with improved log output control.
FTDockerConf enables:
- save_logs_to_file: True (saves full logs to workspace/docker_execution.log)
- terminal_tail_lines: 20 (only shows last 20 lines in terminal)
To customize, set environment variables:
export FT_DOCKER_terminal_tail_lines=50 # show last 50 lines
export FT_DOCKER_save_logs_to_file=false # disable log file
"""
2026-05-02 23:21:38 +02:00
def __init__ ( self , conf : DockerConf | None = None ):
super () . __init__ ( conf if conf is not None else FTDockerConf ())
2026-03-02 19:04:10 +08:00
class BenchmarkDockerEnv ( DockerEnv ):
"""
OpenCompass Benchmark Docker Environment.
Uses BenchmarkDockerConf for evaluation-specific settings:
- Moderate memory/GPU allocation for inference
- Longer terminal output (50 lines) to track benchmark progress
- Automatic Dockerfile building from scenarios/finetune/docker/opencompass
To customize, set environment variables:
export BENCHMARK_DOCKER_running_timeout_period=7200 # 2 hours
export BENCHMARK_DOCKER_terminal_tail_lines=100 # show last 100 lines
"""
2026-05-02 23:21:38 +02:00
def __init__ ( self , conf : DockerConf | None = None ):
super () . __init__ ( conf if conf is not None else BenchmarkDockerConf ())