diff --git a/pyproject.toml b/pyproject.toml index 886ed672c..61cd9700c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,10 +49,6 @@ oss = [ "torchstore", ] -[project.scripts] -forge = "forge.cli.forge:main" - - # ---- Explicit project build information ---- # [build-system] requires = ["setuptools>=61.0"] diff --git a/src/forge/cli/download.py b/src/forge/cli/download.py deleted file mode 100644 index 69ebde9aa..000000000 --- a/src/forge/cli/download.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) Meta Platforms, Inc. and affiliates. -# All rights reserved. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. - -import argparse - -import json -import os -import textwrap -import traceback - -from pathlib import Path - -from huggingface_hub import snapshot_download -from huggingface_hub.utils import GatedRepoError, RepositoryNotFoundError - -from forge.cli.subcommand import Subcommand - -# TODO: update this -REPO_ID_FNAME = "original_repo_id" - - -class Download(Subcommand): - """Holds all the logic for the `forge download` subcommand.""" - - def __init__(self, subparsers: argparse._SubParsersAction): - super().__init__() - self._parser = subparsers.add_parser( - "download", - prog="forge download", - usage="forge download [OPTIONS]", - help="Download a model from the Hugging Face Hub.", - description="Download a model from the Hugging Face Hub.", - epilog=textwrap.dedent( - """\ - examples: - # Download a model from the Hugging Face Hub with a Hugging Face API token - $ forge download meta-llama/Llama-2-7b-hf --hf-token - Successfully downloaded model repo and wrote to the following locations: - /tmp/Llama-2-7b-hf/config.json - /tmp/Llama-2-7b-hf/README.md - /tmp/Llama-2-7b-hf/consolidated.00.pth - ... - - # Download an ungated model from the Hugging Face Hub - $ forge download mistralai/Mistral-7B-Instruct-v0.2 --output-dir /tmp/model - Successfully downloaded model repo and wrote to the following locations: - /tmp/model/config.json - /tmp/model/README.md - /tmp/model/model-00001-of-00002.bin - ... - - For a list of all models, visit the Hugging Face Hub - https://huggingface.co/models. - """ - ), - formatter_class=argparse.RawTextHelpFormatter, - ) - self._add_arguments() - self._parser.set_defaults(func=self._download_cmd) - - def _add_arguments(self) -> None: - """Add arguments to the parser.""" - self._parser.add_argument( - "repo_id", - type=str, - help="Name of the repository on Hugging Face Hub.", - ) - self._parser.add_argument( - "--output-dir", - type=Path, - required=False, - default=None, - help="Directory in which to save the model. Defaults to `/tmp/`.", - ) - self._parser.add_argument( - "--hf-token", - type=str, - required=False, - default=os.getenv("HF_TOKEN", None), - help="Hugging Face API token. Needed for gated models like Llama2.", - ) - self._parser.add_argument( - "--ignore-patterns", - type=str, - required=False, - help="If provided, files matching any of the patterns are not downloaded. Example: '*.safetensors'. " - "Only supported for Hugging Face Hub models.", - ) - - def _download_cmd(self, args: argparse.Namespace) -> None: - return self._download_from_huggingface(args) - - def _download_from_huggingface(self, args: argparse.Namespace) -> None: - """Downloads a model from the Hugging Face Hub.""" - # Download the tokenizer and PyTorch model files - - # Default output_dir is `/tmp/` - output_dir = args.output_dir - if output_dir is None: - model_name = args.repo_id.split("/")[-1] - output_dir = Path("/tmp") / model_name - - print(f"Ignoring files matching the following patterns: {args.ignore_patterns}") - try: - true_output_dir = snapshot_download( - args.repo_id, - local_dir=output_dir, - ignore_patterns=args.ignore_patterns, - token=args.hf_token, - ) - except GatedRepoError: - if args.hf_token: - self._parser.error( - "It looks like you are trying to access a gated repository. Please ensure you " - "have access to the repository." - ) - else: - self._parser.error( - "It looks like you are trying to access a gated repository. Please ensure you " - "have access to the repository and have provided the proper Hugging Face API token " - "using the option `--hf-token` or by running `huggingface-cli login`." - "You can find your token by visiting https://huggingface.co/settings/tokens" - ) - except RepositoryNotFoundError: - self._parser.error( - f"Repository '{args.repo_id}' not found on the Hugging Face Hub." - ) - except Exception as e: - tb = traceback.format_exc() - msg = f"Failed to download {args.repo_id} with error: '{e}' and traceback: {tb}" - self._parser.error(msg) - - # save the repo_id. This is necessary because the download step is a separate command - # from the rest of the CLI. When saving a model adapter, we have to add the repo_id - # to the adapter config. - # TODO: this needs to be updated when we start using HF cache - file_path = os.path.join(true_output_dir, REPO_ID_FNAME + ".json") - with open(file_path, "w") as json_file: - json.dump({"repo_id": args.repo_id}, json_file, indent=4) - - print( - "Successfully downloaded model repo and wrote to the following locations:", - *list(Path(true_output_dir).iterdir()), - sep="\n", - ) diff --git a/src/forge/cli/forge.py b/src/forge/cli/forge.py deleted file mode 100644 index 7e5d2ac73..000000000 --- a/src/forge/cli/forge.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Meta Platforms, Inc. and affiliates. -# All rights reserved. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. - -import argparse - -from forge.cli.download import Download -from forge.cli.run import Run - - -class ForgeCLIParser: - """Holds all information related to running the CLI""" - - def __init__(self): - # Initialize the top-level parser - self._parser = argparse.ArgumentParser( - prog="forge", - description="Welcome to the torchforge CLI!", - add_help=True, - ) - # Default command is to print help - self._parser.set_defaults(func=lambda args: self._parser.print_help()) - - # Add subcommands - subparsers = self._parser.add_subparsers(title="subcommands") - Download.create(subparsers) - Run.create(subparsers) - - def parse_args(self) -> argparse.Namespace: - """Parse CLI arguments""" - return self._parser.parse_args() - - def run(self, args: argparse.Namespace) -> None: - """Execute CLI""" - args.func(args) - - -def main(): - parser = ForgeCLIParser() - args = parser.parse_args() - parser.run(args) - - -if __name__ == "__main__": - main() diff --git a/src/forge/cli/run.py b/src/forge/cli/run.py deleted file mode 100644 index 4a556c1f8..000000000 --- a/src/forge/cli/run.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) Meta Platforms, Inc. and affiliates. -# All rights reserved. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. - -import argparse -import os -import sys -import textwrap - -from pathlib import Path - -from torch.distributed.elastic.multiprocessing.errors import record -from torch.distributed.run import get_args_parser as get_torchrun_args_parser, run - -import forge -from forge.cli.subcommand import Subcommand - -ROOT = Path(forge.__file__).parent.parent - - -class Run(Subcommand): - """Holds all the logic for the `forge run` subcommand.""" - - def __init__(self, subparsers): - super().__init__() - self._parser = subparsers.add_parser( - "run", - prog="forge run", - help="Run a recipe. For distributed recipes, this supports all torchrun arguments.", - description="Run a recipe. For distributed recipes, this supports all torchrun arguments.", - usage="forge run [TORCHRUN-OPTIONS] --config [RECIPE-OPTIONS]", - epilog=textwrap.dedent( - """\ - examples: - - # Run SFT recipe with default values - $ forge run --nproc_per_node 4 apps/sft/sft.py --config apps/sft/configs/llama3_8b.yaml - """ - ), - formatter_class=argparse.RawTextHelpFormatter, - ) - self._add_arguments() - self._parser.set_defaults(func=self._run_cmd) - - def _add_arguments(self) -> None: - """Add arguments to the parser. - - This is a bit hacky since we need to add the torchrun arguments to our parser. - This grabs the argparser from torchrun, iterates over it's actions, and adds them - to our parser. We rename the training_script and training_script_args to recipe and recipe_args - respectively. In addition, we leave out the help argument since we add it manually to ours. - """ - torchrun_argparser = get_torchrun_args_parser() - for action in torchrun_argparser._actions: - if action.dest == "training_script": - action.dest = "recipe" - action.help = """Path to recipe to be launched followed by args.""" - elif action.dest == "training_script_args": - action.dest = "recipe_args" - action.help = "Args to be passed to the recipe." - elif action.dest == "help": - continue - self._parser._add_action(action) - - @record - def _run_distributed(self, args: argparse.Namespace): - """Run a recipe with torchrun.""" - print("Running with torchrun...") - # Have to reset the argv so that the recipe can be run with the correct arguments - args.training_script = args.recipe - args.training_script_args = args.recipe_args - - # If the user does not explicitly pass a rendezvous endpoint, run in standalone mode. - # This allows running multiple distributed training jobs simultaneously. - if not args.rdzv_endpoint: - args.standalone = True - - args.module = True - run(args) - - def _convert_to_dotpath(self, recipe_path: str) -> str: - """Convert a custom recipe path to a dot path that can be run as a module. - - Args: - recipe_path (str): The path of the recipe. - - Returns: - The dot path of the recipe. - """ - filepath, _ = os.path.splitext(recipe_path) - return filepath.replace("/", ".") - - def _run_cmd(self, args: argparse.Namespace): - """Run a recipe.""" - # We have to assume that the recipe supports distributed training - supports_distributed = True - recipe_path, config_path = None, None - - # Try to find config string in args - try: - config_idx = args.recipe_args.index("--config") + 1 - config_str = args.recipe_args[config_idx] - except ValueError: - self._parser.error("The '--config' argument is required.") - - # Get recipe path - recipe_path = self._convert_to_dotpath(args.recipe) - - # Get config path - config_path = config_str - - # Prepare args - args.recipe = recipe_path - args.recipe_args[config_idx] = config_path - - # Make sure user code in current directory is importable - sys.path.append(os.getcwd()) - - self._run_distributed(args) diff --git a/src/forge/cli/subcommand.py b/src/forge/cli/subcommand.py deleted file mode 100644 index db298a0b0..000000000 --- a/src/forge/cli/subcommand.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Meta Platforms, Inc. and affiliates. -# All rights reserved. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. - - -class Subcommand: - def __init__(self, *args, **kwargs): - pass - - @classmethod - def create(cls, *args, **kwargs): - return cls(*args, **kwargs) - - def _add_arguments(self): - pass