From 47ff2b4a76e4f158890088b7be1dfed0fc69f032 Mon Sep 17 00:00:00 2001 From: Laughing <61612323+Laughing-q@users.noreply.github.com> Date: Sun, 21 Jul 2024 02:13:45 +0800 Subject: [PATCH] `ultralytics 8.2.61` fix `num_threads` for CPU training (#14544) Signed-off-by: Glenn Jocher Co-authored-by: UltralyticsAssistant Co-authored-by: Glenn Jocher --- docs/build_docs.py | 39 +++- mkdocs.yml | 3 + ultralytics/__init__.py | 2 +- ultralytics/cfg/__init__.py | 302 ++++++++++++++++++++----------- ultralytics/utils/torch_utils.py | 3 + 5 files changed, 239 insertions(+), 110 deletions(-) diff --git a/docs/build_docs.py b/docs/build_docs.py index 47d328a3..4d7f9393 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -30,6 +30,7 @@ import shutil import subprocess from pathlib import Path +from bs4 import BeautifulSoup from tqdm import tqdm os.environ["JUPYTER_PLATFORM_DIRS"] = "1" # fix DeprecationWarning: Jupyter is migrating to use standard platformdirs @@ -96,8 +97,6 @@ def update_html_head(script=""): def update_subdir_edit_links(subdir="", docs_url=""): """Update the HTML head section of each file.""" - from bs4 import BeautifulSoup - if str(subdir[0]) == "/": subdir = str(subdir[0])[1:] html_files = (SITE / subdir).rglob("*.html") @@ -153,7 +152,7 @@ def update_markdown_files(md_filepath: Path): def update_docs_html(): - """Updates titles, edit links and head sections of HTML documentation for improved accessibility and relevance.""" + """Updates titles, edit links, head sections, and converts plaintext links in HTML documentation.""" update_page_title(SITE / "404.html", new_title="Ultralytics Docs - Not Found") # Update edit links @@ -162,12 +161,46 @@ def update_docs_html(): docs_url="https://github.com/ultralytics/hub-sdk/tree/main/docs/", ) + # Convert plaintext links to HTML hyperlinks + files_modified = 0 + for html_file in tqdm(SITE.rglob("*.html"), desc="Converting plaintext links"): + with open(html_file, "r", encoding="utf-8") as file: + content = file.read() + updated_content = convert_plaintext_links_to_html(content) + if updated_content != content: + with open(html_file, "w", encoding="utf-8") as file: + file.write(updated_content) + files_modified += 1 + print(f"Modified plaintext links in {files_modified} files.") + # Update HTML file head section script = "" if any(script): update_html_head(script) +def convert_plaintext_links_to_html(content): + """Convert plaintext links to HTML hyperlinks in the main content area only.""" + soup = BeautifulSoup(content, "html.parser") + + # Find the main content area (adjust this selector based on your HTML structure) + main_content = soup.find("main") or soup.find("div", class_="md-content") + if not main_content: + return content # Return original content if main content area not found + + modified = False + for paragraph in main_content.find_all(["p", "li"]): # Focus on paragraphs and list items + for text_node in paragraph.find_all(string=True, recursive=False): + if text_node.parent.name not in {"a", "code"}: # Ignore links and code blocks + new_text = re.sub(r"(https?://\S+)", r'\1', str(text_node)) # note: reject http? + if ">> config_dict = cfg2dict('config.yaml') - # Example usage with a file path - config_dict = cfg2dict('config.yaml') + Convert a SimpleNamespace to a dictionary: + >>> from types import SimpleNamespace + >>> config_sn = SimpleNamespace(param1='value1', param2='value2') + >>> config_dict = cfg2dict(config_sn) - # Example usage with a SimpleNamespace - config_sn = SimpleNamespace(param1='value1', param2='value2') - config_dict = cfg2dict(config_sn) - - # Example usage with a dictionary (returns the same dictionary) - config_dict = cfg2dict({'param1': 'value1', 'param2': 'value2'}) - ``` + Pass through an already existing dictionary: + >>> config_dict = cfg2dict({'param1': 'value1', 'param2': 'value2'}) Notes: - If `cfg` is a path or a string, it will be loaded as YAML and converted to a dictionary. @@ -228,9 +224,8 @@ def get_cfg(cfg: Union[str, Path, Dict, SimpleNamespace] = DEFAULT_CFG_DICT, ove Load and merge configuration data from a file or dictionary, with optional overrides. Args: - cfg (str | Path | dict | SimpleNamespace, optional): Configuration data source. Defaults to `DEFAULT_CFG_DICT`. - overrides (dict | None, optional): Dictionary containing key-value pairs to override the base configuration. - Defaults to None. + cfg (str | Path | Dict | SimpleNamespace): Configuration data source. + overrides (Dict | None): Dictionary containing key-value pairs to override the base configuration. Returns: (SimpleNamespace): Namespace containing the merged training arguments. @@ -238,23 +233,15 @@ def get_cfg(cfg: Union[str, Path, Dict, SimpleNamespace] = DEFAULT_CFG_DICT, ove Notes: - If both `cfg` and `overrides` are provided, the values in `overrides` will take precedence. - Special handling ensures alignment and correctness of the configuration, such as converting numeric `project` - and `name` to strings and validating the configuration keys and values. + and `name` to strings and validating configuration keys and values. - Example: - ```python - from ultralytics.cfg import get_cfg + Examples: + Load default configuration: + >>> from ultralytics import get_cfg + >>> config = get_cfg() - # Load default configuration - config = get_cfg() - - # Load from a custom file with overrides - config = get_cfg('path/to/config.yaml', overrides={'epochs': 50, 'batch_size': 16}) - ``` - - Configuration dictionary merged with overrides: - ```python - {'epochs': 50, 'batch_size': 16, ...} - ``` + Load from a custom file with overrides: + >>> config = get_cfg('path/to/config.yaml', overrides={'epochs': 50, 'batch_size': 16}) """ cfg = cfg2dict(cfg) @@ -282,7 +269,26 @@ def get_cfg(cfg: Union[str, Path, Dict, SimpleNamespace] = DEFAULT_CFG_DICT, ove def check_cfg(cfg, hard=True): - """Validate Ultralytics configuration argument types and values, converting them if necessary.""" + """ + Checks configuration argument types and values for the Ultralytics library, ensuring correctness and converting them + if necessary. + + Args: + cfg (Dict): Configuration dictionary to validate. + hard (bool): If True, raises exceptions for invalid types and values; if False, attempts to convert them. + + Examples: + Validate a configuration with a mix of valid and invalid values: + >>> config = { + ... 'epochs': 50, # valid integer + ... 'lr0': 0.01, # valid float + ... 'momentum': 1.2, # invalid float (out of 0.0-1.0 range) + ... 'save': 'true', # invalid bool + ... } + >>> check_cfg(config, hard=False) + >>> print(config) + {'epochs': 50, 'lr0': 0.01, 'momentum': 1.2, 'save': False} # corrected 'save' key and retained other values + """ for k, v in cfg.items(): if v is not None: # None values may be from optional args if k in CFG_FLOAT_KEYS and not isinstance(v, (int, float)): @@ -318,7 +324,26 @@ def check_cfg(cfg, hard=True): def get_save_dir(args, name=None): - """Returns the directory path for saving outputs, derived from arguments or default settings.""" + """ + Returns the directory path for saving outputs, derived from arguments or default settings. + + Args: + args (SimpleNamespace): Namespace object containing configurations such as 'project', 'name', 'task', 'mode', and + 'save_dir'. + name (str | None): Optional name for the output directory. If not provided, it defaults to 'args.name' or the + 'args.mode'. + + Returns: + (Path): Directory path where outputs should be saved. + + Examples: + Generate a save directory using provided arguments + >>> from types import SimpleNamespace + >>> args = SimpleNamespace(project='my_project', task='detect', mode='train', exist_ok=True) + >>> save_dir = get_save_dir(args) + >>> print(save_dir) + my_project/detect/train + """ if getattr(args, "save_dir", None): save_dir = args.save_dir @@ -333,7 +358,18 @@ def get_save_dir(args, name=None): def _handle_deprecation(custom): - """Handles deprecated configuration keys by mapping them to current equivalents with deprecation warnings.""" + """ + Handles deprecated configuration keys by mapping them to current equivalents with deprecation warnings. + + Args: + custom (Dict): Configuration dictionary potentially containing deprecated keys. + + Examples: + >>> custom_config = {"boxes": True, "hide_labels": "False", "line_thickness": 2} + >>> _handle_deprecation(custom_config) + >>> print(custom_config) + {'show_boxes': True, 'show_labels': True, 'line_width': 2} + """ for key in custom.copy().keys(): if key == "boxes": @@ -354,35 +390,32 @@ def _handle_deprecation(custom): def check_dict_alignment(base: Dict, custom: Dict, e=None): """ - Check for key alignment between custom and base configuration dictionaries, catering for deprecated keys and - providing informative error messages for mismatched keys. + Check for key alignment between custom and base configuration dictionaries, handling deprecated keys and providing + informative error messages for mismatched keys. Args: - base (dict): The base configuration dictionary containing valid keys. - custom (dict): The custom configuration dictionary to be checked for alignment. - e (Exception, optional): An optional error instance passed by the calling function. Default is None. + base (Dict): The base configuration dictionary containing valid keys. + custom (Dict): The custom configuration dictionary to be checked for alignment. + e (Exception | None): Optional error instance passed by the calling function. Default is None. Raises: SystemExit: Terminates the program execution if mismatched keys are found. Notes: - - The function provides suggestions for mismatched keys based on their similarity to valid keys in the - base configuration. - - Deprecated keys in the custom configuration are automatically handled and replaced with their updated - equivalents. - - A detailed error message is printed for each mismatched key, helping users to quickly identify and correct - their custom configurations. + - The function suggests corrections for mismatched keys based on similarity to valid keys. + - Deprecated keys in the custom configuration are automatically replaced with their updated equivalents. + - Detailed error messages are printed for each mismatched key to help users identify and correct their custom + configurations. - Example: - ```python - base_cfg = {'epochs': 50, 'lr0': 0.01, 'batch_size': 16} - custom_cfg = {'epoch': 100, 'lr': 0.02, 'batch_size': 32} + Examples: + >>> base_cfg = {'epochs': 50, 'lr0': 0.01, 'batch_size': 16} + >>> custom_cfg = {'epoch': 100, 'lr': 0.02, 'batch_size': 32} - try: - check_dict_alignment(base_cfg, custom_cfg) - except SystemExit: - # Handle the error or correct the configuration - ``` + >>> try: + ... check_dict_alignment(base_cfg, custom_cfg) + ... except SystemExit: + ... # Handle the error or correct the configuration + ... pass """ custom = _handle_deprecation(custom) base_keys, custom_keys = (set(x.keys()) for x in (base, custom)) @@ -401,30 +434,29 @@ def check_dict_alignment(base: Dict, custom: Dict, e=None): def merge_equals_args(args: List[str]) -> List[str]: """ - Merges arguments around isolated '=' args in a list of strings. The function considers cases where the first - argument ends with '=' or the second starts with '=', as well as when the middle one is an equals sign. + Merges arguments around isolated '=' in a list of strings. Args: - args (List[str]): A list of strings where each element is an argument. + args (List[str]): A list of strings where each element represents an argument. Returns: (List[str]): A list of strings where the arguments around isolated '=' are merged. - Example: - The function modifies the argument list as follows: - ```python - args = ["arg1", "=", "value"] - new_args = merge_equals_args(args) - print(new_args) # Output: ["arg1=value"] + Examples: + Merge arguments where equals sign is separated: + >>> args = ["arg1", "=", "value"] + >>> merge_equals_args(args) + ["arg1=value"] - args = ["arg1=", "value"] - new_args = merge_equals_args(args) - print(new_args) # Output: ["arg1=value"] + Merge arguments where equals sign is at the end of the first argument: + >>> args = ["arg1=", "value"] + >>> merge_equals_args(args) + ["arg1=value"] - args = ["arg1", "=value"] - new_args = merge_equals_args(args) - print(new_args) # Output: ["arg1=value"] - ``` + Merge arguments where equals sign is at the beginning of the second argument: + >>> args = ["arg1", "=value"] + >>> merge_equals_args(args) + ["arg1=value"] """ new_args = [] for i, arg in enumerate(args): @@ -445,16 +477,13 @@ def handle_yolo_hub(args: List[str]) -> None: """ Handle Ultralytics HUB command-line interface (CLI) commands. - This function processes Ultralytics HUB CLI commands such as login and logout. It should be called when executing - a script with arguments related to HUB authentication. + This function processes Ultralytics HUB CLI commands such as login and logout. It should be called when executing a + script with arguments related to HUB authentication. Args: args (List[str]): A list of command line arguments. - Returns: - None - - Example: + Examples: ```bash yolo hub login YOUR_API_KEY ``` @@ -480,13 +509,9 @@ def handle_yolo_settings(args: List[str]) -> None: Args: args (List[str]): A list of command line arguments for YOLO settings management. - Returns: - None - - Example: - ```bash - yolo settings reset - ``` + Examples: + Reset YOLO settings: + >>> yolo settings reset Notes: For more information on handling YOLO settings, visit: @@ -511,21 +536,58 @@ def handle_yolo_settings(args: List[str]) -> None: def handle_explorer(): - """Open the Ultralytics Explorer GUI for dataset exploration and analysis.""" + """ + Open the Ultralytics Explorer GUI for dataset exploration and analysis. + + This function launches a graphical user interface that provides tools for interacting with and analyzing datasets + using the Ultralytics Explorer API. + + Examples: + Start the Ultralytics Explorer: + >>> handle_explorer() + """ checks.check_requirements("streamlit>=1.29.0") LOGGER.info("💡 Loading Explorer dashboard...") subprocess.run(["streamlit", "run", ROOT / "data/explorer/gui/dash.py", "--server.maxMessageSize", "2048"]) def handle_streamlit_inference(): - """Open the Ultralytics Live Inference streamlit app for real time object detection.""" + """ + Open the Ultralytics Live Inference streamlit app for real-time object detection. + + This function initializes and runs a Streamlit application designed for performing live object detection using + Ultralytics models. + + References: + - Streamlit documentation: https://docs.streamlit.io/ + - Ultralytics: https://docs.ultralytics.com + + Examples: + To run the live inference Streamlit app, execute: + >>> handle_streamlit_inference() + """ checks.check_requirements("streamlit>=1.29.0") LOGGER.info("💡 Loading Ultralytics Live Inference app...") subprocess.run(["streamlit", "run", ROOT / "solutions/streamlit_inference.py", "--server.headless", "true"]) def parse_key_value_pair(pair): - """Parse one 'key=value' pair and return key and value.""" + """ + Parse a 'key=value' pair and return the key and value. + + Args: + pair (str): The 'key=value' string to be parsed. + + Returns: + (tuple[str, str]): A tuple containing the key and value as separate strings. + + Examples: + >>> key, value = parse_key_value_pair("model=yolov8n.pt") + >>> key + 'model' + >>> value + 'yolov8n.pt + """ k, v = pair.split("=", 1) # split on first '=' sign k, v = k.strip(), v.strip() # remove spaces assert v, f"missing '{k}' value" @@ -533,7 +595,29 @@ def parse_key_value_pair(pair): def smart_value(v): - """Convert a string to its appropriate type (int, float, bool, None, etc.).""" + """ + Convert a string representation of a value into its appropriate Python type (int, float, bool, None, etc.). + + Args: + v (str): String representation of the value to be converted. + + Returns: + (Any): The converted value, which can be of type int, float, bool, None, or the original string if no conversion + is applicable. + + Examples: + Convert a string to various types: + >>> smart_value("42") + 42 + >>> smart_value("3.14") + 3.14 + >>> smart_value("True") + True + >>> smart_value("None") + None + >>> smart_value("some_string") + 'some_string' + """ v_lower = v.lower() if v_lower == "none": return None @@ -551,31 +635,26 @@ def entrypoint(debug=""): """ Ultralytics entrypoint function for parsing and executing command-line arguments. - This function serves as the main entry point for the Ultralytics CLI, parsing command-line arguments and + This function serves as the main entry point for the Ultralytics CLI, parsing command-line arguments and executing the corresponding tasks such as training, validation, prediction, exporting models, and more. Args: - debug (str, optional): Space-separated string of command-line arguments for debugging purposes. Default is "". + debug (str, optional): Space-separated string of command-line arguments for debugging purposes. - Returns: - (None): This function does not return any value. + Examples: + Train a detection model for 10 epochs with an initial learning_rate of 0.01: + >>> entrypoint("train data=coco8.yaml model=yolov8n.pt epochs=10 lr0=0.01") + + Predict a YouTube video using a pretrained segmentation model at image size 320: + >>> entrypoint("predict model=yolov8n-seg.pt source='https://youtu.be/LNwODJXcvt4' imgsz=320") + + Validate a pretrained detection model at batch-size 1 and image size 640: + >>> entrypoint("val model=yolov8n.pt data=coco8.yaml batch=1 imgsz=640") Notes: - For a list of all available commands and their arguments, see the provided help messages and the Ultralytics documentation at https://docs.ultralytics.com. - If no arguments are passed, the function will display the usage help message. - - Example: - ```python - # Train a detection model for 10 epochs with an initial learning_rate of 0.01 - entrypoint("train data=coco8.yaml model=yolov8n.pt epochs=10 lr0=0.01") - - # Predict a YouTube video using a pretrained segmentation model at image size 320 - entrypoint("predict model=yolov8n-seg.pt source='https://youtu.be/LNwODJXcvt4' imgsz=320") - - # Validate a pretrained detection model at batch-size 1 and image size 640 - entrypoint("val model=yolov8n.pt data=coco8.yaml batch=1 imgsz=640") - ``` """ args = (debug.split(" ") if debug else ARGV)[1:] if not args: # no arguments passed @@ -713,7 +792,18 @@ def entrypoint(debug=""): # Special modes -------------------------------------------------------------------------------------------------------- def copy_default_cfg(): - """Copy and create a new default configuration file with '_copy' appended to its name, providing usage example.""" + """ + Copy and create a new default configuration file with '_copy' appended to its name, providing a usage example. + + This function duplicates the existing default configuration file and appends '_copy' to its name in the current + working directory. + + Examples: + Copy the default configuration file and use it in a YOLO command: + >>> copy_default_cfg() + >>> # Example YOLO command with this new custom cfg: + >>> # yolo cfg='default_copy.yaml' imgsz=320 batch=8 + """ new_file = Path.cwd() / DEFAULT_CFG_PATH.name.replace(".yaml", "_copy.yaml") shutil.copy2(DEFAULT_CFG_PATH, new_file) LOGGER.info( diff --git a/ultralytics/utils/torch_utils.py b/ultralytics/utils/torch_utils.py index db17813f..21973d7e 100644 --- a/ultralytics/utils/torch_utils.py +++ b/ultralytics/utils/torch_utils.py @@ -21,6 +21,7 @@ from ultralytics.utils import ( DEFAULT_CFG_DICT, DEFAULT_CFG_KEYS, LOGGER, + NUM_THREADS, PYTHON_VERSION, TORCHVISION_VERSION, __version__, @@ -172,6 +173,8 @@ def select_device(device="", batch=0, newline=False, verbose=True): s += f"CPU ({get_cpu_info()})\n" arg = "cpu" + if arg in {"cpu", "mps"}: + torch.set_num_threads(NUM_THREADS) # reset OMP_NUM_THREADS for cpu training if verbose: LOGGER.info(s if newline else s.rstrip()) return torch.device(arg)