From 9af30de6e7f68571d7a749225a29059835b5c91e Mon Sep 17 00:00:00 2001 From: "Fabrizio (Misto) Milo" Date: Fri, 3 Feb 2023 21:39:48 -0800 Subject: [PATCH] feature: add new website --- .github/workflows/docs.yml | 53 +++ .pre-commit-config.yaml | 21 +- .readthedocs.yml | 22 +- CHANGELOG.md | 11 + CONTRIBUTING.md | 65 +++- Makefile | 6 + README.md | 54 +-- dictionary.txt | 1 + docker/docs/Dockerfile | 13 + docs/Makefile | 2 +- docs/README.md | 5 + docs/_static/apple-touch-icon-114x114.png | Bin 0 -> 4056 bytes docs/_static/apple-touch-icon-120x120.png | Bin 0 -> 4282 bytes docs/_static/apple-touch-icon-144x144.png | Bin 0 -> 5425 bytes docs/_static/apple-touch-icon-152x152.png | Bin 0 -> 5706 bytes docs/_static/apple-touch-icon-167x167.png | Bin 0 -> 6637 bytes docs/_static/apple-touch-icon-180x180.png | Bin 0 -> 7003 bytes docs/_static/apple-touch-icon-57x57.png | Bin 0 -> 1941 bytes docs/_static/apple-touch-icon-60x60.png | Bin 0 -> 2192 bytes docs/_static/apple-touch-icon-72x72.png | Bin 0 -> 2484 bytes docs/_static/apple-touch-icon-76x76.png | Bin 0 -> 2634 bytes docs/_static/favicon-128x128.png | Bin 0 -> 4399 bytes docs/_static/favicon-16x16.png | Bin 0 -> 534 bytes docs/_static/favicon-196x196.png | Bin 0 -> 8020 bytes docs/_static/favicon-32x32.png | Bin 0 -> 1135 bytes docs/_static/favicon-96x96.png | Bin 0 -> 3280 bytes docs/_static/style.css | 26 ++ docs/_static/trlx-logo-512x512.png | Bin 0 -> 22859 bytes docs/_templates/layout.html | 2 + docs/build.sh | 3 + docs/changelog.md | 2 + docs/conf.py | 187 ++++++++++ docs/{source => }/configs.rst | 4 +- docs/{source => }/data.rst | 0 docs/examples.md | 26 ++ docs/faq.md | 8 + docs/glossary.md | 81 +++++ docs/{source => }/index.rst | 20 ++ docs/models.md | 1 + docs/{source => }/orchestrator.rst | 0 docs/pipeline.md | 30 ++ docs/requirements.txt | 29 +- docs/source/conf.py | 54 --- docs/source/examples.rst | 18 - docs/source/pipeline.rst | 28 -- docs/{source => }/trainer.rst | 0 .../grounded_program_synthesis/lang.py | 2 +- .../grounded_program_synthesis/train_trlx.py | 4 +- pyproject.toml | 5 + setup.cfg | 55 ++- setup.py | 36 +- trlx/__init__.py | 1 - trlx/orchestrator/offline_orchestrator.py | 36 +- trlx/orchestrator/ppo_orchestrator.py | 27 +- trlx/trainer/__init__.py | 2 +- trlx/trainer/accelerate_base_trainer.py | 59 +-- trlx/trainer/accelerate_ilql_trainer.py | 2 +- trlx/trainer/nemo/gpt.py | 2 +- trlx/trlx.py | 4 +- trlx/utils/logging.py | 340 ------------------ 60 files changed, 718 insertions(+), 629 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 CHANGELOG.md create mode 100644 Makefile create mode 100644 dictionary.txt create mode 100644 docker/docs/Dockerfile create mode 100644 docs/README.md create mode 100644 docs/_static/apple-touch-icon-114x114.png create mode 100644 docs/_static/apple-touch-icon-120x120.png create mode 100644 docs/_static/apple-touch-icon-144x144.png create mode 100644 docs/_static/apple-touch-icon-152x152.png create mode 100644 docs/_static/apple-touch-icon-167x167.png create mode 100644 docs/_static/apple-touch-icon-180x180.png create mode 100644 docs/_static/apple-touch-icon-57x57.png create mode 100644 docs/_static/apple-touch-icon-60x60.png create mode 100644 docs/_static/apple-touch-icon-72x72.png create mode 100644 docs/_static/apple-touch-icon-76x76.png create mode 100644 docs/_static/favicon-128x128.png create mode 100644 docs/_static/favicon-16x16.png create mode 100644 docs/_static/favicon-196x196.png create mode 100644 docs/_static/favicon-32x32.png create mode 100644 docs/_static/favicon-96x96.png create mode 100644 docs/_static/style.css create mode 100644 docs/_static/trlx-logo-512x512.png create mode 100644 docs/_templates/layout.html create mode 100755 docs/build.sh create mode 100644 docs/changelog.md create mode 100644 docs/conf.py rename docs/{source => }/configs.rst (86%) rename docs/{source => }/data.rst (100%) create mode 100644 docs/examples.md create mode 100644 docs/faq.md create mode 100644 docs/glossary.md rename docs/{source => }/index.rst (76%) create mode 100644 docs/models.md rename docs/{source => }/orchestrator.rst (100%) create mode 100644 docs/pipeline.md delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/examples.rst delete mode 100644 docs/source/pipeline.rst rename docs/{source => }/trainer.rst (100%) delete mode 100644 trlx/utils/logging.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..3c15b7be6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: docs_pages_workflow + +on: [pull_request] + +permissions: + pull-requests: write + +jobs: + build_docs_job: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: 3.8 + + - name: Get pip cache dir + id: pip-cache + run: | + python -m pip install --upgrade pip + echo "dir={$(pip cache dir)}" >> $GITHUB_OUTPUT + + - name: pip cache + uses: actions/cache@v3 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py', '**/requirements.txt', '**/docs/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install docs requirements + run: | + python -m pip install -r docs/requirements.txt + + - name: make the sphinx docs + run: | + make -C docs clean + make -C docs html + + - uses: readthedocs/actions/preview@v1 + with: + project-slug: "trlx" + project-language: "en" + # see: https://github.com/readthedocs/actions/tree/main/preview + # message-template (optional): Text message to be injected by the action in the Pull Request description. It supports the following placeholders to be replaced: + # {docs-pr-index-url}: URL to the root of the documentation for the Pull Request preview. + # platform (optional): Read the Docs Community (community) or Read the Docs for Business (business). (default: community) + # single-version (optional): Set this to 'true' if your project is single version, so we can link to the correct URL. (default: 'false') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d94d14f2a..cbf9fa775 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-case-conflict @@ -18,17 +18,24 @@ repos: args: [--fix=lf] - id: requirements-txt-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black + - repo: https://github.com/psf/black rev: 23.1.0 hooks: - - id: black + - id: black files: ^(trlx|examples|tests|setup.py)/ -- repo: https://github.com/pycqa/isort + - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - - id: isort + - id: isort name: isort (python) -- repo: https://github.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - - id: flake8 + - id: flake8 + - repo: https://github.com/codespell-project/codespell + rev: v2.2.2 + hooks: + - id: codespell + args: [--ignore-words, dictionary.txt] + additional_dependencies: + - tomli diff --git a/.readthedocs.yml b/.readthedocs.yml index c8f03ab0a..d5f60f2e8 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,9 +1,25 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required version: 2 +build: + os: "ubuntu-20.04" + tools: + python: "3.8" + +# Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/source/conf.py + configuration: docs/conf.py + fail_on_warning: false + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - htmlzip +# Optionally set the version of Python and requirements required to build your docs python: - version: 3.9 install: - - requirements: docs/requirements.txt + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..6f9ee79d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change log + +Best viewed on [trlx.readthedocs.io](https://trlx.readthedocs.io/en/latest/changelog.html). + + + +## trlx 0.4.0 (2022-12-05) + +- python 3.8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c5169ed5..8f97f071d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,11 @@ Looking to improve `trlX`? Thanks for considering! -There are many ways to contribute, from writing tutorials in [Colab notebooks](https://colab.research.google.com) to improving the project's [documentation](https://trlx.readthedocs.io), submitting bug reports and feature requests, or even implementing new features themselves. See the outstanding [issues](https://github.com/CarperAI/trlx/issues) for ideas on where to begin. +There are many ways to contribute, from writing tutorials in [Colab notebooks](https://colab.research.google.com) to improving the project's [documentation](https://trlx.readthedocs.io), to submitting bug reports and feature requests, or even implementing new features themselves. See the outstanding [issues](https://github.com/CarperAI/trlx/issues) for ideas on where to begin. + +- [Documentation Issues](https://github.com/CarperAI/trlx/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) +- [Bug Fixes](https://github.com/CarperAI/trlx/issues?q=is%3Aissue+is%3Aopen+label%3Abug) +- [Feature Requests](https://github.com/CarperAI/trlx/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature+request%22) Here are some guidelines to help you get started 🚀. @@ -16,40 +20,83 @@ To submit a bug report or a feature request, please open an [issue](https://gith Follow these steps to start contributing code: +1. Setup your environment: + +```bash +conda create -n trlx python=3.8 torch torch-cuda=11.7 -c pytorch -c nvidia +git clone https://github.com/CarperAI/trlx +cd trlx +pip install -e ".[dev]" +pre-commit install +``` + 1. Create your own [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository) of the repository and clone it to your local machine. + ```bash git clone https://github.com//trlx.git cd trlx git remote add upstream https://github.com/CarperAI/trlx.git ``` -2. Create a new branch for your changes and give it a concise name that reflects your contribution. + +1. Create a new branch for your changes and give it a concise name that reflects your contribution. + ```bash git checkout -b ``` -2. Install the development dependencies in a Python environment. + +1. Install the development dependencies in a Python environment. + ```bash pip install -e ".[dev]" pre-commit install ``` -4. Implement your changes. Make small, independent, and well documented commits along the way (check out [these](https://cbea.ms/git-commit/) tips). -5. Add unit tests whenever appropriate and ensure that the tests pass. To run the entire test suite, use the following command from within the project root directory. + +install pre-commit + +```bash +pip install pre-commit +pre-commit install +``` + +bonus: force run pre-commit on all the files + +```bash +pre-commit run --all-files +``` + +1. Implement your changes. Make small, independent, and well documented commits along the way (check out [these](https://cbea.ms/git-commit/) tips). + +1. Add unit tests whenever appropriate and ensure that the tests pass. To run the entire test suite, use the following command from within the project root directory. + ```bash pytest ``` + For changes with minimal project scope (e.g. a simple bug fix), you might want to run the unit tests for just a specific test file instead: + ```bash pytest -vv -k "" ``` -5. Commit your final changes. Our `pre-commit` hooks will automatically run before each commit and will prevent you from committing code that does not pass our style and linter checks. They'll also automatically format your code! To run these manually, use the following command: + +1. Commit your final changes. Our `pre-commit` hooks will automatically run before each commit and will prevent you from committing code that does not pass our style and linter checks. They'll also automatically format your code! To run these manually, use the following command: + ```bash pre-commit run --all-files ``` -6. Push the changes to your fork. +1. Push the changes to your fork. Finally ... 🥁 ... Create a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) to the `trlX` repository! Make sure to include a description of your changes and link to any relevant issues. -> __Tip__: If you're looking to introduce an experimental feature, we suggest testing the behavior of your proposed feature on some of the existing [examples](https://github.com/CarperAI/trlx/tree/master/examples), such as [random walks](https://github.com/CarperAI/trlx/blob/master/examples/randomwalks). This will help you get a better sense of how the feature would work in practice and will also help you identify any potential flaws in the implementation. +> **Tip**: If you're looking to introduce an experimental feature, we suggest testing the behavior of your proposed feature on some of the existing [examples](https://github.com/CarperAI/trlx/tree/master/examples), such as [random walks](https://github.com/CarperAI/trlx/blob/master/examples/randomwalks). This will help you get a better sense of how the feature would work in practice and will also help you identify any potential flaws in the implementation. + +## Tips & Tricks + +Set transformers verbosity level + +```bash +TRANSFORMERS_VERBOSITY=error +``` ## Asking questions @@ -63,4 +110,4 @@ This project adheres to the [Contributor Covenant Code of Conduct](https://githu By contributing, you agree that your contributions will be licensed under its MIT License. -# Thank you for your contribution 🐠! +## Thank you for your contribution! 🐠 diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..b13cd2dd2 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +gendoc: + docker build -t trlxgendocs -f docker/docs/Dockerfile . +run: + docker run --rm -it -v ${PWD}:/build \ + --entrypoint /bin/bash \ + trlxgendocs diff --git a/README.md b/README.md index 27b8ffa7d..5ca573cb7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![TRLX](./docs/_static/apple-touch-icon-114x114.png) + [docs-image]: https://readthedocs.org/projects/trlX/badge/?version=latest [docs-url]: https://trlX.readthedocs.io/en/latest/?badge=latest @@ -12,6 +14,7 @@ You can read more about trlX in our [documentation](https://trlX.readthedocs.io) Want to collect human annotations for your RL application? Check out [CHEESE!](https://github.com/carperai/cheese), our library for HiTL data collection. ## Installation + ```bash git clone https://github.com/CarperAI/trlx.git cd trlx @@ -28,23 +31,29 @@ For more usage see [examples](./examples). You can also try the colab notebooks ## How to Train + You can train a model using a reward function or a reward-labeled dataset. -#### Using a reward function +### Using a reward function + ```python trainer = trlx.train('gpt2', reward_fn=lambda samples, **kwargs: [sample.count('cats') for sample in samples]) ``` -#### Using a reward-labeled dataset + +### Using a reward-labeled dataset + ```python trainer = trlx.train('EleutherAI/gpt-j-6B', dataset=[('dolphins', 'geese'), (1.0, 100.0)]) ``` -#### Trainers provide a wrapper over their underlying model +### Trainers provide a wrapper over their underlying model + ```python trainer.generate(**tokenizer('Q: Who rules the world? A:', return_tensors='pt'), do_sample=True) ``` -#### Save the resulting model to a Hugging Face pretrained language model. (Ready to upload to the Hub!) +### Save the resulting model to a Hugging Face pretrained language model. (Ready to upload to the Hub!) + ```python trainer.save_pretrained('/path/to/output/folder/') ``` @@ -69,46 +78,11 @@ python examples/nemo_ilql_sentiments.py For more usage see the [NeMo README](./trlx/trainer/nemo) #### Use Ray Tune to launch hyperparameter sweep + ```bash python -m trlx.sweep --config configs/sweeps/ppo_sweep.yml examples/ppo_sentiments.py ``` -## Logging - -trlX uses the standard Python `logging` library to log training information to the console. The default logger is set to the `INFO` level, which means that `INFO`, `WARNING`, `ERROR`, and `CRITICAL` level messages will be printed to standard output. - -To change the log level directly, you can use the verbosity setter. For example, to set the log level to `WARNING` use: - -```python -import trlx - -trlx.logging.set_verbosity(trlx.logging.WARNING) -``` - -This will suppress `INFO` level messages, but still print `WARNING`, `ERROR`, and `CRITICAL` level messages. - -You can also control logging verbosity by setting the `TRLX_VERBOSITY` environment variable to one of the standard logging [level names](https://docs.python.org/3/library/logging.html#logging-levels): - -* `CRITICAL` (`trlx.logging.CRITICAL`) -* `ERROR` (`trlx.logging.ERROR`) -* `WARNING` (`trlx.logging.WARNING`) -* `INFO` (`trlx.logging.INFO`) -* `DEBUG` (`trlx.logging.DEBUG`) - -```sh -export TRLX_VERBOSITY=WARNING -``` - -By default, [`tqdm`](https://tqdm.github.io/docs/tqdm/) progress bars are used to display training progress. You can disable them by calling `trlx.logging.disable_progress_bar()`, otherwise `trlx.logging.enable_progress_bar()` to enable. - -Messages can be formatted with greater detail by setting `trlx.logging.enable_explicit_format()`. This will inject call-site information into each log which may be helpful for debugging. - -```sh -[2023-01-01 05:00:00,000] [INFO] [ppo_orchestrator.py:63:make_experience] [RANK 0] Message... -``` - -> 💡 Tip: To reduce the amount of logging output, you might find it helpful to change log levels of third-party libraries used by trlX. For example, try adding `transformers.logging.set_verbosity_error()` to the top of your trlX scripts to silence verbose messages from the `transformers` library (see their [logging docs](https://huggingface.co/docs/transformers/main_classes/logging#logging) for more details). - ## Contributing For development check out these [guidelines](./CONTRIBUTING.md) diff --git a/dictionary.txt b/dictionary.txt new file mode 100644 index 000000000..1cd3c8cd5 --- /dev/null +++ b/dictionary.txt @@ -0,0 +1 @@ +rouge diff --git a/docker/docs/Dockerfile b/docker/docs/Dockerfile new file mode 100644 index 000000000..7c8287b94 --- /dev/null +++ b/docker/docs/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.8-slim + +# pip install -r docs/requirements.txt +# sphinx-build -b html docs docs/build/html -j auto +# sphinx-build -b html -D nb_execution_mode=off docs docs/build/html -j auto + +RUN python -m pip install --upgrade --no-cache-dir pip +ADD docs/requirements.txt /tmp/requirements.txt +RUN python -m pip install --exists-action=w --no-cache-dir -r /tmp/requirements.txt +RUN mkdir /build +WORKDIR /build/docs/ +ENTRYPOINT /build/docs/build.sh +# RUN `which sphinx-build` -T -E -b html -d _build/doctrees-readthedocs -D language=en . _build/html diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf10..ed8809902 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,7 +5,7 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = source +SOURCEDIR = . BUILDDIR = build # Put it first so that "make" without argument is like "make help". diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..62a4ae956 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,5 @@ +# How To build the documentation + +```bash +make -c docs html +``` diff --git a/docs/_static/apple-touch-icon-114x114.png b/docs/_static/apple-touch-icon-114x114.png new file mode 100644 index 0000000000000000000000000000000000000000..f41c09a0920feb6fe39772c505035e2fee3b0178 GIT binary patch literal 4056 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 IDp`mA7Z#Pl761SM literal 0 HcmV?d00001 diff --git a/docs/_static/apple-touch-icon-120x120.png b/docs/_static/apple-touch-icon-120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff0b418e89982e760fc9502aeb8b386e8b18768 GIT binary patch literal 4282 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 literal 0 HcmV?d00001 diff --git a/docs/_static/apple-touch-icon-144x144.png b/docs/_static/apple-touch-icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..4657719af96fe842414c9a7ef92b1591ad448541 GIT binary patch literal 5425 zcmbVQWl$7Q*Cu3P>5iq5?v`$rh9yK;q&p;~q!%Tnk#3M)0SS?GK}lIjfu$FaTtYyU z{CIzUGvCZN^Uj=eo^#KSGxyIu_c>1@#88Wb5JZTDg+-#Htzq(j>Hl#8z(d>tqdOk} z=&NlRfQ3aw^&iK^Dk!3VC}IbgXsKa+Kr-z;93Hu<8mMAnwWJZ<*yCYgQ4i^8sG5af zV~T;#pU(aKLg%|NrofL@iMHTHmgnelE6$jGZ<_v?`>xJe5pMHd@j%glQR$XxOe{c` zBq9ghm>xh{#T&;_fy0lJL@Yi+OxM?*dU-{DEPGiLh6uyJ?~j8c={m3V!jb8=Rh3#WUY54B}e$QuhY`VYR&k=>{tlRoSmKRbjrYw zer+H?^h1vsA#JZA%@(JM%gyx85^a-vhXq|UjNG@~7V zzoGT)isJ41^5=jcP>M@yN{Yb~%ChKatk|8cogH->8x~&G_a-q-O--uo)BM@xAyw#z zxQ!i(DNl2Ab3(fB{{mUeatme?-&VWdU#eENP`<9|>0z4nXrHN1iQhmwZ#t@V@H6`Q zeY`w*u{Xs(IWJ%32stqJke?IuI^?~1g~xK;QlY`9z|B2G%Q)T38(CwuH4y=g zE>M_GsVEH!F^i^nxjR|za_|!#5J$?~h0zG-_gV~qehiiHYUG|!9SAW(gtkXtvL~ZoI$&f0me0TJUIpu7NR@^@Y^$Gue#v$juzLjaqde`7}FL zc)=?*FgQ+->NOa|!^88}1<9T8R*uW^&w-K6KyKZHZP&{^HPK1FJAt>m`}?5IAVo6s zJAZnxz6J4_xrY1}UC!I#CejM>cI-3tUKwR^aq$5|XFS%v3&C29mnKo}GqVBMT-X=X z5bg8l&s9ikb=H@^5ic+?^H_e^bcC zKCMp7OHW#;+Q_y?Hz&0V@<~U*Exg#T(pwRFZni0RFS(Y#3lVZ z03PxCKv1o)JHzLbrq3Cio$8acHv)MK!NHdx%DAd3PPZ8p)hffH(Z`!bqP=Fh-?e4- z+K_YA`SA&nn7y-4x;72((&TRf(2&j`Oo6XZPqm;j!W#Rd$s*dG=K#wSH}S3c&yP)9 zOkg*bb`)(9bK!EZjBG89_3>mP(TM;leh9KVKR=(;&V`pM9YkJ=H`KRGhr-wGw&+Wf zVWVG`%$^%YAXJ+(%bA#po%Gq!tWX74UaeOXKF5u!4!jg z^4nGTj`m5UHzh9zkCySENWK!GT9`uhWq0OBLi+?%m>`9~akJ_~$A0eP6aC|L!vzA7 zjV4dTTL?3x(zZX+n*Np2wUx}*(YyAR6vH{?{x{Q8p;rgXdK7*&5!_HyS51EMPazlMRY+=D-QB74bt-3&0 zg=cR+TE0xeaIVo(qN(G>Qk*{Ma+yl~DzJm$a?xXCl*-8Lelr}z$?Xbl zCpsY=w2i`f%nT#JlPasMd|Xvst(KNf9^z+5;FUyqshJ@bA@herVNnWel7@|-u%SU< zVX@BsXw1!vC}sDvFp^aCU$H7|<*9`QfkklHu4P@!Xg;r2bgh11VEejQ7VZV0rRB5m zIdL1M$_Lb}s?D!wwh@7f%RsB$vgAnvW?CWWY{2(CG26(CDWZwB!Yzz z{HyZ;*3o;%db(vN?ZWgLhMJj^)6SS?vUYb>Um*Akr3GaQ4G#ou&z*YsS@y5RD&n~C|R3GbvY}G%Ji#Kz#hF5f_9<@e> zzV9GcdO~veXBXV1Ln|L_xO6aX5A2`g~upG63S)Fq7zBF|VsLUL8ynasdBy z_zVsWWc*GX$UV)mx3@Aw14chGa@pq2@hl3MDJWX- zzDd=1#%pmly?X{xW0iim(hDFhj=PVCS%Ep(;Sxm`o?ic8!H8q}J5vHA64~rKp)@iw zmMf@yal3=~SxxJ`+{wORJ=BI!S4_!09he)>75Sl}+Ko9v^IA4nog3-{!k2cGcs!M9Qg|=t$AeiW2Qoex$^qXgW5LOM6EKCX23SF&voG27fXc{45-`Z^v)m)!x)}+{U>0 zrhcJyx-~-!8e+n&vf60b`n_A!noMqO@>?>U>I+X2AI1}i-qg<~*&gY}TwkH$6lX%R zcC2z2V^*c`g%t6fws?6Kn-EC&fwf?!#dE0GdV}T?*R6_1Zcrb!M%7IQf3r$$xqCrz zULL_<2?h|g$4so-($X?yx0hU2_klX|a%4eT>dwu$P=HseVoFn#O;WNga+=&S_D@wr zYKeXv7sWOE(el&L1BK~nDz}`R(x`*rW-Qk92VTJ0{U$YSnDq5RYi{{&#|`r@zy;<} z@ltzS7LO63Fb)@stC*l}boP0iRACwd8SAPYyEB8ktKieeH zDYumCeIuL1k?Jz1Vumkdl@vxfqG|zwVBOu_iIO@x2Rze=Wl~sS5)%|R^EJ$jN>xBQ zv|4Zt79>%oY^iMkyE==6b;-2Ibb71B*ybbDAHG(`x5QBuw1XVmxcH>ujF__dlSR*# z4Blpbfesi7N#p86CH76&2{#qv9Lub6MvL)sD29 z?#JMzGt#mRro_uyv2}nBE<-KkodJG+nIjS;^k=>~=f;`3N#*eiVH?|^v}$+RA*Y^o zq_A~L36q92qUZ-0)*3s;&$V5TA5?8T+YKC($~hn!0K7!xp)L09F3Q0JfX6&ps@Q7w zBj3{A$Bk{Y8hnZKwQLJ2J+xO=+5;V?$&%fc5DmY-gbeCo~HOm&TpsBT&U;SQrsoJzGK%iY(!WBM4Y|89u>mA%`GaKoaW z7~rK8d4_H&G6ucI=}s}w){08VyCyvcy6ZKFyryR*Va#b(LXz)NK`l&7j#^~U(P$kl zX{3hheRFafTBC6XIf=zfS$4MbV_dkwTHMjEP{|bHe|vMfj#+&4=XgzoSZr%=Zbv^$ z07Ax}McEz|6<%GQy;Kx7-WhTnSKN}G?H?<7WMpY+$<0`sg2DG_6JA*!r-9*EEe9_w z>ZXO$wRuhmKksB)kdbT<3zWNNlcWQ0WBhiZV0Y&1H7~*Je3^@G?W-2tpdw>CjuaPH zxAECj z)zF!LFf7I>7?vO;R^f~+GKwdWPqj*6y4n()^}zSFkbH$*f<|R6EmfIENK@}ZlJmtJ zhYGtMOGyQepQx_*b@3~O)g#94{Wql9quqcM!Lnt&)6DH#f7`86)u7eq{=bZdNMNHr zWH|Rfr>UZkBbRZX1Q1#KRx1?en8#-D(z3nT>~&cApl9?B7wP-k6~zi zoqhVq3E|-i8b2zjCq?L9P{9)Yp9K{pf3WU0tZkfrSt=(&c>q@6`i58~y}P(O+j?)f z{K;qSu-OLDZf~S3Vz}HVQo{66CdtZ3Q>&Ja9#P5;vNhf5>~;8-bn*1*yC>XzcXXpH zA^)JHhe{#lc`^C*Z$RS-Lr99~l}~ z%7NJsR+{3NkY#M2gb!qP>~540f+rVnsw_SUFbteSdhHM}ry_eeGmOT=&0wyPH3;2U zHRhzn>i+S|7Zv-1?5p^o112eH^PaC1$)NR;?f2BgL+n=v-y6eo8HnEJK5st{iZ>c2 zt6u(f!K)B&YGN`i?X_?cm39^83GS!o4;2=Qg2qshHC_f!{oDr-x*yurqd(L+5zHXV zj`)yo-@#fP>^wAN;Qr^(*{IF49Tm{ly}@Z4~Pzu59obp0MA zQ&vcA=}n-B4eM(OJa*SM7^hXdcReQ|Aw8%d^j>m^baDBGMf@uVrcX_j{yxm}iTAR0 zoZ4!;=R768!rWAi&kwf0n6G$Kzt5gwXl9j3oU61+%%9L#We9H#9 z!hC$E2&CBALYg~q)-c0hdCYxen)QV)Y1hsyFr2Acgl;@Rx=#!p_ zPQC^Wc?~~z+3?d9+P}_IYgcxET_o?^#YU=HTMs*#O46y}mXFS#t z9v&VRA;FyT5{J~sZne7=-wv&`NHRqwvv@2v5ZyQO!9gf%n190cg$MpEgh=P~n0c68 z>|fXVJLzD2cKJt`3?R|ZLp=4+<);2gMigF0Z9+oAUqW0&mC~`{Oq) zgop;*UR?y-LPOn$dO0wxs1#p}j-v^NM;7#$s*2Eh%gfDn_wO?ob&^qq1IK9R{^m+t zC1#F?-|iAc!r!kb%KMu5v2l+Q%I}!WIsj^FXzBE&{6JA;PBV~9x^40_29_iV8b7Oq zHu7yF)1gw?Hps(be8cCNVG_8B)ZxMX6WJrAFw-6J;^C2yJxtL`X1Dj5f0|3@sD&** zZt)0ARvF!|Yo0oAWR@;(jJvCu?1pQ^FI5vV$M3J+|5 zSavi{htC=dH496L@2P)|u9pOM#RKJS%&&*1rlyK)HeGU2*lT(rEayZiS;mA9f5I7y zy0iHXcewy_o|lGquu0>#KI`rvh}BExdY>H?NBhlnK#;-Gx|@|BAhtje{lgYlvW)A8 z`}`2Tha^pnn!36zLWWkS5FzNu1hNjspe|f279`df{B$tu@iKvDL0=fdls6uinE~AI5YERnXl(rHY1;MeArrd zCm3Ei#hk3ht1gZ%ut_V3bvs?zX&Mbul9tS8&#Fq4e;pV}^wwid_ zbpVc-O2KeCh8+`@lR!)j!gD5a zFtn^D75E;MP*q&j*JovFdt?M(Fd$LcG>7(jt_DOjT3Bly1Z}6k24!#@%T;zC(Mnt? zyr|Nw;%XRw z?-f(otvq|(>m4z#yE2>yiE9K)V82=i*cNK4C(33HYlpPU+ggkz%>%4uM(^SA|Boj2 zzf`m4T184m5cJsj=;FBg5OE`F_fMXm0SF$rUe{u?07%gn+Y%8EivAZy7JZLjALm=s U#jKe3pm1X8Xc}sKP_u*m4>Ce4^Z)<= literal 0 HcmV?d00001 diff --git a/docs/_static/apple-touch-icon-152x152.png b/docs/_static/apple-touch-icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..7bf86af1cba9bc30c70a5f964a308870a7ae988a GIT binary patch literal 5706 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 literal 0 HcmV?d00001 diff --git a/docs/_static/apple-touch-icon-167x167.png b/docs/_static/apple-touch-icon-167x167.png new file mode 100644 index 0000000000000000000000000000000000000000..7e2945647ab34de53ee68d37dc5f9ebca56ed70a GIT binary patch literal 6637 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 literal 0 HcmV?d00001 diff --git a/docs/_static/apple-touch-icon-57x57.png b/docs/_static/apple-touch-icon-57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..25b5a2d8a40437fb7ebb1a6a465925869ec78b14 GIT binary patch literal 1941 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 B7ytkO literal 0 HcmV?d00001 diff --git a/docs/_static/favicon-96x96.png b/docs/_static/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..9d11839a512d5783565f105d24e805d355d9ea32 GIT binary patch literal 3280 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 literal 0 HcmV?d00001 diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 000000000..2fac0848d --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,26 @@ +@import url("theme.css"); + +:root { + --block-bg-opacity: .5; +} + +.wy-side-nav-search { + background-color: #fff; +} + +.getting-started { + background-color: rgba(78, 150, 253, var(--block-bg-opacity)); +} + +.user-guides { + background-color: rgba(0, 169, 154, var(--block-bg-opacity)); +} + +.developer-docs { + background-color: rgba(171, 0, 182, var(--block-bg-opacity)); +} + +.key-ideas +{ + border: 0px +} diff --git a/docs/_static/trlx-logo-512x512.png b/docs/_static/trlx-logo-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d8bdb400c9a0717d1ab6bd19be51b456766b0cb3 GIT binary patch literal 22859 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 literal 0 HcmV?d00001 diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 000000000..4c57ba830 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,2 @@ +{% extends "!layout.html" %} +{% set css_files = css_files + ["_static/style.css"] %} diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 000000000..147ebab99 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +`which sphinx-build` -T -E -b html -d _build/doctrees-readthedocs -D language=en . _build/html diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 000000000..66efc0fec --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,2 @@ +```{include} ../CHANGELOG.md +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..32a8c2df3 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,187 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + + +# -- Project information ----------------------------------------------------- + +project = "trlX" +copyright = "2023, CarperAI" +author = "CarperAI" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "matplotlib.sphinxext.plot_directive", + "sphinx_autodoc_typehints", + "myst_nb", + # "myst_parser", + "sphinx_remove_toctrees", + "sphinx_copybutton", + "sphinx_design", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), + "pytorch": ("https://pytorch.readthedocs.io/", None), +} + +autodoc_preserve_defaults = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = [".rst", ".md"] + +# The master toctree document. +main_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [ + # Sometimes sphinx reads its own outputs as inputs! + "build/html", +] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +autosummary_generate = True +napolean_use_rtype = False + +# -- Options for nbsphinx ----------------------------------------------------- + +# Execute notebooks before conversion: 'always', 'never', 'auto' (default) +# We execute all notebooks, exclude the slow ones using 'exclude_patterns' +nbsphinx_execute = "always" + +# Use this kernel instead of the one stored in the notebook metadata: +# nbsphinx_kernel_name = 'python3' + +# List of arguments to be passed to the kernel that executes the notebooks: +# nbsphinx_execute_arguments = [] + +# If True, the build process is continued even if an exception occurs: +# nbsphinx_allow_errors = True + + +# Controls when a cell will time out (defaults to 30; use -1 for no timeout): +nbsphinx_timeout = 180 + +# Default Pygments lexer for syntax highlighting in code cells: +# nbsphinx_codecell_lexer = 'ipython3' + +# Width of input/output prompts used in CSS: +# nbsphinx_prompt_width = '8ex' + +# If window is narrower than this, input/output prompts are on separate lines: +# nbsphinx_responsive_width = '700px' + +# This is processed by Jinja2 and inserted before each notebook +nbsphinx_prolog = r""" # noqa: E501 +{% set docname = 'docs/' + env.doc2path(env.docname, base=None) %} +.. only:: html + .. role:: raw-html(raw) + :format: html + .. nbinfo:: + Interactive online version: + :raw-html:`Open In Colab` + __ https://github.com/CarperAI/trlx/blob/ + {{ env.config.release }}/{{ docname }} +""" + +# This is processed by Jinja2 and inserted after each notebook +# nbsphinx_epilog = r""" +# """ + +# Input prompt for code cells. "%s" is replaced by the execution count. +# nbsphinx_input_prompt = 'In [%s]:' + +# Output prompt for code cells. "%s" is replaced by the execution count. +# nbsphinx_output_prompt = 'Out[%s]:' + +# Specify conversion functions for custom notebook formats: +# import jupytext +# nbsphinx_custom_formats = { +# '.Rmd': lambda s: jupytext.reads(s, '.Rmd'), +# } + +# Link or path to require.js, set to empty string to disable +# nbsphinx_requirejs_path = '' + +# Options for loading require.js +# nbsphinx_requirejs_options = {'async': 'async'} + +# mathjax_config = { +# 'TeX': {'equationNumbers': {'autoNumber': 'AMS', 'useLabelIds': True}}, +# } + +# Additional files needed for generating LaTeX/PDF output: +# latex_additional_files = ['references.bib'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_book_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Output file base name for HTML help builder. +htmlhelp_basename = "TRLXdoc" + +# -- Extension configuration ------------------------------------------------- + +# Tell sphinx-autodoc-typehints to generate stub parameter annotations including +# types, even if the parameters aren't explicitly documented. +always_document_param_types = True + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + # "logo_only": True, + "show_toc_level": 2, + "repository_url": "https://github.com/CarperAI/trlx", + "use_repository_button": True, # add a "link to repository" button +} + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = "_static/apple-touch-icon-144x144.png" + +html_favicon = "_static/favicon-16x16.png" diff --git a/docs/source/configs.rst b/docs/configs.rst similarity index 86% rename from docs/source/configs.rst rename to docs/configs.rst index da5e1f2e6..0e2abd369 100644 --- a/docs/source/configs.rst +++ b/docs/configs.rst @@ -25,10 +25,10 @@ the specific method being used (i.e. ILQL or PPO) **PPO** -.. autoclass:: trlx.data.method_configs.PPOConfig +.. autoclass:: trlx.trainer.nn.ppo_models.MethodConfig :members: **ILQL** -.. autoclass:: trlx.data.method_configs.ILQLConfig +.. autoclass:: trlx.trainer.nn.ilql_models.ILQLConfig :members: diff --git a/docs/source/data.rst b/docs/data.rst similarity index 100% rename from docs/source/data.rst rename to docs/data.rst diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 000000000..3f518b7d9 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,26 @@ +# Examples + +In the ``examples`` folder you can find several example training tasks. + +Check the configs folder for the associated configs files. + +## [randomwalks](/examples.randomwalks) + +does offline reinforcement on a set of graph random walks to stitch shortest paths +to some destination. + +## simulacra + +optimizes prompts by using [prompts-ratings dataset](https://github.com/JD-P/simulacra-aesthetic-captions). + +## architext + +tries to optimize designs represented textually by minimizing number of rooms (pre-trained model is under a license on hf). + +## ilql_sentiments and ppo_sentiments + +train to generate movie reviews with a positive sentiment, in offline setting – by fitting to IMDB +dataset sentiment scores, and in online setting – by sampling finetuned on IMDB +model and rating samples with learned sentiment reward model, You can tweak +these scripts to your liking and tune hyperparameters to your problem if you +wish to use trlx for some custom task. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 000000000..e663db2d2 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,8 @@ +# Frequently Asked Questions + +```{admonition} How to add a new page to the documentation? +RST primer for Sphinx: https://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html +``` + +We are collecting here answers to frequently asked questions. +Contributions welcome! diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 000000000..aef6f47bb --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,81 @@ +# Glossary of Terms + +```{glossary} +[Agent]() + An agent in reinforcement learning is the entity that interacts with the {term}`Environment` to learn how to maximize its {term}`Reward`. + +[Action]() + An action in reinforcement learning is the signal that the {term}`Agent` provides to the {term}`Environment` to indicate what it wants to do. + + In other words, an action is a scalar value that the agent provides to the environment to indicate what it wants to do. The agent's goal is to maximize the total reward it receives over a sequence of {term}`Steps`. + +[CPU](https://en.wikipedia.org/wiki/Central_processing_unit) + Short for *Central Processing Unit*, CPUs are the standard computational architecture + available in most computers. trlX can run computations on CPUs, but often can achieve + much better performance on {term}`GPU` . + + +[Device](https://en.wikipedia.org/wiki/Device_computing) + The generic name used to refer to the {term}`CPU`, {term}`GPU`, used + by TRLX to perform computations. + +[Environment]() + An environment in reinforcement learning is the system that the agent interacts with. It is the source of {term}`State`, {term}`Action`, and {term}`Reward`. + + In other words, an environment is a system that defines the agent's observation space, action space, and reward function. It is the source of the agent's experience, and the goal of the agent is to maximize the total reward it receives over a sequence of {term}`Steps`. + +[GPU](https://en.wikipedia.org/wiki/Graphics_processing_unit) + Short for *Graphical Processing Unit*, GPUs were originally specialized for operations + related to rendering of images on screen, but now are much more general-purpose. TRLX is + able to target GPUs for fast operations on arrays (see also {term}`CPU`). + +[Policy]() + A policy in reinforcement learning is a function that maps {term}`State` to {term}`Action`. + + In other words, a policy is a function that maps the agent's current state to the action it should take. The agent's goal is to maximize the total reward it receives over a sequence of {term}`Steps`. + +[PPO](https://arxiv.org/abs/1707.06347) + Short for *Proximal Policy Optimization*, PPO is a {term}`Policy Gradient` algorithm + that is able to learn policies in high-dimensional, continuous action spaces. + +[Policy Gradient](https://spinningup.openai.com/en/latest/spinningup/rl_intro3.html#policy-gradients) + Policy gradient methods are a class of reinforcement learning algorithms that are able to + learn policies in high-dimensional, continuous action spaces. + +[Reinforcement Learning](https://en.wikipedia.org/wiki/Reinforcement_learning) + Reinforcement learning (RL) is a machine learning paradigm that trains an agent to maximize its + {term}`Reward` by interacting with an {term}`Environment`. + +[Reward]() + A reward in reinforcement learning is the signal that the {term}`Environment` provides to the {term}`Agent` to indicate how well it is performing. + + In other words, a reward is a scalar value that the environment provides to the agent to indicate how well it is performing. The agent's goal is to maximize the total reward it receives over a sequence of {term}`Steps`. + +[Rollout]() + A rollout in reinforcement learning is the process of executing a {term}`Policy`, starting from a specific state in the {term}`Environment`, and following it to the end to obtain a complete trajectory of {term}`State`, {term}`Action`, and {term}`Reward`. + + In other words, a Rollout is a simulation of a policy's behavior in the environment over a fixed number of {term}`Steps` or until a terminal state is reached. It provides a means of evaluating the {term}`Policy`'s performance, as the total reward collected over the trajectory can be used as a measure of its effectiveness. + +[State]() + A state in reinforcement learning is the observation that the {term}`Environment` provides to the {term}`Agent`. + +[Steps]() + A step in reinforcement learning is the process of taking a single {term}`Action` in the {term}`Environment`, and observing the resulting {term}`State` and {term}`Reward`. + + In other words, a step is a single iteration of the environment's dynamics, where the agent takes an action and receives a reward and a new state. The agent's goal is to maximize the total reward it receives over a sequence of steps. + +[Trajectory] + + In a {term}`PPO` (Proximal Policy Optimization) setup, a fixed-length trajectory + segment refers to a fixed number of time steps in an episode of an + environment.At each time step, the agent takes an action based on the current + state and receives a reward from the environment. By using fixed-length + trajectory segments, the agent's behavior is divided into chunks of a fixed + length, and each chunk is used for a single PPO update. This allows for more + efficient use of the {term}`Agent`'s experience by breaking it into smaller pieces, and + it also helps to stabilize the learning process by making the training updates + less sensitive to the length of the episode. Fixed-length trajectory segments + are often used in Reinforcement Learning (RL) algorithms, including {term}`PPO`, to + update the policy network. + +``` diff --git a/docs/source/index.rst b/docs/index.rst similarity index 76% rename from docs/source/index.rst rename to docs/index.rst index 782e29ecc..fe381aaf0 100644 --- a/docs/source/index.rst +++ b/docs/index.rst @@ -8,6 +8,15 @@ Welcome to trlX's documentation! trlX is a library made for training large language models using reinforcement learning. It currently supports training using PPO or ILQL for models up to 20B using Accelerate. +Installation +------------ +.. tab-set:: + + .. code-block:: bash + + pip install "trlx" + + .. toctree:: :maxdepth: 2 :caption: Contents: @@ -17,8 +26,19 @@ currently supports training using PPO or ILQL for models up to 20B using Acceler orchestrator configs pipeline + trainer examples +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Resources + + changelog + faq + glossary + + Indices and tables ================== diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 000000000..91361720e --- /dev/null +++ b/docs/models.md @@ -0,0 +1 @@ +# Models diff --git a/docs/source/orchestrator.rst b/docs/orchestrator.rst similarity index 100% rename from docs/source/orchestrator.rst rename to docs/orchestrator.rst diff --git a/docs/pipeline.md b/docs/pipeline.md new file mode 100644 index 000000000..066a28de5 --- /dev/null +++ b/docs/pipeline.md @@ -0,0 +1,30 @@ +# Pipelines and Rollout Store + +## Pipelines + +Pipelines in trlX provide a way to read from a dataset. They are used to fetch data from the dataset and feed it to the models for training or inference. The pipelines allow for efficient processing of the data and ensure that the models have access to the data they need for their tasks. + +## Rollout Stores + +Rollout stores in trlX are used to store experiences created for the models by the orchestrator. The experiences in the rollout stores serve as the training data for the models. The models use the experiences stored in their rollout stores to learn and improve their behavior. The rollout stores provide a convenient and efficient way for the models to access the experiences they need for training. + +## General + +.. autoclass:: trlx.pipeline.BasePipeline + :members: + +.. autoclass:: trlx.pipeline.BaseRolloutStore + :members: + +## PPO + +.. autoclass:: trlx.pipeline.ppo_pipeline.PPORolloutStorage + :members: + +## ILQL + +.. autoclass:: trlx.pipeline.offline_pipeline.PromptPipeline + :members: + +.. autoclass:: trlx.pipeline.offline_pipeline.ILQLRolloutStorage + :members: diff --git a/docs/requirements.txt b/docs/requirements.txt index 7a33f300e..3052a2f0c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,11 +1,20 @@ -accelerate==0.12.0 -datasets==2.4.0 -deepspeed==0.7.3 -einops==0.4.1 -numpy==1.23.2 -sphinx==4.0.0 -sphinx_rtd_theme +accelerate +commonmark +datasets +deepspeed +docutils +jupyter-sphinx +matplotlib +myst-nb +nbsphinx +Pygments +ray +readthedocs-sphinx-ext +rich +sphinx-autodoc-typehints +sphinx-book-theme +sphinx-copybutton +sphinx-design +sphinx-remove-toctrees torchtyping -tqdm==4.64.0 -transformers==4.21.2 -wandb==0.13.2 +transformers diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 0a9a11c86..000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,54 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys - -import sphinx_rtd_theme - -sys.path.insert(0, os.path.abspath('../..')) - - -# -- Project information ----------------------------------------------------- - -project = 'trlX' -copyright = '2022, CarperAI' -author = 'CarperAI' - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. - -extensions = ['sphinx_rtd_theme', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.autosectionlabel'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] diff --git a/docs/source/examples.rst b/docs/source/examples.rst deleted file mode 100644 index 6f5db49d1..000000000 --- a/docs/source/examples.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. _examples: - -Examples -************************ - -In the ``examples`` folder you can find several example training tasks. Check -the configs folder for the associated configs files. ``examples.randomwalks`` -does offline reinforcement on a set of graph random walks to stitch shortest -paths to some destination. ``examples.simulacra`` optimizes prompts by using -prompts-ratings dataset (https://github.com/JD-P/simulacra-aesthetic-captions). -``examples.architext`` tries to optimize designs represented textually by -minimazing number of rooms (pretrained model is under a license on hf). -``examples.ilql_sentiments`` and ``examples.ppo_sentiments`` train to generate -movie reviews with a positive sentiment, in offline setting – by fitting to IMDB -dataset sentiment scores, and in online setting – by sampling finetuned on IMDB -model and rating samples with learned sentiment reward model, You can tweak -these scripts to your liking and tune hyperparameters to your problem if you -wish to use trlx for some custom task. diff --git a/docs/source/pipeline.rst b/docs/source/pipeline.rst deleted file mode 100644 index 04d1a8c04..000000000 --- a/docs/source/pipeline.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _pipeline: - -Pipelines -************************ - -Pipelines are how you read from a dataset with trlX. Rollout stores are how models store experiences created -for them by the orchestrator. It is these experiences in their rollout store that they are trained on. - -**General** - -.. autoclass:: trlx.pipeline.BasePipeline - :members: - -.. autoclass:: trlx.pipeline.BaseRolloutStore - :members: - -**PPO** - -.. autoclass:: trlx.pipeline.ppo_pipeline.PPORolloutStorage - :members: - -**ILQL** - -.. autoclass:: trlx.pipeline.offline_pipeline.PromptPipeline - :members: - -.. autoclass:: trlx.pipeline.offline_pipeline.ILQLRolloutStorage - :members: diff --git a/docs/source/trainer.rst b/docs/trainer.rst similarity index 100% rename from docs/source/trainer.rst rename to docs/trainer.rst diff --git a/examples/experiments/grounded_program_synthesis/lang.py b/examples/experiments/grounded_program_synthesis/lang.py index d2436c3f6..9c3f076c0 100644 --- a/examples/experiments/grounded_program_synthesis/lang.py +++ b/examples/experiments/grounded_program_synthesis/lang.py @@ -109,7 +109,7 @@ def __call__(self, statement_string: str): # This is used to store the input, output and the function template. # Input : List given as an input to the function. # function_template : The atomic function in a given DSL Grammar -# Output : Transformed outut by applying function on the input. +# Output : Transformed output by applying function on the input. generation_template = {"function_template": "NONE", "output": "NONE", "input": []} diff --git a/examples/experiments/grounded_program_synthesis/train_trlx.py b/examples/experiments/grounded_program_synthesis/train_trlx.py index 8071fc210..6cfe793a0 100644 --- a/examples/experiments/grounded_program_synthesis/train_trlx.py +++ b/examples/experiments/grounded_program_synthesis/train_trlx.py @@ -17,7 +17,7 @@ def __init__(self): self.train_data = json.load(f) with open("dataset/test.json", "r") as f: self.test_data = json.load(f) - logger.info("Sucessfully loaded the dataset") + logger.info("Successfully loaded the dataset") def load_datapoints(self, split="train"): if split == "train": @@ -74,7 +74,7 @@ def main(hparams={}): if __name__ == "__main__": - # TEST REWARD FUNTION + # TEST REWARD FUNCTION assert (reward_fn(["Input: 1 Output: [-4,-5,-2] Function: div_n(reverse([-2, -5, -4]),1)"])) == [1] assert (reward_fn(["Input: 1 Output: [-4,-5,-2] Function: div_n(reverse([-2, -5, -a]),1)"])) == [-1] assert (reward_fn(["Input: 1 Output: [-4,-5,-2] Function: div_n(reverse([-2, -5, -3]),1)"])) == [-0.5] diff --git a/pyproject.toml b/pyproject.toml index f6ec87f55..40e261241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,8 @@ profile = "black" [tool.black] line-length = 120 + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --doctest-modules -vv --cov=trlx/ " +testpaths = ["tests"] diff --git a/setup.cfg b/setup.cfg index 4a54f7747..2bf3cca73 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,38 +1,65 @@ [metadata] -name = trlx -author = Alex Havrilla -version = 0.3.0 -url = https://github.com/CarperAI/trlx -description = A repo for distributed training of language models with Reinforcement Learning via Human Feedback (RLHF) +author = The CarperAI team +description = Transformer Reinforcement Learning X: A repo for distributed training of language models with Reinforcement Learning via Human Feedback (RLHF) +keywords = + Deep Learning + Reinforcement Learning +license = MIT long_description = file: README.md long_description_content_type = text/markdown -license = MIT +name = trlx +url = https://github.com/CarperAI/trlx +version = v0.4.0-dev [options] +python_requires = + >=3.8.0 packages = find: install_requires = accelerate>=0.12.0 + accelerate~=0.15.0 datasets deepspeed>=0.7.3 einops>=0.4.1 + networkx + numpy>=1.17 numpy>=1.23.2 + packaging>=20.0 + psutil + pyyaml + ray + ray>=2.0.1 + tabulate>=0.9.0 + torch~=1.13.0 torchtyping - transformers>=4.21.2 + torchtyping~=0.1.4 tqdm + transformers>=4.21.2 + typing-extensions~=3.10.0 rich - wandb>=0.13.5 - ray>=2.0.1 - tabulate>=0.9.0 - networkx + +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + Intended Audience :: Education + Intended Audience :: Science/Research + License :: OSI Approved :: MIT>=0.13.5 + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Topic :: Scientific/Engineering :: Artificial Intelligence [options.extras_require] -bnb = bitsandbytes +bnb = + bitsandbytes +wandb = + wandb dev = black isort flake8 - pre-commit - pytest + pre-commit>= 2.21.0 + pytest>=6.0 pytest-cov [options.packages.find] diff --git a/setup.py b/setup.py index 606849326..e3d859351 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,35 @@ -from setuptools import setup +from setuptools import find_packages, setup -setup() +extras = {} +extras["quality"] = [ + "black ~= 22.10.0", + "isort >= 5.5.4", + "flake8 >= 3.8.3", +] +extras["docs"] = ["sphinx==4.0.0", "sphinx_rtd_theme"] +extras["test_prod"] = ["pytest", "pytest-xdist", "pytest-subtests", "parameterized"] +extras["test_dev"] = [ + "datasets", + "evaluate", + "transformers", + "scipy", + "scikit-learn", + "deepspeed<0.7.7", + "tqdm", +] +extras["testing"] = extras["test_prod"] + extras["test_dev"] +extras["rich"] = ["rich"] + +extras["test_trackers"] = ["wandb"] +extras["dev"] = extras["quality"] + extras["testing"] + extras["rich"] + + +setup( + packages=find_packages("."), + entry_points={ + "console_scripts": [ + "trlx=trlx.commands.trlx_cli:main", + ] + }, + +) diff --git a/trlx/__init__.py b/trlx/__init__.py index 7b26a92a9..e84114dc7 100644 --- a/trlx/__init__.py +++ b/trlx/__init__.py @@ -1,2 +1 @@ from .trlx import train -from .utils import logging diff --git a/trlx/orchestrator/offline_orchestrator.py b/trlx/orchestrator/offline_orchestrator.py index d426e7aad..90207b19a 100644 --- a/trlx/orchestrator/offline_orchestrator.py +++ b/trlx/orchestrator/offline_orchestrator.py @@ -1,16 +1,11 @@ -import os from typing import List, Union import numpy as np import torch -from rich.console import Console -from rich.table import Table -import trlx.utils.logging as logging from trlx.orchestrator import Orchestrator, register_orchestrator from trlx.pipeline.offline_pipeline import ILQLRolloutStorage - -logger = logging.get_logger(__name__) +from trlx.utils import print_rank_0 def tokenize_dialogue( # noqa: C901 @@ -65,8 +60,6 @@ def make_experience(self, samples, rewards, max_length=2048): """ Tokenizes samples and shapes rewards into proper tensors and then inserts the resulting dataset into the trainer """ - logger.info("Collecting rollouts") - if self.trainer.tokenizer: samples = [tokenize_dialogue(s, self.trainer.tokenizer, max_length) for s in samples] @@ -91,29 +84,26 @@ def make_experience(self, samples, rewards, max_length=2048): all_actions_ixs.append(torch.hstack(actions_ixs)) all_states_ixs.append(states_ixs) - if self.trainer.tokenizer and os.environ.get("RANK", "0") == "0": - logger.info("Logging sample example") + if self.trainer.tokenizer: prompt = self.trainer.tokenizer.decode(all_input_ids[0][: all_states_ixs[0][1]]) response = self.trainer.tokenizer.decode(all_input_ids[0][all_states_ixs[0][1] :]) - columns = ["Prompt", "Response", "Reward"] - table = Table(*columns, title="Sample Example", show_lines=True) - table.add_row(prompt, response, str(rewards[0])) - Console().print(table) + print_rank_0("[Sample example]") + print_rank_0("Prompt: ", prompt) + print_rank_0("Response: ", response) + print_rank_0("Reward: ", rewards[0]) sample_lengths = np.array(list(map(len, all_input_ids))) output_lengths = np.array(list(map(len, all_actions_ixs))) prompt_lengths = sample_lengths - output_lengths returns = torch.tensor(rewards, dtype=float) - if os.environ.get("RANK", "0") == "0": - logger.info("Logging experience string statistics") - columns = ["Prompt Length", "Output Length", "Sample Length"] - table = Table(*columns, title="Experience String Stats (mean ∈ \[min, max])", show_lines=True) - row = [] - for lengths in [prompt_lengths, output_lengths, sample_lengths]: - row.append(f"{lengths.mean():.2f} ∈ [{min(lengths)}, {max(lengths)}]") - table.add_row(*row) - Console().print(table) + def string_stats(name: str, xs: np.array): + return f"[Mean {name}] {xs.mean():.2f} ∈ [{min(xs)}, {max(xs)}]" + + print_rank_0(string_stats("prompt length", prompt_lengths)) + print_rank_0(string_stats("output length", output_lengths)) + print_rank_0(string_stats("sample length", sample_lengths)) + print_rank_0(string_stats("return", returns)) returns = (returns - returns.mean()) / (returns.std() + 1e-30) rewards = [torch.zeros(len(x)) for x in all_actions_ixs] diff --git a/trlx/orchestrator/ppo_orchestrator.py b/trlx/orchestrator/ppo_orchestrator.py index 4f73f01c3..cd2b8f674 100644 --- a/trlx/orchestrator/ppo_orchestrator.py +++ b/trlx/orchestrator/ppo_orchestrator.py @@ -1,11 +1,9 @@ -import os from time import time import ray import torch import torch.nn.functional as F -import trlx.utils.logging as logging from trlx.data.accelerate_base_datatypes import PromptBatch from trlx.data.ppo_types import PPORLElement from trlx.orchestrator import Orchestrator, register_orchestrator @@ -14,8 +12,6 @@ from trlx.utils import Clock from trlx.utils.modeling import RunningMoments, logprobs_from_logits -logger = logging.get_logger(__name__) - @register_orchestrator class PPOOrchestrator(Orchestrator): @@ -57,24 +53,11 @@ def score(self, samples): def make_experience(self, num_rollouts: int = 1024, iter_count: int = 0): # noqa: """ Takes `num_rollouts` prompts from `pipeline`, samples model and computes the - KL againts a reference model. It then appends PPOElements to trainer's `store` + KL against a reference model. It then appends PPOElements to trainer's `store` """ - logger.info("Collecting rollouts") - tbar = logging.tqdm( - total=num_rollouts, - disable=os.environ.get("RANK", 0) != "0", - desc=f"[rollout 0 / {num_rollouts}]", - # Lower progress bar by 1 if we're in WARNING mode or above to avoid hiding high priority progress - # bars (e.g. loss progress in trainers) - position=logging.get_verbosity() >= logging.WARNING, - # Leave progress bar if we're in INFO mode or lower to avoid spamming in suppressed verbosity levels - leave=logging.get_verbosity() < logging.WARNING, - ) - ppo_rl_elements = [] stats = {} clock = Clock() - while len(ppo_rl_elements) < num_rollouts: # Get next batch in prompt dataset and refresh if exhausted try: @@ -92,7 +75,7 @@ def make_experience(self, num_rollouts: int = 1024, iter_count: int = 0): # noq str_samples, str_prompts, str_outputs = self.trainer.decode(query_tensors, samples) # Convert trimmed samples back into tensors for another head pass - # This can be defered, instead letting the pass to made over the original samples + # This can be deferred, instead letting the pass to made over the original samples # after unbinding and truncating operations lower are fixed outputs = self.trainer.tokenizer(str_outputs).input_ids outputs = list(map(torch.LongTensor, outputs)) @@ -215,7 +198,6 @@ def make_experience(self, num_rollouts: int = 1024, iter_count: int = 0): # noq rewards = -self.trainer.kl_ctl.value * (logprobs - ref_logprobs) rewards = [rs[start : ends[ix]] for ix, rs in enumerate(rewards)] - rollout_count = 0 for ix in range(n): if len(rewards[ix]) == 0 or len(all_logprobs[ix]) == 0: continue @@ -231,11 +213,8 @@ def make_experience(self, num_rollouts: int = 1024, iter_count: int = 0): # noq rewards=rewards[ix], ) ) - rollout_count += 1 + exp_time = clock.tick() - tbar.set_description(f"[rollout {len(ppo_rl_elements)} / {num_rollouts}]") - tbar.update(min(rollout_count, num_rollouts)) - tbar.close() stats["kl_ctl_value"] = self.trainer.kl_ctl.value stats["time/exp"] = exp_time diff --git a/trlx/trainer/__init__.py b/trlx/trainer/__init__.py index 68142bab2..63bf189da 100644 --- a/trlx/trainer/__init__.py +++ b/trlx/trainer/__init__.py @@ -76,7 +76,7 @@ def sample(self, prompts: Iterable[str], length: int, n_samples: int) -> Iterabl :param prompts: List of prompts to tokenize and use as context - :param length: How many new tokens to genrate for each prompt + :param length: How many new tokens to generate for each prompt :type length: int :param n_samples: Default behavior is to take number of prompts as this diff --git a/trlx/trainer/accelerate_base_trainer.py b/trlx/trainer/accelerate_base_trainer.py index 11c0ce68d..a76ee5cbb 100644 --- a/trlx/trainer/accelerate_base_trainer.py +++ b/trlx/trainer/accelerate_base_trainer.py @@ -13,9 +13,9 @@ from ray.air.checkpoint import Checkpoint from rich.console import Console from rich.table import Table +from tqdm import tqdm from transformers import AutoTokenizer -import trlx.utils.logging as logging from trlx.data.configs import TRLConfig from trlx.trainer import BaseRLTrainer, register_trainer from trlx.utils import ( @@ -24,6 +24,7 @@ get_git_tag, get_optimizer_class, get_scheduler_class, + print_rank_0, significant, ) from trlx.utils.modeling import ( @@ -34,8 +35,6 @@ parse_delta_kwargs, ) -logger = logging.get_logger(__name__) - @register_trainer class AccelerateRLTrainer(BaseRLTrainer): @@ -117,8 +116,6 @@ def setup_model(self): """ Returns a model derived from an instance's TRLConfig """ - logger.info(f"Initializing model: {self.config.model.model_path}") - # Retrieves model equipped for ppo, ilql, etc model = self.get_arch(self.config) if self.config.model.model_arch_type == "seq2seq": @@ -282,7 +279,8 @@ def add_eval_pipeline(self, eval_pipeline): def evaluate(self): # noqa: C901 """Samples model on `eval_prompts`, logs stats with `reward_fn` or `metric_fn` if provided""" - logger.info("Evaluating model") + stats = {} + table = [] # Do multiple evaluations over a single list in `gen_kwargs` if present if self.generate_sweep_kwarg is not None: @@ -290,22 +288,7 @@ def evaluate(self): # noqa: C901 else: gen_sweep_values = [None] - desc = [ - f"generation sweep 0/{len(gen_sweep_values)}", - f"eval batch 0/{len(self.eval_dataloader)}", - ] - tbar = logging.tqdm( - total=len(self.eval_dataloader) * len(gen_sweep_values), - desc=f"[{' | '.join(desc)}]", - disable=not self.accelerator.is_main_process, - position=0, - leave=True, - ) - - stats = {} - table = [] - - for i_sweep, gen_sweep_value in enumerate(gen_sweep_values): + for gen_sweep_value in gen_sweep_values: # A dedicated suffix for wandb logging if gen_sweep_value is not None: sweep_suffix = f"@{gen_sweep_arg}={gen_sweep_value}" @@ -316,7 +299,7 @@ def evaluate(self): # noqa: C901 all_prompts = [] prompt_sizes = [] generate_time = time() - for i_prompt, prompts in enumerate(self.eval_dataloader): + for prompts in self.eval_dataloader: if self.generate_sweep_kwarg: samples = self.generate_eval(**prompts, **{gen_sweep_arg: gen_sweep_value}) else: @@ -343,14 +326,6 @@ def evaluate(self): # noqa: C901 torch.tensor(prompts.input_ids.shape[1], device=samples.device).repeat(len(prompts.input_ids)) ) - desc = [ - f"generation sweep {i_sweep + 1}/{len(gen_sweep_values)}", - f"eval batch {i_prompt + 1}/{len(self.eval_dataloader)}", - ] - tbar.set_description(f"[{' | '.join(desc)}]") - tbar.update() - tbar.close() - stats["time/generate"] = time() - generate_time samples = self.accelerator.gather(torch.vstack(all_samples)) @@ -365,7 +340,6 @@ def evaluate(self): # noqa: C901 # in online setting, compute the reward for validation if self.reward_fn: - logger.info("Computing rewards") rewards = torch.tensor( self.reward_fn( samples=str_samples, @@ -383,7 +357,6 @@ def evaluate(self): # noqa: C901 # additionally log any other metrics if self.metric_fn: - logger.info("Computing metrics") metric_time = time() metrics = self.metric_fn( samples=str_samples, @@ -412,7 +385,6 @@ def evaluate(self): # noqa: C901 table.append(list(zip(*columns_data))) # Log and display evaluation metrics - logger.info("Summarizing evaluation") if self.accelerator.is_main_process: rows = sum(list(map(list, zip(*table))), []) @@ -423,9 +395,9 @@ def evaluate(self): # noqa: C901 table_title += f" {k}: {significant(x)}" rich_table = Table(*columns, title=table_title, show_lines=True) + for ix in range(max(min(3, len(rows)), len(gen_sweep_values))): rich_table.add_row(*[str(significant(x)) for x in rows[ix]]) - Console().print(rich_table) if not ray.is_initialized(): if self.config.train.tracker == "wandb": @@ -433,6 +405,8 @@ def evaluate(self): # noqa: C901 stats["samples"] = wandb.Table(columns, rows) + Console().print(rich_table) + self.nth_evaluation += 1 return stats @@ -440,13 +414,11 @@ def learn(self): # noqa: C901 """ Samples batches from `self.store`, updates model and periodically evaluates it on `self.eval_dataloader` """ - logger.info("Starting training") - self.generate_sweep_kwarg = None for k, v in self.config.method.gen_kwargs.items(): if isinstance(v, list): if self.generate_sweep_kwarg is not None: - logger.info("Only a single sweep is allowed, {k} is going to be set to {v[0]}") + print_rank_0("Only a single sweep is allowed, {k} is going to be set to {v[0]}") self.generate_kwargs[k] = v[0] else: self.generate_sweep_kwarg = (k, v) @@ -468,12 +440,10 @@ def learn(self): # noqa: C901 results = self.evaluate() self.accelerator.log(results, step=self.iter_count) - tbar = logging.tqdm( + tbar = tqdm( initial=self.iter_count, total=self.total_steps, disable=not self.accelerator.is_local_main_process, - position=0, - leave=True, ) best_reward = -float("inf") @@ -521,7 +491,7 @@ def learn(self): # noqa: C901 torch.distributed.all_reduce(do_save, torch.distributed.ReduceOp.MAX) if do_save: best_path = f"{self.config.train.checkpoint_dir}/best_checkpoint" - logger.info(f"Saving the best state so far into {best_path}") + print_rank_0(f"saving the best state so far into {best_path}") self.save(best_path) # Report the metrics to Ray Tune. @@ -535,8 +505,8 @@ def learn(self): # noqa: C901 if not ray.is_initialized(): self.accelerator.log(stats, step=self.iter_count) - desc = " | ".join(f"{k}: {v:.2f}" for k, v in stats.items() if k.startswith("loss")) - tbar.set_description(f"[{desc}]") + desc = ", ".join(f"{k}: {v:.2f}" for k, v in stats.items() if k.startswith("loss")) + tbar.set_description(desc) tbar.update() if self.iter_count >= self.total_steps: @@ -546,7 +516,6 @@ def learn(self): # noqa: C901 self.post_backward_callback() self.post_epoch_callback() - tbar.close() @abstractmethod def get_arch(self, config: TRLConfig): diff --git a/trlx/trainer/accelerate_ilql_trainer.py b/trlx/trainer/accelerate_ilql_trainer.py index 35d0031b6..3c059b868 100644 --- a/trlx/trainer/accelerate_ilql_trainer.py +++ b/trlx/trainer/accelerate_ilql_trainer.py @@ -87,7 +87,7 @@ def save_pretrained(self, directory: Optional[str] = None): of the Trainer config checkpoint dir named "hf_model" (e.g. `/ckpts/hf_model`). """ # TODO: Support saving with `transformers.PreTrainedModel.save_pretrained`. - # This is currently not supported becasue `nn.ilql_models.CausalLMWithValueHeads` + # This is currently not supported because `nn.ilql_models.CausalLMWithValueHeads` # requires a custom `generate` method using its (value/q) heads to steer # sampling - something that is not possible with the default # `transformers.PreTrainedModel.generate`. diff --git a/trlx/trainer/nemo/gpt.py b/trlx/trainer/nemo/gpt.py index 89eb2554b..ca1abc51f 100644 --- a/trlx/trainer/nemo/gpt.py +++ b/trlx/trainer/nemo/gpt.py @@ -666,7 +666,7 @@ def fwd_output_and_loss_func(batch: List[torch.Tensor], model, checkpoint_activa ) else: # In-between stages are given data via the pipeline engine - # Still need to specify thes arguments to avoid errors + # Still need to specify these arguments to avoid errors model_output = model(input_ids=None, position_ids=None, attention_mask=None) def gather_ntc(t: torch.Tensor): diff --git a/trlx/trlx.py b/trlx/trlx.py index a6b269f81..fb1958544 100644 --- a/trlx/trlx.py +++ b/trlx/trlx.py @@ -34,14 +34,14 @@ def train( prompts (List[str]): Prompts to sample off from during online training eval_prompts (List[str]): Prompts to periodically validate training on metric_fn (Optional[Callable[[List[str], List[str], List[str]], Dict[str, List[float]]]]): - Function to compute statistics on batches of gnerated samples. Its arguments are the same + Function to compute statistics on batches of generated samples. Its arguments are the same as in `reward_fn` (`samples`, `prompts`, `outputs`) but the return is dictionary with keys as metric's name and values and lists of numeric values per each sample in batch config (Optional[TRLConfig]): TRL configuration object to override default settings logit_mask (Optional[List]): Bigram masking matrix stop_sequences (Optional[List[str]]): String sequences to trim generations (either for experience or evaluation) up to its - encounter in them. Generatations will not contain them and also will be right-stripped + encounter in them. Generations will not contain them and also will be right-stripped """ if reward_fn is not None: diff --git a/trlx/utils/logging.py b/trlx/utils/logging.py deleted file mode 100644 index 79badb4a3..000000000 --- a/trlx/utils/logging.py +++ /dev/null @@ -1,340 +0,0 @@ -# Copyright 2023 Optuna, Hugging Face, CarperAI -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Logging utilities.""" - -import logging -import os -import sys -import threading -from logging import CRITICAL # NOQA -from logging import DEBUG # NOQA -from logging import ERROR # NOQA -from logging import FATAL # NOQA -from logging import INFO # NOQA -from logging import NOTSET # NOQA -from logging import WARN # NOQA -from logging import WARNING # NOQA -from typing import Optional - -import torch -from tqdm import auto as tqdm_lib - -_lock = threading.Lock() -_default_handler: Optional[logging.Handler] = None - -log_levels = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL, -} - -_default_log_level = logging.INFO - - -def _get_default_logging_level(): - """ - If `TRLX_VERBOSITY` env var is set to one of the valid choices, return that as the new default level. If it is - not - fall back to `_default_log_level` - """ - env_level_str = os.getenv("TRLX_VERBOSITY", None) - if env_level_str: - if env_level_str.lower() in log_levels: - return log_levels[env_level_str.lower()] - else: - logging.getLogger().warning( - f"Unknown option TRLX_VERBOSITY={env_level_str}, " f"has to be one of: { ', '.join(log_levels.keys()) }" - ) - return _default_log_level - - -def _get_library_name() -> str: - return __name__.split(".")[0] - - -def _get_library_root_logger() -> logging.Logger: - return logging.getLogger(_get_library_name()) - - -def _configure_library_root_logger() -> None: - global _default_handler - - with _lock: - if _default_handler: - # This library has already configured the library root logger. - return - _default_handler = logging.StreamHandler() # Set sys.stderr as stream. - _default_handler.flush = sys.stderr.flush - - # Apply our default configuration to the library root logger. - library_root_logger = _get_library_root_logger() - library_root_logger.addHandler(_default_handler) - library_root_logger.setLevel(_get_default_logging_level()) - library_root_logger.propagate = False - - -def _reset_library_root_logger() -> None: - global _default_handler - - with _lock: - if not _default_handler: - return - - library_root_logger = _get_library_root_logger() - library_root_logger.removeHandler(_default_handler) - library_root_logger.setLevel(logging.NOTSET) - _default_handler = None - - -def get_log_levels_dict(): - return log_levels - - -class MultiProcessAdapter(logging.LoggerAdapter): - """A logger adapter for handling multi-process logging""" - - def log(self, level, msg, *args, **kwargs): - """ - Consumes an additional kwarg called `ranks` to determine which processes should log. - NOTE: To specify all processes, pass in an empty list `ranks=[]` - - Default: ["0"], i.e. only the main process logs - """ - # By default, silence all non-main processes - ranks = kwargs.pop("ranks", ["0"]) - should_log = os.environ.get("RANK", "0") in ranks or len(ranks) == 0 - if self.isEnabledFor(level) and should_log: - msg, kwargs = self.process(msg, kwargs) - self.logger._log(level, msg, args, **kwargs) - - def process(self, msg, kwargs): - this_rank = torch.distributed.get_rank() if torch.distributed.is_initialized() else 0 - return f"[RANK {this_rank}] {msg}", kwargs - - -def get_logger(name: Optional[str] = None) -> MultiProcessAdapter: - """ - Returns a `logging.Logger` for `name` that can handle multiple processes - - Args: - name: Name of the logger - - Usage: - >> logger = get_logger(__name__) - >> logger.debug("Check the...", ranks=["0", "1"]) # Only main and rank 1 log - """ - if name is None: - name = _get_library_name() - _configure_library_root_logger() - logger = logging.getLogger(name) - return MultiProcessAdapter(logger, {}) - - -def get_verbosity() -> int: - """ - Return the current level for trlx's root logger as an int. - Returns: - `int`: The logging level. - - trlx has following logging levels: - - 50: `trlx.logging.CRITICAL` or `trlx.logging.FATAL` - - 40: `trlx.logging.ERROR` - - 30: `trlx.logging.WARNING` or `trlx.logging.WARN` - - 20: `trlx.logging.INFO` - - 10: `trlx.logging.DEBUG` - - """ - - _configure_library_root_logger() - return _get_library_root_logger().getEffectiveLevel() - - -def set_verbosity(verbosity: int) -> None: - """ - Set the verbosity level for trlX's root logger. - Args: - verbosity (`int`): - Logging level, e.g., one of: - - `trlx.logging.CRITICAL` or `trlx.logging.FATAL` - - `trlx.logging.ERROR` - - `trlx.logging.WARNING` or `trlx.logging.WARN` - - `trlx.logging.INFO` - - `trlx.logging.DEBUG` - """ - - _configure_library_root_logger() - _get_library_root_logger().setLevel(verbosity) - - -def disable_default_handler() -> None: - """Disable the default handler of trlx's root logger.""" - - _configure_library_root_logger() - - assert _default_handler is not None - _get_library_root_logger().removeHandler(_default_handler) - - -def enable_default_handler() -> None: - """Enable the default handler of trlx's root logger.""" - - _configure_library_root_logger() - - assert _default_handler is not None - _get_library_root_logger().addHandler(_default_handler) - - -def add_handler(handler: logging.Handler) -> None: - """Adds a handler to trlx's root logger.""" - - _configure_library_root_logger() - - assert handler is not None - _get_library_root_logger().addHandler(handler) - - -def remove_handler(handler: logging.Handler) -> None: - """Removes given handler from the trlx's root logger.""" - - _configure_library_root_logger() - - assert handler is not None and handler not in _get_library_root_logger().handlers - _get_library_root_logger().removeHandler(handler) - - -def disable_propagation() -> None: - """ - Disable propagation of the library log outputs. Note that log propagation is disabled by default. - """ - - _configure_library_root_logger() - _get_library_root_logger().propagate = False - - -def enable_propagation() -> None: - """ - Enable propagation of the library log outputs. Please disable the trlx's default handler to prevent - double logging if the root logger has been configured. - """ - - _configure_library_root_logger() - _get_library_root_logger().propagate = True - - -def enable_explicit_format() -> None: - """ - Enable explicit formatting for every trlx's logger. The explicit formatter is as follows: - ``` - [ASCTIME] [LEVELNAME] [FILENAME:LINE NUMBER:FUNCNAME] MESSAGE - ``` - All handlers currently bound to the root logger are affected by this method. - """ - handlers = _get_library_root_logger().handlers - - for handler in handlers: - formatter = logging.Formatter( - "[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d:%(funcName)s] %(message)s" - ) - handler.setFormatter(formatter) - - -def reset_format() -> None: - """ - Resets the formatting for trlx's loggers. - All handlers currently bound to the root logger are affected by this method. - """ - handlers = _get_library_root_logger().handlers - - for handler in handlers: - handler.setFormatter(None) - - -def warning_advice(self, *args, **kwargs): - """ - This method is identical to `logger.warning()`, but if env var TRLX_NO_ADVISORY_WARNINGS=1 is set, this - warning will not be printed - """ - no_advisory_warnings = os.getenv("TRLX_NO_ADVISORY_WARNINGS", False) - if no_advisory_warnings: - return - self.warning(*args, **kwargs) - - -logging.Logger.warning_advice = warning_advice - - -class EmptyTqdm: - """Dummy tqdm which doesn't do anything.""" - - def __init__(self, *args, **kwargs): # pylint: disable=unused-argument - self._iterator = args[0] if args else None - - def __iter__(self): - return iter(self._iterator) - - def __getattr__(self, _): - """Return empty function.""" - - def empty_fn(*args, **kwargs): # pylint: disable=unused-argument - return - - return empty_fn - - def __enter__(self): - return self - - def __exit__(self, type_, value, traceback): - return - - -_tqdm_active = True - - -class _tqdm_cls: - def __call__(self, *args, **kwargs): - if _tqdm_active: - return tqdm_lib.tqdm(*args, **kwargs) - else: - return EmptyTqdm(*args, **kwargs) - - def set_lock(self, *args, **kwargs): - self._lock = None - if _tqdm_active: - return tqdm_lib.tqdm.set_lock(*args, **kwargs) - - def get_lock(self): - if _tqdm_active: - return tqdm_lib.tqdm.get_lock() - - -tqdm = _tqdm_cls() - - -def is_progress_bar_enabled() -> bool: - """Return a boolean indicating whether tqdm progress bars are enabled.""" - global _tqdm_active - return bool(_tqdm_active) - - -def enable_progress_bar(): - """Enable tqdm progress bar.""" - global _tqdm_active - _tqdm_active = True - - -def disable_progress_bar(): - """Disable tqdm progress bar.""" - global _tqdm_active - _tqdm_active = False